亚马逊AWS官方博客
Karpenter 实践:一种多 EKS 集群下的 Spot 实例中断事件处理总线设计
项目背景
Karpenter 是 AWS 开源的 Kubernetes 工作节点动态调度控制器,其相较于 Cluster Autoscaler,具有调度速度快、调度更灵活、资源利用率高等众多优势。因而,越来越多的客户开始使用 Karpenter 来简化和优化 Amazon EKS 集群的调度任务(如扩缩容)。另外,对于希望进一步控制成本的客户来说,结合 Karpenter 与 Amazon EC2 Spot 实例进行使用也成为了一种可选的手段。但由于 Amazon EC2 Spot 实例在容量不足时会被回收,在带来经济性同时,也带来了应用稳定性的挑战。为避免 Amazon EC2 Spot 实例回收导致的业务中断,目前主要的处理手段为接收 Amazon EC2 Spot 实例回收前发出的相关事件(Spot Interruption 和 Spot Rebalance Recommendation 两类),驱逐即将回收节点上的 Pod,并同时进行节点替换等操作。在社区中,如何处理 Amazon EC2 Spot 实例回收机制带来的应用稳定性挑战,提出了两种设计方案:
- 方案 1:通过 DaemonSet 方式,在每个节点部署 AWS Node Termination Handler,然后通过其进行探知和处理相关事件
- 方案 2:Karpenter 原生集成事件处理逻辑,然后通过 Amazon EventBridge 和 Amazon SQS 进行事件的监听和传递
对于方案 1,已经有很多文章进行了相关阐述,如 Kubernetes 节点弹性伸缩开源组件 Karpenter 实践:使用 Spot 实例进行成本优化,本文不再进行展开。目前在 Karpenter v0.27.3 版本中,方案 2 由于其原生集成,无需管理额外组件等优势,被设定为了默认方案。依照源码可知,其内部原理大致如下图所示:
可以看到 Karpenter 基于 Kubernetes 的 Reconcile 机制,不断的同步来自 AWS Event 的事件,然后内部进行如下处理:
- Handle Message:对于接收到的 Message,根据其所属的 Node 节点进行分发;
- Handle Node:对指定的节点依据事件的类别进行相关处理;
- Action for Message:基于不同的消息种类进行分类处理:
- 如果事件类型满足上图所示,则进行节点 Graceful Shutdown 处理,驱逐节点上的所有 Pod,并创建新节点。同时通过 Notify for Message 发送相关日志。
- 如果是其他事件,仅通过 Notify for Message 发送对应日志信息。
在了解完内部机制后,在接下来的篇章中会对方案 2 整体架构的进一步介绍,进而会介绍方案 2 在处理多集群 Spot 实例中断事件时存在的扩展性等问题,以及我们基于此提出的基于 AWS Lambda 作为事件处理总线的优化设计。最后,会基于 AWS Fault Injection Simulator(FIS)服务进行相关中断事件的模拟,从而阐述新方案的优势和工作流程。另外通过上述描述可知,中断事件除了 Spot 实例相关的以外,还包括了一些 Schedule 的 Health event 和 EC2 节点状态的变化相关事件,后续的优化设计也包括了对其的处理,但在本文不进行重点阐述。
方案介绍
根据社区提供的 Cloudformation 模版和 Getting started 介绍,笔者绘制了 Karpenter 和其相关 Spot 实例中断事件处理的整体架构示意图:
其基本的逻辑为:
- 在每个 EKS 集群中创建 Karpenter 时,都会创建如下的 Amazon EventBridge 规则,监听对应事件:
- EC2 Spot Instance Interruption Warning
- EC2 Instance Rebalance Recommendation
- EC2 Instance State-change Notification
- AWS Health Event
- 在每个 EKS 集群创建 Karpenter 时,会创建单独的 SQS 队列,用以接受上述事件。
- 通过配置 Karpenter 的 Configmap 来指定对应集群的 SQS 队列,Karpenter 会从队列中消费上述事件并处理。
当 EKS 集群和 Karpenter 相关配置在单账户单区域中数量较少时,上述的设计并无不妥。但在某些客户的实际使用中,单账户单区域下,会有大量的 EKS 集群和对应的 Karpenter,这时候我们会发现一些明显的问题:
- 重复造轮子:需要为每一个集群都创建一套 Amazon EventBridge 规则,当集群数量增多时会有额外的维护成本,且每个事件总线的规则数量有限制。
- 无效投递:通过源码分析可知,集群中有很多事件(如 State Change 事件中的 Running 事件)被 Karpenter 接收到后并不做任何处理,只是记录对应的日志。
- 事件放大:每套集群对应的 Amazon EventBridge 规则的内容完全一样。因为规则完全相同,任一集群发生的相关事件同时也会发送到其他集群对应的 SQS 队列中, 从而造成事件放大效应。
下面通过 Amazon EventBridge Rule 样例来对,来对上述问题产生的原因进行进一步说明。
- EC2 Spot Instance Interruption Warning Sample Event
通过上述 Amazon EventBridge Rule 可以看到,其并不会传递相关集群 tag,进而无法基于集群 tag 精准监听,从而导致了上述事件放大。在下一章节中,笔者会阐述利用 AWS Lambda 实现事件分发总线的改进方案,其一方面依据源码的处理逻辑进一步精简相关事件监听,另一方面以一种可扩展,易维护的设计优雅的应对多集群多 Karpenter 下的中断事件处理流程。
利用 AWS Lambda 实现事件分发总线的改进方案介绍及测试
借鉴事件总线的处理逻辑,新方案采用基于 Lambda 作为事件总线中心,基于不同 EKS 集群的特定标签,进行事件的精准分发。项目的架构如下所示:
其整体的流程为:
- 首先创建 Karpenter 事件处理总线,其中包含了全套的监听事件和基于 Lambda 的事件分发处理器。
- 在后续集群安装 Karpenter 的时候,除了基础的 Karpenter 配置以外,只需要创建对应的 SQS 事件队列即可。
- 当对应集群有 Spot 实例中断告警事件产生时,Lambda 函数通过事件中的 instance-id 得到对应的 tag,从而判断发生事件的集群,进而把对应的事件指向性投递到队列中。
在后续的章节中,我们首先会进行整个环境的搭建,然后会利用 AWS FIS 进行模拟其中的 Spot 实例中断事件的发生,进而来观察改进方案的整个流程。
部署和配置环境
在部署环境时,会按以下顺序进行相关组件的安装配置:
- 部署事件处理总线
- 部署两套 EKS 集群及 Karpenter 相关组件,分别为
- test-eks
- test-eks1
前提条件
安装如下的组件
- AWS CLI
- kubectl- the Kubernetes CLI
- eksctl- the CLI for AWS EKS
- helm- the package manager for Kubernetes
- 配置 AWS CLI – configure aws CLI
- 安装 jq – install jq on AMl2
除了以上的前提条件外,本文其他的默认条件为
- 一个 AWS 账户
- 拥有管理员或同等权限的 IAM 用户或角色
- Karpenter 版本为 27.3
- EKS 版本为 24
- 部署 Region 默认为为 us-west-2
安装事件处理总线
运行以下命令以部署和安装事件处理总线:
该模版相较于 Karpenter Getting Started 提供的 Cloudformation,在事件监听中依据 Karpenter 源码,进一步对无效事件过滤。具体变动为:
- InstanceStateChangeRule:排除“pending”,“running”事件的接收,因为 Karpenter 并不对其处理
- RebalanceRule:因为目前 Karpenter 并不对该事件进行具体处理,故移除该 Rule,排除该事件的监听
部署得到如下输出,证明部署成功:
安装测试 EKS 集群及 Karpenter 相关组件
1. 安装 Karpenter 的相关 IAM 角色,Amazon SQS 事件接收队列,以及 Amazon EKS 集群
其中 test-eks 为创建的 Amazon EKS 的集群名称。此处在创建 Karpenter 相关 IAM 角色和策略,Amazon SQS 队列名称的时候与其做了关联。得到如下输出表示部署成功:
2. 安装 Karpenter 和配套的 Provisioner
其中 test-eks 为测试集群名称. 看到如下输出,表示 Karpenter 部署完成:
查看部署状态:
3. 部署示例应用,触发 Karpenter 生成 Spot 节点
在实验中,EKS 集群初始创建的托管节点组实例类型均为 2 vCPU。在 EKS Cluster 中部署一个 5 副本,每副本需要 3 vCPU 的应用,即可触发对应 Spot 节点的生成。具体步骤如下:
查看应用状态:
查看节点状态:
观察发现 Karpenter 动态生成了 5 个额外的 Spot 实例工作节点,得到以上输出后,说明部署完毕。
重复上述 1~3 步骤,并将集群名称参数从 test-eks 改为 test-eks-1,从而部署第二套测试集群和应用。
至此基于 AWS Lambda 和 Amazon Eventbridge 构建的事件分发总线,对应的两套 EKS 测试集群,和其配套的 Karpenter 和事件接收队列已经部署完成。
使用 AWS FIS 进行模拟测试
AWS FIS 是一项托管服务,可让您在 AWS 工作负载上执行故障注入实验。故障注入基于混沌工程原理,这些实验通过创建干扰事件(如数据库切换,Spot 实例中断等)来对应用程序进行压力测试,以便观察应用程序的响应情况,从而可以利用这些信息来改进应用程序的性能和弹性,以使其表现符合预期。下面我们基于 AWS FIS 服务来模拟 Spot 实例中断告警事件,并观察整个事件处理总线的处理流程。
生成 AWS FIS 实验模版
该模版中的一些配置解释:
- Action:发送 aws:ec2:send-spot-instance-interruptions 事件
- Target:发送到具有以下 Tag/Value 对的目标 Spot 实例,该标签对为 Karpenter 自动生成,应用在对应集群的工作节点上
- Tag:aws:eks:cluster-name
- Value:${ClusterName}
配置针对第二套集群的模版:
执行测试和观察结果
实验:使用 AWS FIS 服务同时对上述的两个 EKS 集群的 Spot 实例工作节点发出 Spot 实例中断告警事件,并观察整个事件处理总线的处理流程。
1. 开始模拟测试
2. 查看 AWS FIS 日志
我们以 test-eks 集群为例,查看相关的触发
截取部分关键日志可知,整个测试总共 6 步,experiment-start,target-resolution-start,target-resolution-end,action-start,action-end,experiment-end。其中:
- target-resolution-start/target-resolution-end:基于 tag 筛选出了需要触发 Spot 实例中断告警的 Spot 实例
- action-end:在 2023-05-01T03:04:28.245Z(UTC 时间)执行完了所有 Spot 实例中断告警的触发
3. 查看 Karpenter Controller 日志
同样以 test-eks 集群为例查看相关日志
上述 Controller 日志经过拆解,主要分为三部分:
- Interruption detection:探测到 Spot 实例中断告警事件
- Graceful deletion:判断需要处理的节点,然后对节点进行优雅下线
- Launch new Spot instance:Karpenter 探知有 Pod 未得到调度,生成新的 Spot 例节点
结合 FIS 日志可以看出,从 FIS 处模拟事件发生(03:04:30)到 Karpenter 事件处理完毕(03:04:36),总计约 5 秒 即可完成 Spot 实例中断告警事件的处理(基于 5 次测试结果结果)。相对比,同样的测试逻辑,在原方案 2 中消耗的时间约为 4秒(基于 5 次的测试结果),主要是因为少了 Lambda 层的开销,但是其整体耗时的差异基本可以忽略不计。
4. 观察事件总线触发和小结
登录到 AWS 控制台,点击进入 Amazon EventBridge Rule 的监控界面中,可以查看到各个规则的具体触发次数:
- EC2 Spot Instance Interruption Warning Rule:10 次
- Spot 实例替换触发的 EC2 Instance State Rule:20 次
登录到 AWS 控制台,在 Amazon SQS 的监控界面中,可以看到两个集群的接收和发送的消息都为 15,即最终每个集群的 Karpenter Controller 需要消费 15 个事件。与之对比,在原始设计中,相同的事件触发,两个集群的 Amazon EventBridge Rule 均会产生放大和无效触发,单集群的数据为:
- EC2 Spot Instance Interruption Warning Rule:10 次
- EC2 Instance State Change Rule:40 次
- EC2 Spot Rebalance Recommdation:10 次
即每个 Karpenter Controller 需要消费 60 个事件,产生的压力为优化设计的 4 倍(60/15)。可以预见的是,随着集群的增多,会进一步放大该对比。
清理
为避免产生额外费用,运行下列命令以清理之前创建的资源:
总结
本文以 Spot 实例中断告警为例,阐述了一种基于 Karpenter 进行多集群事件处理的优化设计,其从维护性和扩展性等多个角度都进行了改善。 但该方案还有改进空间:
1. 是否一定要切换到该方案
对于是否一定要切换到该方案,笔者认为可依据不同场景进行选择。尤其是通过前文可知,目前该方案对 Spot 实例再平衡事件并未进行处理。由于该功能有可能前 10~20 分钟提前发预警,对于需要更长处理事件的应用来说(如 2 分钟以上),方案 1 可能更适合该场景。但是需要注意 Spot 实例再平衡事件的触发存在一定的不确定性,能否依赖其进行相关逻辑的处理,请参考。
2. 其他可选方案
对部分拥有大批量集群的客户来说(如 SaaS 类型),从进一步优化吞吐和成本的角度来说,可以在本文的 Router 函数前设置一个集中化的 Amazon SQS 队列。即所有事件通过 EventBridge 统一发送到该队列,利用 Amazon Lambda 的 Batch 机制批量处理对应请求,然后再批量发送到指定的下游队列中。此外也可以利用 SNS 替代 Lambda,采用 Fan-Out 的方式进行事件的广播。此方式减少了 Lambda 侧的维护成本,但是仍旧存在事件放大,并且其将 Lambda 部分的资源消耗,转嫁给了每个 Karpenter Controller。
最后,由于 Karpenter 的版本更新很快,很多功能可能在新版本中会进行修改,建议读者关注本文的实验环境后续版本的差异。
参考文档
Karpenter:https://karpenter.sh/v0.27.3/getting-started/getting-started-with-karpenter/
Karpenter 中断处理配置:https://karpenter.sh/v0.27.3/concepts/deprovisioning/
Karpenter 源代码逻辑:https://github.com/aws/karpenter/
Karpenter 中断处理源码:https://github.com/aws/karpenter/blob/9f3e40727dbedc6da7ffeb3d7d94278096d2ff27/pkg/controllers/interruption/controller.go