在之前的博客里我们介绍了Karpenter的基本概念和部署的详细步骤,在这个博客里我们会展开讨论如何利用Spot实例来进行进一步进行成本优化。
1.关于Spot实例
Spot实例是一种使用备用 EC2 容量的实例,以低于按需价格提供。由于Spot实例允许用户以极低的折扣请求未使用的 EC2 实例,这可能会显著降低用户的 Amazon EC2 成本。Spot实例的每小时价格称为 Spot 价格。每个可用区中的每种实例类型的 Spot 价格是由 Amazon EC2 设置的,并根据Spot实例的长期供求趋势逐步调整。只要容量可用,并且请求的每小时最高价超过 Spot 价格,Spot实例就会运行。如果 Amazon EC2 需要收回容量,或者 Spot 价格超过用户的请求的最高价,Amazon EC2 中断用户的Spot实例。
正常处理Spot实例中断的最佳方法是,设计应用程序以提供容错能力。为此,用户可以利用 EC2 实例rebalance recommendation和Spot实例interruption notification。其中,EC2 实例 rebalance recommendation 是一个新的信号,可在Spot实例处于较高的中断风险时通知用户。该信号可能比该两分钟的Spot实例interruption notification更早到达,从而让用户有机会主动管理Spot实例。
2.使用Karpenter管理Spot实例
接下来我们承接上一篇博客,继续以 Karpenter 0.6.3为例,介绍如何在Karpenter中进行Spot实例的管理。部署流程如下:

在上一篇博客中,我们设置了一个默认的On-demand Provisioner,并扩展了一个GPU推理应用程序。接下来我们修改启动实例类型为Spot和On-demand。
2.1 配置 Provisioner CRD
修改Provisioner属性
在上述 provisioner 的参数配置中,我们在 capacity-type 中配置了 Spot 和 On-demand 两种类型。 在karpenter 选择资源时,会优先选择 Spot 实例类型,当 Spot 实例容量不足的时候,会尝试启动 On-demand 实例类型。
同时建议广泛的添加可选的实例类型以提高获得 Spot 实例的几率,因为对于 Spot 实例来说,karpenter 的分配策略是 capacity optimized prioritized, 调用EC2 Fleet API优先选择底层可用容量最多的实例类型。
在前面的例子中,g4dn.4xlarge的Spot价格还低于g4dn.xlarge,因此可以加进来,以最大限度拿到便宜的Spot机器。关于Provisioner的详细配置可以参考官网上的文档。下面我们更新Provisioner:
执行kubectl get provisioners查看provisioners部署情况:

执行kubectl describe provisioners查看配置


