亚马逊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,已经有很多文章进行了相关阐述,如 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
 {
  "version": "0",
  "id": "1e5527d7-bb36-4607-3370-4164db56a40e",
  "detail-type": "EC2 Spot Instance Interruption Warning",
  "source": "aws.ec2",
  "account": "123456789012",
  "time": "1970-01-01T00:00:00Z",
  "region": "us-east-1",
  "resources": ["arn:aws:ec2:us-east-1b:instance/i-0b662ef9931388ba0"],
  "detail": {
    "instance-id": "i-0b662ef9931388ba0",
    "instance-action": "terminate"
  }
}

通过上述 Amazon EventBridge Rule 可以看到,其并不会传递相关集群 tag,进而无法基于集群 tag 精准监听,从而导致了上述事件放大。在下一章节中,笔者会阐述利用 AWS Lambda 实现事件分发总线的改进方案,其一方面依据源码的处理逻辑进一步精简相关事件监听,另一方面以一种可扩展,易维护的设计优雅的应对多集群多 Karpenter 下的中断事件处理流程。

利用 AWS Lambda 实现事件分发总线的改进方案介绍及测试

借鉴事件总线的处理逻辑,新方案采用基于 Lambda 作为事件总线中心,基于不同 EKS 集群的特定标签,进行事件的精准分发。项目的架构如下所示:

其整体的流程为:

  1. 首先创建 Karpenter 事件处理总线,其中包含了全套的监听事件和基于 Lambda 的事件分发处理器。
  2. 在后续集群安装 Karpenter 的时候,除了基础的 Karpenter 配置以外,只需要创建对应的 SQS 事件队列即可。
  3. 当对应集群有 Spot 实例中断告警事件产生时,Lambda 函数通过事件中的 instance-id 得到对应的 tag,从而判断发生事件的集群,进而把对应的事件指向性投递到队列中。

在后续的章节中,我们首先会进行整个环境的搭建,然后会利用 AWS FIS 进行模拟其中的 Spot 实例中断事件的发生,进而来观察改进方案的整个流程。

部署和配置环境

在部署环境时,会按以下顺序进行相关组件的安装配置:

  1. 部署事件处理总线
  2. 部署两套 EKS 集群及 Karpenter 相关组件,分别为
    • test-eks
    • test-eks1

前提条件

安装如下的组件

  1. AWS CLI
  2. kubectl- the Kubernetes CLI
  3. eksctl- the CLI for AWS EKS
  4. helm- the package manager for Kubernetes
  5. 配置 AWS CLI – configure aws CLI
  6. 安装 jq – install jq on AMl2

除了以上的前提条件外,本文其他的默认条件为

  • 一个 AWS 账户
  • 拥有管理员或同等权限的 IAM 用户或角色
  • Karpenter 版本为 27.3
  • EKS 版本为 24
  • 部署 Region 默认为为 us-west-2

安装事件处理总线

运行以下命令以部署和安装事件处理总线:

$ git clone https://github.com/jansony1/multi-eks-cluster-interruption-mangement-karpenter.git
$ sh scripts/install_interruption_handler_event_bus.sh

该模版相较于 Karpenter Getting Started 提供的 Cloudformation,在事件监听中依据 Karpenter 源码,进一步对无效事件过滤。具体变动为:

  • InstanceStateChangeRule:排除“pending”,“running”事件的接收,因为 Karpenter 并不对其处理
  • RebalanceRule:因为目前 Karpenter 并不对该事件进行具体处理,故移除该 Rule,排除该事件的监听

部署得到如下输出,证明部署成功:

Waiting for changeset to be created..
Waiting for stack create/update to complete
Successfully created/updated stack - Karpenter-interruption-handler-event-bus

安装测试 EKS 集群及 Karpenter 相关组件

1. 安装 Karpenter 的相关 IAM 角色,Amazon SQS 事件接收队列,以及 Amazon EKS 集群

$ sh scripts/install_eks_and_karpenter_queue_related_resouces.sh test-eks

其中 test-eks 为创建的 Amazon EKS 的集群名称。此处在创建 Karpenter 相关 IAM 角色和策略,Amazon SQS 队列名称的时候与其做了关联。得到如下输出表示部署成功:

Waiting for changeset to be created..
Waiting for stack create/update to complete
Successfully created/updated stack - Karpenter-test-eks-role-and-interruption-queue
2023-04-30 10:38:38 [✔]  EKS cluster "test-eks" in "us-west-2" region is ready

2. 安装 Karpenter 和配套的 Provisioner

$ sh scripts/install_karpenter_and_provisioner.sh test-eks

其中 test-eks 为测试集群名称. 看到如下输出,表示 Karpenter 部署完成:

…
NAME: karpenter
LAST DEPLOYED: Sun Apr 30 10:01:58 2023
NAMESPACE: karpenter
STATUS: deployed
…

查看部署状态:

$ kubectl get pods -nkarpenter
NAME                            READY   STATUS    RESTARTS   AGE
pod/karpenter-c58964679-d9z66   1/1     Running   0          44s
pod/karpenter-c58964679-mpj5s   1/1     Running   0          44s

3. 部署示例应用,触发 Karpenter 生成 Spot 节点

在实验中,EKS 集群初始创建的托管节点组实例类型均为 2 vCPU。在 EKS Cluster 中部署一个 5 副本,每副本需要 3 vCPU 的应用,即可触发对应 Spot 节点的生成。具体步骤如下:

$ kubectl apply -f yamls/test_deployment.yaml
deployment.apps/inflate created

查看应用状态:

$ kubectl get pods
NAME                      READY   STATUS    RESTARTS   AGE
inflate-d4d675747-7j6wd   5/5     Running   0          67s

查看节点状态:

$  kubectl get nodes
NAME                                            STATUS   ROLES    AGE    VERSION
ip-192-168-15-0.us-west-2.compute.internal      Ready    <none>   2m7s   v1.24.11-eks-a59e1f0
ip-192-168-154-62.us-west-2.compute.internal    Ready    <none>   2m4s   v1.24.11-eks-a59e1f0
ip-192-168-33-25.us-west-2.compute.internal     Ready    <none>   16h    v1.24.11-eks-a59e1f08
ip-192-168-7-123.us-west-2.compute.internal     Ready    <none>   16h    v1.24.11-eks-a59e1f0
…
ip-192-168-98-249.us-west-2.compute.internal    Ready    <none>   2m3s   v1.24.11-eks-a59e1f0

观察发现 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 实验模版

$ sh scripts/install_fis_experiments.sh test-eks
Waiting for changeset to be created..
Waiting for stack create/update to complete
Successfully created/updated stack - Karpenter-test-eks-FIS-experiments

该模版中的一些配置解释:

  • Action:发送 aws:ec2:send-spot-instance-interruptions 事件
  • Target:发送到具有以下 Tag/Value 对的目标 Spot 实例,该标签对为 Karpenter 自动生成,应用在对应集群的工作节点上
    • Tag:aws:eks:cluster-name
    • Value:${ClusterName}

配置针对第二套集群的模版:

$ sh scripts/install_fis_experiments.sh test-eks-1
Waiting for changeset to be created..
Waiting for stack create/update to complete
Successfully created/updated stack - Karpenter-test-eks-1-FIS-experiments

执行测试和观察结果

实验:使用 AWS FIS 服务同时对上述的两个 EKS 集群的 Spot 实例工作节点发出 Spot 实例中断告警事件,并观察整个事件处理总线的处理流程。

1. 开始模拟测试