2.2 配置 AWS Node Termination Handler
在使用了Spot实例后,会面临实例回收的情况(或者现货价格超过用户请求的最高价格)。在这种情况下,可以利用Spot实例重平衡的特性来处理中断。在EC2发出重平衡信号后,会通知用户Spot实例存在较高的中断风险。此信号使用户有机会在现有或新的Spot实例之间,主动地重新平衡工作负载,而不必等待两分钟的Spot实例中断通知。
为了捕获这些信号并优雅的处理中断,我们将部署一个名为AWS Node Termination Handler。Node Termination Handler以两种不同的模式运行:队列模式和实例元数据模式。我们将使用实例元数据模式。在这种模式下,Node Termination Handler实例元数据服务监视器将运行一个非常小的Pod (DaemonSet) ,在每台主机上监控IMDS路径(如/spot或/events),并对相应节点的drain或cordon作出反应。
由于Karpenter本身不对Spot的rebalance recommendation和interruption notification信号进行额外处理,因此需要安装 Node Termination Handler 配合使用。通过下面命令可以利用 helm 来安装 AWS Node Termination Handler:
安装参数的意义如下:
enableRebalanceMonitoring
可以让 node interruption handler 同时监控 rebalance recommendation和 interruption notification信号
可以让 node interruption handler 收到 rebalance recommendation信号的时候 drain 实例而不仅仅 cordon 实例(其中 cordon 是默认的操作)
enableSpotInterruptionDraining:
可以对收到 interruption notification信号的时候 drain实例
enableScheduledEventDraining:
可以让 node interruption handler 在 EC2 instance scheduled event 的维护窗口开始之前 drain 实例
nodeSelector."karpenter\\.sh/capacity-type"=spot:
使用 node selector 让node interruption handler只部署在 spot 实例上,不需要部署在按需实例上,避免资源的浪费。
2.3 查看 AWS Node Termination Handler 的日志
我们可以配置 Fluentbit 将 AWS Node Termination Handler的日志发送到Cloudwatch Logs 中,以便后续测试过程中可以看到该Handler的具体处理细节,配置 Fluentbit 的详细步骤如下:
- 创建 amazon-cloudwatch 的命名空间
- 运行以下命令以创建一个名为 cluster-info 的 ConfigMap,该 ConfigMap 以集群名称和要向其发送日志的区域命名。将 cluster-name 和 cluster-region 分别替换为用户的集群的名称和区域。
- 运行以下命令,将 Fluent Bit daemonset 下载并部署到集群中。
- 等待相关资源创建完毕后,默认情况下用户可以从名字为
/aws/containerinsights/Cluster_Name/dataplane 的 CloudWatch log group 中看到 AWS Node Termination Handler 的日志
2.4 监控 Spot 实例中断情况
为了监控 spot 实例的的中断情况,我们部署一个第三方的插件 Spot termination exporter 以便在 Prometheus 和 Grafana dashboard 中查看到集群中 spot 实例的中断情况。
首先我们在 EKS 集群中部署 Prometheus 和 Grafana dashboard,详细步骤参考如下:
- 添加 Prometheus 和 Grafana 的 helm repo
- 通过 helm 在 EKS 集群中创建 Prometheus
- 配置 Grafana 数据源为 Prometheus
- 通过 helm 创建 Grafana,这里我们配置通过 LoadBalancer 接入 Grafana dashboard
- 等待几分钟,让 ELB 启动,DNS 传播,ELB target 完成注册并通过健康检查,接下来用户可以通过如下命令看到 Grafana dashboard 的 URL
- 将上述 URL 复制粘贴到浏览器中即可出现登陆页面,登录时,使用用户名 admin 并通过运行以下命令获取密码:
- 创建一个 dashboard,选择 Prometheus 作为数据源
接下来我们使用 helm 安装 Prometheus Spot termination exporter创建一个 dashboard,选择 Prometheus 作为数据源
等待 Spot termination exporter pods 创建完成,刷新 Grafana Prometheus dashboard, 查找 aws_instance_termination_imminent 指标即可监控 spot 实例的中断情况,更多关于 Spot termination exporter 的 Prometheus 指标,可以参考文档
2.5 部署示例应用与测试
参考之前的博客以部署示例的推理应用。增加 Pod 的数量为6个(kubectl scale deployment resnet –replicas 6),以便触发 Karpenter 进行扩容。通过如下命令查看 Karpenter Controller 日志:

在 karpenter 的日志中发现在执行了Pod扩容后,karpenter会优先使用Spot,并且选择价格合适的实例类型。
另外,再来看看当Spot 实例容量不足时的情况。如下日志所示在尝试遍历所有可用区的 g4dn.8xlarge Spot 实例(为了验证效果,修改请求类型为g4dn.8xlarge)依然没有容量之后,karpenter会切换为启动 On-demand 实例。这也侧面印证了前面所说的,建议广泛的添加可选的实例类型以提高获得 Spot 实例的几率。