$ export cluster1_tag='SpotInterruptionTest-test-eks'
$ export cluster2_tag='SpotInterruptionTest-test-eks-1'
$ export log_group_name='fis-log-group'
$ experiment__template_id_1=$(aws fis list-experiment-templates| jq -r  '.experimentTemplates[] | select(.tags.Name=="'"$cluster1_tag"'") | .id' | awk 'NR==1 {print $1}') 
$ aws fis start-experiment --experiment-template-id $experiment__template_id_1 --tags Name=$cluster1_tag & 
$ experiment__template_id_2=$(aws fis list-experiment-templates| jq -r  '.experimentTemplates[] | select(.tags.Name=="'"$cluster2_tag"'") | .id' | awk 'NR==1 {print $1}') 
$ aws fis start-experiment --experiment-template-id $experiment__template_id_2  --tags Name=$cluster2_tag &

2. 查看 AWS FIS 日志

我们以 test-eks 集群为例,查看相关的触发

$ experiment_id=$(aws fis list-experiments | jq -r  '.experiments[] | select(.tags.Name=="'"$cluster1_tag"'") | .id' | awk 'NR==1 {print $1}') 
$ aws logs filter-log-events --log-group-name $log-group-name --cli-input-json '{ "filterPattern": "{ $.id = \"'"$experiment_id"'\" }" }'
{
    "events": [
        {"message": "{\"id\":\"EXPgy2jXiWLCWWfkpr\",\"log_type\":\"experiment-start\",\"event_timestamp\":\"2023-05-01T03:04:16.578Z\",\"version\":\"1\",\"details\":{\"experiment_template_id\":\"EXTCj2ZJxTJDXmfAY\",\"experiment_start_time\":\"2023-05-01T03:04:14.617Z\"}}",},
        {"message": "{\"id\":\"EXPgy2jXiWLCWWfkpr\",\"log_type\":\"target-resolution-start\",\"event_timestamp\":\"2023-05-01T03:04:16.851Z\",\"version\":\"1\",\"details\":{\"target_resolution_start_time\":\"2023-05-01T03:04:16.850Z\",\"target_name\":\"spotInTargetCluster\"}}"},
        {"message": "{\"id\":\"EXPgy2jXiWLCWWfkpr\",\"log_type\":\"target-resolution-end\",\"event_timestamp\":\"2023-05-01T03:04:17.203Z\",\"version\":\"1\",\"details\":{\"target_resolution_end_time\":\"2023-05-01T03:04:17.20
2Z\",\"target_name\":\"spotInTargetCluster\",\"resolved_targets\":[\"arn:aws:ec2:us-west-2:269562551342:instance/i-0d65a1ac36a28d439\",\"arn:aws:ec2:us-west-2:269562551342:instance/i-0de85ed6454e05ed6\",\"arn:aws:ec2:us-west-2:269562551342:instance/i-04567a27bb15e3ed3\",\"arn:aws:ec2:us-west-2:269562551342:instance/i-00c0ea17b8a885f94\",\"arn:aws:ec2:us-west-2:269562551342:instance/i-02d4ae7607716aa23\"],\"page\":1,\"total_pages\":1}}",},
        {"message": "{\"id\":\"EXPgy2jXiWLCWWfkpr\",\"log_type\":\"action-start\",\"event_timestamp\":\"2023-05-01T03:04:28.020Z\",\"version\":\"1\",\"details\":{\"action_name\":\"spotInstanceinterruption\",\"action_id\":\"aws
:ec2:send-spot-instance-interruptions\",\"action_start_time\":\"2023-05 01T03:04:28.008Z\",\"action_targets\":{\"SpotInstances\":\"spotInTargetCluster\"},\"parameters\":{\"durationBeforeInterruption\":\"PT2M\"}}}",},
        {"message": "{\"id\":\"EXPgy2jXiWLCWWfkpr\",\"log_type\":\"action-end\",\"event_timestamp\":\"2023-05-01T03:04:28.255Z\",\"version\":\"1\",\"details\":{\"action_name\":\"spotInstanceinterruption\",\"action_id\":\"aws:e
c2:send-spot-instance-interruptions\",\"action_end_time\":\"2023-05-01T03:04:28.245Z\",\"action_state\":{\"status\":\"completed\",\"reason\":\"Action was completed.\"}}}",},
        {,"message": "{\"id\":\"EXPgy2jXiWLCWWfkpr\",\"log_type\":\"experiment-end\",\"event_timestamp\":\"2023-05-01T03:04:28.773Z\",\"version\":\"1\",\"details\":{\"experiment_end_time\":\"2023-05-01T03:04:28.764Z\",\"experim
ent_state\":{\"status\":\"completed\",\"reason\":\"Experiment completed.\"}}}",}
    ]}

截取部分关键日志可知,整个测试总共 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 集群为例查看相关日志

$ kubectl logs {Karpenter_pod_name} -nkarpenter
#  Interruption detected
2023-05-01T03:04:30.798Z    INFO    controller.interruption deleted node from interruption message  {"commit": "d7e22b1-dirty", "queue": "test-eks", "messageKind": "SpotInterruptionKind", "node": "ip-192-168-136-169.us-west-2.compute.internal", "action": "CordonAndDrain"}
2023-05-01T03:04:30.844Z    INFO    controller.termination  cordoned node   {"commit": "d7e22b1-dirty", "node": "ip-192-168-136-169.us-west-2.compute.internal"}
...
23-05-01T03:04:31.022Z  INFO    controller.termination  cordoned node   {"commit": "d7e22b1-dirty", "node": "ip-192-168-15-0.us-west-2.compute.internal"}
2023-05-01T03:04:31.275Z    INFO    controller.interruption deleted node from interruption message  {"commit": "d7e22b1-dirty", "queue": "test-eks", "messageKind": "SpotInterruptionKind", "node": "ip-192-168-15-0.us-west-2.compute.internal", "action": "CordonAndDrain"}
# graceful shutdown
2023-05-01T03:04:31.708Z    INFO    controller.termination  deleted node    {"commit": "d7e22b1-dirty", "node": "ip-192-168-136-169.us-west-2.compute.internal"}
...
2023-05-01T03:04:31.764Z    INFO    controller.termination  deleted node    {"commit": "d7e22b1-dirty", "node": "ip-192-168-15-0.us-west-2.compute.internal"}
# launch new instance 
023-05-01T03:04:36.017Z INFO    controller.provisioner  launching machine with 1 pods requesting {"cpu":"3125m","pods":"3"} from types m5.xlarge    {"commit": "d7e22b1-dirty", "provisioner": "default"}
2023-05-01T03:04:36.027Z    INFO    controller.provisioner  launching machine with 1 pods requesting {"cpu":"3125m","pods":"3"} from types m5.xlarge    {"commit": "d7e22b1-dirty", "provisioner": "default"}
...
2023-05-01T03:04:36.056Z    INFO    controller.provisioner  launching machine with 1 pods requesting {"cpu":"3125m","pods":"3"} from types m5.xlarge    {"commit": "d7e22b1-dirty", "provisioner": "default"}

上述 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)。可以预见的是,随着集群的增多,会进一步放大该对比。

清理

为避免产生额外费用,运行下列命令以清理之前创建的资源:

$ sh scripts /clean_all.sh

总结

本文以 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

本篇作者

尹振宇

AWS 解决方案架构师,负责基于 AWS 云平台的解决方案咨询和设计,尤其在无服务器领域和微服务领域有着丰富的实践经验。

于昺蛟

亚马逊云科技解决方案架构师,负责互联网客户云计算方案的架构咨询和设计。在容器平台的建设和运维,应用现代化,DevOps 等领域有多年经验,致力于容器技术和现代化应用的推广。

孙大木

亚马逊云科技资深解决方案架构师,负责基于 AWS 云计算方案架构的咨询和设计,在国内推广 AWS 云平台技术和各种解决方案。