当有 Spot 实例需要被回收的时候,AWS node terminate handler 会接收到 rbr 信号并且开始 cordon&drain 相应的 Spot 实例。可以通过 fluent Bit 将 AWS node terminate handler 的日志推送到 cloudwatch log 上观察相应的日志
{ "log": "2022/04/12 07:03:13 INF Adding new event to the event store event={\"AutoScalingGroupName\":\"\",\"Description\":\"Rebalance recommendation received. Instance will be cordoned at 2022-04-12T07:03:12Z \\n\",\"EndTime\":\"0001-01-01T00:00:00Z\",\"EventID\":\"rebalance-recommendation-d487975a7f68b05f264cf06941208a5a76b1c2717ed99ee0f912122ee44406b2\",\"InProgress\":false,\"InstanceID\":\"\",\"IsManaged\":false,\"Kind\":\"REBALANCE_RECOMMENDATION\",\"NodeLabels\":null,\"NodeName\":\"ip-192-168-154-203.ap-southeast-1.compute.internal\",\"NodeProcessed\":false,\"Pods\":null,\"StartTime\":\"2022-04-12T07:03:12Z\",\"State\":\"\"}\n", "stream": "stderr", "az": "ap-southeast-1a", "ec2_instance_id":"i-00727e5dd268f477c" }
{ "log": "2022/04/12 07:03:14 INF Requesting instance drain event-id=rebalance-recommendation-d487975a7f68b05f264cf06941208a5a76b1c2717ed99ee0f912122ee44406b2 instance-id= kind=REBALANCE_RECOMMENDATION node-name=ip-192-168-154-203.ap-southeast-1.compute.internal\n", "stream": "stderr", "az": "ap-southeast-1a", "ec2_instance_id": "i-00727e5dd268f477c" }
{ "log": "2022/04/12 07:03:14 INF Pods on node node_name=ip-192-168-154-203.ap-southeast-1.compute.internal pod_names=[\"fluent-bit-nzwrh\",\"resnet-deployment-cdf59b549-h5qsk\",\"spot-termination-exporter-1642989993-spot-termination-expo9brtf\",\"aws-node-99txh\",\"aws-node-termination-handler-tg8kz\",\"kube-proxy-6965k\",\"prometheus-node-exporter-8dfqk\"]\n", "stream":"stderr", "az": "ap-southeast-1a", "ec2_instance_id": "i-00727e5dd268f477c" }
{ "log": "2022/04/12 07:03:14 INF Draining the node\n", "stream": "stderr", "az": "ap-southeast-1a", "ec2_instance_id": "i-00727e5dd268f477c" }
{ "log": "2022/04/12 07:03:14 ??? WARNING: ignoring DaemonSet-managed Pods: amazon-cloudwatch/fluent-bit-nzwrh, default/spot-termination-exporter-1642989993-spot-termination-expo9brtf, kube-system/aws-node-99txh, kube-system/aws-node-termination-handler-tg8kz, kube-system/kube-proxy-6965k, prometheus/prometheus-node-exporter-8dfqk\n", "stream": "stderr", "az":"ap-southeast-1a", "ec2_instance_id": "i-00727e5dd268f477c" }
{ "log": "2022/04/12 07:03:14 ??? evicting pod default/resnet-deployment-cdf59b549-h5qsk\n", "stream": "stderr", "az": "ap-southeast-1a", "ec2_instance_id": "i-00727e5dd268f477c" }
{ "log": "2022/04/12 07:03:40 INF event store statistics drainable-events=0 size=1\n", "stream":"stderr", "az": "ap-southeast-1a", "ec2_instance_id": "i-00727e5dd268f477c" }
{ "log": "2022/04/12 07:03:52 INF Node successfully cordoned and drained node_name=ip-192-168-154-203.ap-southeast-1.compute.internal reason=\"Rebalance recommendation received. Instance will be cordoned at 2022-04-12T07:03:12Z \\n\"\n", "stream": "stderr", "az": "ap-southeast-1a", "ec2_instance_id": "i-00727e5dd268f477c" }
更详细的信息可以参考:https://github.com/aws/aws-node-termination-handler
除此之外,也可以通过 Grafana dashboard 查看Prometheus Spot termination exporter 发布的 Prometheus 的指标来监控 Spot 实例被回收的情况

2.6 异常处理
异常情况1:On-demand实例不足
这时候karpenter 会一直尝试请求 On-demand实例,从karpenter的日志中可以看到如下信息:
我们发现当 On-demand 实例没有容量的时候,会报如下错误
异常情况2:Spot 实例达到了账户的限制
从 karpenter的日志中可以看到诸如MaxSpotInstanceCountExceeded的报错信息,这时候需要请求提高Spot实例限制,详细参考文档[1]
3.小结
在这个博客里我们承接上一篇博客,继续以 Karpenter 0.6.3为例,介绍如何在Karpenter中进行Spot实例的管理。Spot实例允许用户以极低的折扣请求未使用的 EC2 实例,显著降低用户的 Amazon EC2 成本。Karpenter 默认使用 capacity optimized prioritized 策略, 调用EC2 Fleet API优先选择底层可用容量最多的实例类型。配合 AWS Node Termination Handler 处理 Spot 实例的中断,再部署 Prometheus 以及 Fluentbit 处理 Spot 实例中断的相关指标和日志。
本篇作者