手把手带你使用Karpenter减少K8s集群资源浪费

Kubernetes 集群的主要成本因素之一是数据平面上的计算层。将 Kubernetes 集群运行在 Amazon EC2 Spot 实例上是一种显著降低计算成本的有效方式。使用 Spot 实例可以比按需实例节省高达 90% 的费用。

Spot 实例非常适合无状态、容错性强且灵活的应用场景,例如大数据处理、容器化工作负载、CI/CD、Web 服务器、高性能计算(HPC)以及测试和开发工作负载。容器通常具有这些特性,因此与 Spot 实例高度契合。对于不适合 Spot 实例的工作负载,例如集群中的有状态应用,您仍然可以继续使用按需实例。

为了进一步优化数据平面的容量,当由于可用容量不足导致 Pod 无法调度时,您可以增加节点数量;当节点不再需要时,可以将其移除。要实现节点的自动调整,可以使用 Cluster Autoscaler (CA) 或 Karpenter。这两个工具都支持 Spot 实例,在本教程中,我将重点介绍 Karpenter。

我将引导您完成以下步骤:如何配置一个使用 Spot 实例和 Karpenter 的 EKS 集群。 此外,我还会展示如何配置工作负载,以通过使用 Spot 实例来动态提供所需的容量,从而直观感受到 Karpenter 的实际效果。

为什么选择 Karpenter

Karpenter 是一个为 Kubernetes 设计的节点调度开源项目。当新的 Pod 不断加入集群(无论是因为您手动增加了副本数量,还是通过 Horizontal Pod Autoscaling (HPA) 策略或 Kubernetes Event-driven Autoscaling (KEDA) 事件自动扩容),您的数据平面节点最终会达到容量上限,导致出现挂载(无法调度)的 Pod。

Karpenter 控制器会响应这一问题,通过评估调度约束(例如资源请求、节点选择器、亲和性、容忍度以及拓扑分布约束)整合这些挂载 Pod 的容量,然后调度符合这些 Pod 要求的节点。

使用 Karpenter 的主要优势之一在于,它能合理配置 Spot 实例,例如实例类型的多样化(包括多种实例家族、规格和代际等),这一功能通过 Karpenter 的 NodePool 实现。

如果您刚开始在 EKS 中使用 Spot 实例,或者在配置多个节点组的复杂性上遇到了困难,我建议您使用 Karpenter。

GitHub 地址:

github.com/kubernetes-...

00 前期准备

在开始之前,您需要具备以下条件:

  • 拥有具有 IAM 权限的 AWS 账户,以创建 EKS 集群。

  • 如果您计划直接运行本教程中的命令,还需要一个 AWS Cloud9 环境。

  • 安装并配置 AWS CLI。

  • 安装 Kubernetes CLI (kubectl)。

  • 安装 Terraform CLI。

  • 安装 Helm(Kubernetes 的包管理工具)。

01/创建 Cloud9 环境

Tips:

💡如果您已经有一个 Cloud9 环境,或者计划在自己的电脑上完成所有步骤,可以跳过此步骤。请确保您具备本教程前置条件中列出的必要权限。

💡 您可以通过设置 AWS_REGION 环境变量来控制 Cloud9 环境的启动区域。

我已经准备了一份 AWS CloudFormation 模板,用于创建一个预配置的 Cloud9 环境。这个环境包含所有本教程所需的工具,例如 kubectl 和 Terraform CLI。

您可以通过 AWS 控制台创建 CloudFormation 堆栈,也可以通过命令行完成。您可以在这里下载 CloudFormation 模板。接下来,我将提供所有需要的命令,帮助您通过 CLI 创建工具栈。

重要提示

您需要在 AWS 控制台和 AWS CLI 配置中使用相同的 IAM 用户或角色,否则在尝试打开 Cloud9 环境时可能会因权限不足而失败。

在创建 CloudFormation 堆栈之前,您需要获取一个 公有子网 ID,以便为 Cloud9 实例提供互联网访问权限。获取子网 ID 后,请设置以下环境变量:

ini 复制代码
export C9PUBLICSUBNET='<<YOUR PUBLIC SUBNET ID GOES HERE>>'

现在,运行下方命令以创建 Cloud9 环境:

arduino 复制代码
wget https://raw.githubusercontent.com/build-on-aws/run-kubernetes-clusters-for-less-with-amazon-ec2-spot-and-karpenter/main/cloud9-cnf.yaml
aws cloudformation deploy --stack-name EKSKarpenterCloud9 --parameter-overrides C9PublicSubnet=$C9PUBLICSUBNET --template-file cloud9-cnf.yaml --capabilities "CAPABILITY_IAM"

在 CloudFormation 完成后,请等待 3-5 分钟,然后打开 Cloud9 控制台并进入新创建的环境。从现在开始,您将通过 Cloud9 终端运行本教程中的所有命令。

注意: Cloud9 通常会动态管理 IAM 凭证,但这与 EKS 的 IAM 身份验证目前不兼容。因此,您需要禁用动态凭证管理,并改为依赖 IAM 角色。为此,请在 Cloud9 终端中运行以下命令:

bash 复制代码
# 禁用 Cloud9 的 IAM 凭证管理
aws cloud9 update-environment --environment-id ${C9_PID} --managed-credentials-action DISABLE

# 移除已有的动态凭证配置
rm -vf ${HOME}/.aws/credentials

为了确保您已安装本教程所需的所有 CLI 工具,请在 Cloud9 终端中输入以下命令:

ini 复制代码
aws --version
kubectl version --client=true -o json
terraform version
helm version

注意:

如果 CloudFormation 堆栈尚未达到 "CREATE_COMPLETE" 状态,CLI 工具可能尚未安装完成。在运行任何 CLI 命令之前,请耐心等待堆栈完成创建。

02/使用 EKS Blueprints for Terraform 创建 EKS 集群并部署 Karpenter

💡 提示:

本教程中使用的 Terraform 模板创建了一个 On-Demand 管理节点组来托管 Karpenter 控制器。如果您已有一个现有集群,也可以使用现有的节点组(基于 On-Demand 实例)来部署 Karpenter 控制器。为此,您需要遵循 Karpenter 的入门指南。

在本步骤中,您将使用 EKS Blueprints for Terraform 项目来创建一个 Amazon EKS 集群。

您将使用的 Terraform 模板会创建一个 VPC,一个 EKS 控制平面,一个 Kubernetes 服务账户,以及与之关联的 IAM 角色,通过 IAM Roles for Service Accounts (IRSA) 让 Karpenter 启动实例。

此外,模板还会将 Karpenter 节点角色配置到 aws-auth 配置映射中,以允许节点连接,并为 kube-systemkarpenter 命名空间创建一个 On-Demand 管理节点组。

要创建集群,请运行以下命令:

ini 复制代码
wget https://raw.githubusercontent.com/build-on-aws/run-kubernetes-clusters-for-less-with-amazon-ec2-spot-and-karpenter/main/cluster/terraform/main.tf
helm registry logout public.ecr.aws
export TF_VAR_region=$AWS_REGION
terraform init
terraform apply -target="module.vpc" -auto-approve
terraform apply -target="module.eks" -auto-approve
terraform apply --auto-approve

创建集群完成后(大约等待 15 分钟),运行以下命令来更新 kube.config 文件,以便通过 kubectl 与集群进行交互:

css 复制代码
aws eks --region $AWS_REGION update-kubeconfig --name spot-and-karpenter

💡 提示:

如果您使用的是不同的区域,或更改了集群的名称,可以通过运行以下命令,从 Terraform 输出中获取与您设置匹配的命令:

lua 复制代码
terraform output -raw configure_kubectl

确保您可以与集群进行交互,并且 Karpenter 的 Pod 已经启动。为此,运行以下命令来检查 Karpenter 的 Pod 状态:

sql 复制代码
$ kubectl get pods -n karpenter
NAME                       READY STATUS  RESTARTS AGE
karpenter-5f97c944df-bm85s 1/1   Running 0        15m
karpenter-5f97c944df-xr9jf 1/1   Running 0        15m

03/设置 Karpenter NodePool

EKS 集群已经为 kube-system 和 karpenter 命名空间配置了一个静态的管理节点组,并且这是您所需的唯一节点组。对于其余的 Pod,Karpenter 将通过 NodePool CRD 启动节点。

NodePool 设置了 Karpenter 可以创建的节点约束,以及可以在这些节点上运行的 Pod。单个 Karpenter NodePool 可以处理多种不同的 Pod 类型,在本教程中,您将只创建默认的 NodePool。

💡 提示:

Karpenter 通过一种称为无组自动扩展(group-less auto scaling)的方法简化了数据平面容量管理。这是因为 Karpenter 不再使用与自动扩展组(Auto Scaling Groups)匹配的节点组来启动节点。

在继续之前,如果您还没有启动过 Spot 实例,需要先启用 AWS 账户来启动 Spot 实例。为此,请通过以下命令创建服务关联角色(service-linked role):

lua 复制代码
aws iam create-service-linked-role --aws-service-name spot.amazonaws.com || true

如果角色已经成功创建,您会看到以下结果:

kotlin 复制代码
An error occurred (InvalidInput) when calling the CreateServiceLinkedRole operation: Service role name AWSServiceRoleForEC2Spot has been taken in this account, please try a different suffix.

您无需担心此错误,您只需运行上述命令,确保您拥有启动 Spot 实例所需的服务关联角色(service-linked role)。

接下来,您需要创建两个环境变量,稍后会使用到。所需的值可以通过 Terraform 输出变量获得。请确保您在包含 main.tf 文件的同一文件夹中,然后运行以下命令:

ini 复制代码
export CLUSTER_NAME=$(terraform output -raw cluster_name)
export KARPENTER_NODE_IAM_ROLE_NAME=$(terraform output -raw node_instance_profile_name)

💡 注意:

如果您正在使用现有的 EKS 集群,请确保为之前的环境变量设置正确的值,因为我们将使用这些值来配置 Karpenter 的节点池(NodePool)。

接下来,让我们通过以下命令创建默认的 NodePool:

yaml 复制代码
cat <<EOF | kubectl apply -f -
apiVersion: karpenter.sh/v1beta1
kind: NodePool
metadata:
  name: default 
spec:  
  template:
    metadata:
      labels:
        intent: apps
    spec:
      requirements:
        - key: kubernetes.io/arch
          operator: In
          values: ["amd64", "arm64"]
        - key: "karpenter.k8s.aws/instance-cpu"
          operator: Gt
          values: ["4"]
        - key: "karpenter.k8s.aws/instance-memory"
          operator: Gt
          values: ["8191"] # 8 * 1024 - 1
        - key: karpenter.sh/capacity-type
          operator: In
          values: ["spot", "on-demand"]
        - key: karpenter.k8s.aws/instance-category
          operator: In
          values: ["c", "m", "r"]
      nodeClassRef:
        apiVersion: karpenter.k8s.aws/v1beta1
        kind: EC2NodeClass
        name: default
      kubelet:
        systemReserved:
          cpu: 100m
          memory: 100Mi
  disruption:
    consolidationPolicy: WhenUnderutilized
    expireAfter: 168h # 7 * 24h = 168h
  
---   

apiVersion: karpenter.k8s.aws/v1beta1
kind: EC2NodeClass
metadata:
  name: default
spec:
  amiFamily: AL2
  subnetSelectorTerms:          
    - tags:
        karpenter.sh/discovery: ${CLUSTER_NAME}
  securityGroupSelectorTerms:
    - tags:
        karpenter.sh/discovery: ${CLUSTER_NAME}
  role: ${KARPENTER_NODE_IAM_ROLE_NAME}
  tags:
    project: build-on-aws
    IntentLabel: apps
    KarpenterNodePoolName: default
    NodeType: default
    intent: apps
    karpenter.sh/discovery: ${CLUSTER_NAME}
EOF

Karpenter 现在已激活并准备开始启动节点。

让我强调一下您刚刚创建的默认 NodePool 中的几个重要设置:

  • requirements:在此部分,您定义了 Karpenter 可以启动的节点类型。尽量保持灵活,让 Karpenter 根据 Pod 的要求选择合适的实例类型。对于这个 NodePool,您指示 Karpenter 可以启动 Spot 或 On-Demand 实例,包括 c、m 和 r 系列,且要求最少 4 个 vCPU 和 8 GiB 内存。通过这种配置,您可以从 AWS 上 700 多种可用实例类型中选择大约 150 种实例类型。下一节将告诉您为什么选择合适的实例类型如此重要。

  • limits:这是您限制 NodePool 管理的最大资源量的地方。Karpenter 可以启动不同规格的实例,因此,您不需要像在 Auto Scaling 组中那样限制实例的最大数量,而是定义 vCPU 或内存的最大值,从而限制启动的节点数量。Karpenter 提供了一个度量标准,用于监控 NodePool 基于您配置的限制条件的使用百分比。

  • disruption:Karpenter 在启动所需节点方面做得很好,但由于 Pod 会不断增减,集群容量最终可能进入碎片化状态。为避免碎片化并优化集群中的计算节点,您可以启用 consolidation(合并)。启用后,Karpenter 会自动发现可中断的节点,并在需要时启动替代节点。

  • expireAfter:在此,您定义了节点将被删除的时间。这对于强制使用最新 AMI 的新节点非常有用。在本例中,我们将值设置为 7 天。

  • nodeClassRef:在此,您引用了用于启动节点的模板。EC2NodeClass 是您定义节点将使用哪些子网、安全组和 IAM 角色的地方。您还可以设置节点标签,甚至配置 user-data。

这些设置帮助您精细化地管理 Karpenter 节点池的资源和行为,以确保集群能在高效、灵活的状态下运行。关于 Nodepool 的其他配置参数,请参见:

karpenter.sh/docs/concep...

为什么配置多样化的实例类型是最佳实践?

正如您所注意到的,在上述 NodePool 配置中,我们基本上是让 Karpenter 从多样化的实例类型中选择,以启动最合适的实例类型。

如果是 On-Demand 实例,Karpenter 会使用最低价格分配策略,从具有可用容量的最便宜实例类型中启动。当您使用多种实例类型时,可以避免出现 InsufficientInstanceCapacity 错误。

如果是 Spot 实例,Karpenter 会使用价格-容量优化(PCO)分配策略。PCO 会同时考虑价格和容量的可用性,从最不可能被中断且价格最低的 Spot 实例池中启动实例。

对于 Spot 实例,应用多样化配置至关重要。Spot 实例是 EC2 的闲置容量,在需要时可以被 EC2 回收。Karpenter 允许您进行广泛的多样化配置,自动用其他容量池中可用的实例替换被回收的 Spot 实例。

通过这种方式,Karpenter 可以提高集群的可靠性和成本效益,确保在面对容量不足或价格波动时,仍能维持集群的稳定运行。

CloudPilot AI (www.cloudpilot.ai)在 Karpenter 的基础上对节点选择功能进行智能化升级。在选取实例的过程中,除了价格因素外,还将网络带宽、磁盘 I/O、芯片类型等因素纳入考虑范围内,通过智能算法选出兼顾成本和性能的实例类型,以减少资源浪费,增强应用稳定性。

04/部署支持 Spot 实例的工作负载

现在,您将看到 Karpenter 的实际运作。您的默认 NodePool 可以启动 On-Demand 和 Spot 实例,但 Karpenter 会根据您在 Pod 中配置的约束条件,选择合适的节点来启动。接下来,让我们创建一个带有 nodeSelector 的 Deployment,以便将 Pod 部署到 Spot 实例 上。为此,请运行以下命令:

yaml 复制代码
cat <<EOF > workload.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: stateless
spec:
  replicas: 10
  selector:
    matchLabels:
      app: stateless
  template:
    metadata:
      labels:
        app: stateless
    spec:
      nodeSelector:
        intent: apps
        karpenter.sh/capacity-type: spot
      containers:
      - image: public.ecr.aws/eks-distro/kubernetes/pause:v1.29.0-eks-1-29-latest
        name: app
        resources:
          requests:
            cpu: 512m
            memory: 512Mi
EOF
kubectl apply -f workload.yaml

由于没有与 Pod 要求匹配的节点,所有 Pod 将处于 Pending 状态,这将触发 Karpenter 以启动新的节点,产生类似于下方的输出:

arduino 复制代码
$ kubectl get pods
NAME                        READY STATUS  RESTARTS AGE
stateless-5c77994ccb-4mtsp   0/1  Pending 0        3s
stateless-5c77994ccb-4mtsp   0/1  Pending 0        3s
stateless-5c77994ccb-4mtsp   0/1  Pending 0        3s
stateless-5c77994ccb-4mtsp   0/1  Pending 0        3s
stateless-5c77994ccb-4mtsp   0/1  Pending 0        3s
stateless-5c77994ccb-4mtsp   0/1  Pending 0        3s
stateless-5c77994ccb-4mtsp   0/1  Pending 0        3s
stateless-5c77994ccb-4mtsp   0/1  Pending 0        3s
stateless-5c77994ccb-4mtsp   0/1  Pending 0        3s
stateless-5c77994ccb-4mtsp   0/1  Pending 0        3s

为了在等待新节点准备就绪时查看 Karpenter 的日志,您可以创建以下 alias 以便快速访问 Karpenter 控制器的日志:

ini 复制代码
alias kl='kubectl -n karpenter logs -l app.kubernetes.io/name=karpenter --all-containers=true -f --tail=20'

Karpenter 的日志应类似于以下内容(我只突出显示值得注意的其中几行):

ruby 复制代码
$ kl
{"level":"INFO","time":"2024-01-28T21:14:32.625Z","logger":"controller.provisioner","message":"computed new nodeclaim(s) to fit pod(s)","commit":"1072d3b","nodeclaims":1,"pods":10}
{"level":"INFO","time":"2024-01-28T21:14:32.652Z","logger":"controller.provisioner","message":"created nodeclaim","commit":"1072d3b","nodepool":"default","nodeclaim":"default-8blnj","requests":{"cpu":"5330m","memory":"5360Mi","pods":"14"},"instance-types":"c4.2xlarge, c4.4xlarge, c5.2xlarge, c5.4xlarge, c5a.2xlarge and 95 other(s)"}
{"level":"INFO","time":"2024-01-28T21:14:36.823Z","logger":"controller.nodeclaim.lifecycle","message":"launched nodeclaim","commit":"1072d3b","nodeclaim":"default-8blnj","nodepool":"default","provider-id":"aws:///eu-west-2a/i-094792dc93778aa2a","instance-type":"c7g.2xlarge","zone":"eu-west-2a","capacity-type":"spot","allocatable":{"cpu":"7810m","ephemeral-storage":"17Gi","memory":"14003Mi","pods":"58","vpc.amazonaws.com/pod-eni":"38"}}

通过查看日志,您可以看到 Karpenter 做出了以下决策:

  • 检测到有 10 个 Pending 的 Pod,并判断所有这些 Pod 可以部署在一个节点上。

  • 考虑到了 kubeletkube-proxy Daemonsets(另外 2 个 Pod),并将 12 个 Pod 所需的资源进行了汇总。而且发现了 100 种实例类型满足这些要求。

  • 启动了一个 c7g.2xlarge Spot 实例,选择了 eu-west-2a 区域中的该实例,因为这是具有最多空闲容量并且价格最低的 Spot 实例池。

05/将Pod分布在多个可用区

Karpenter 只为所有待调度的 Pod 启动了一个节点。然而,不建议将所有的"鸡蛋放在同一个篮子里",因为如果丢失了这个节点,您需要等待 Karpenter 启动一个替代节点(虽然这个过程可以很快完成,但仍然会有影响)。

为了避免这种情况,并提高工作负载的高可用性,让我们将 Pods 分布到多个可用区。接下来,我们将在部署中配置拓扑分布约束(Topology Spread Constraint,TSP)。

在继续之前,请先删除无状态的 Deployment:

arduino 复制代码
kubectl delete deployment stateless

💡注意:

为了查看 Pods 如何在多个可用区(AZ)中分布,且实例大小相似,请等待 Karpenter 启动的 Pod 和现有的 EC2 实例被移除。

要配置拓扑分布约束(TSP),请在之前下载的 workload.yaml 文件中,将以下代码片段添加到 nodeSelector 和 containers 之间:

yaml 复制代码
      topologySpreadConstraints:
        - labelSelector:
            matchLabels:
              app: stateless
          maxSkew: 1
          minDomains: 2
          topologyKey: topology.kubernetes.io/zone
          whenUnsatisfiable: DoNotSchedule

💡 提示:

您可以在下方链接中下载包含 TSP 的完整版部署Manifest:

raw.githubusercontent.com/build-on-aw...

重新创建无状态部署。如果您从 GitHub 下载了 Manifest,可以直接运行以下命令:

kubectl apply -f workload.yaml

然后,您可以查看 Karpenter 的日志,并注意到其中的不同之处。等待大约一分钟,您应该会看到 Pods 在不同可用区(AZ)中的三个节点上运行:

lua 复制代码
kubectl get nodes -L karpenter.sh/capacity-type,beta.kubernetes.io/instance-type,topology.kubernetes.io/zone -l karpenter.sh/capacity-type=spot

您应该看到以下输出:

csharp 复制代码
NAME                                         STATUS     ROLES    AGE   VERSION               CAPACITY-TYPE   INSTANCE-TYPE   ZONE
ip-10-0-102-121.eu-west-2.compute.internal   NotReady   <none>   1s    v1.29.0-eks-5e0fdde   spot            m7g.2xlarge     eu-west-2c
ip-10-0-36-60.eu-west-2.compute.internal     NotReady   <none>   4s    v1.29.0-eks-5e0fdde   spot            c7g.2xlarge     eu-west-2a
ip-10-0-92-180.eu-west-2.compute.internal    NotReady   <none>   4s    v1.29.0-eks-5e0fdde   spot            c7g.2xlarge     eu-west-2b

06/模拟 Spot 中断(可选步骤)

您可以模拟 Spot 中断来测试应用程序的弹性。如前所述,Spot 是以较大折扣提供的空闲容量,当 EC2 需要回收容量时会终止实例。Spot 中断会提前 2 分钟通知,EC2 在此后会回收该实例。Karpenter 可以监控这些中断(您使用 Terraform 创建的集群已配置为这种方式)。

当发生中断时,NodePool 会在收到 Spot 中断警告后立即启动一个新节点。Karpenter 的平均节点启动时间意味着通常在新节点准备就绪并将 Pods 移动到新节点之前,有足够的时间完成此操作,以避免节点被回收。

CloudPilot AI (www.cloudpilot.ai)针对这一特性也进行了升级,它通过机器学习算法可以预测超过7500个实例的中断事件,并且提前 120 分钟通知用户。并且还能将相应的应用自动迁移到更稳定的实例上。保障服务稳定性,同时解放运维团队的时间。

目前 CloudPilot AI 已开放30天免费试用,复制上方地址至浏览器即可尝鲜

您可以使用故障注入模拟器(FIS)来模拟 Spot 中断。您可以通过控制台或使用 Amazon EC2 Spot Interrupter CLI 来执行此操作。

在本教程中,我将使用一个 CloudFormation 模板来创建 FIS 实验模板,然后运行实验,将 Spot 中断发送到 Karpenter 启动的一个(随机)实例。首先,您需要下载 CloudFormation 模板:

bash 复制代码
wget https://raw.githubusercontent.com/build-on-aws/run-kubernetes-clusters-for-less-with-amazon-ec2-spot-and-karpenter/main/fis/spotinterruption.yaml

运行以下命令创建 FIS 实验模板:

arduino 复制代码
aws cloudformation deploy --stack-name fis-spot-and-karpenter --template-file spotinterruption.yaml --capabilities "CAPABILITY_NAMED_IAM"

现在,您还需要两个额外的终端:

  1. 用于监视节点状态的终端,

  2. 用于查看 Karpenter 日志的终端。

在一个终端中,使用以下命令监控节点状态:

lua 复制代码
kubectl get nodes -L karpenter.sh/capacity-type,beta.kubernetes.io/instance-type,topology.kubernetes.io/zone -l karpenter.sh/capacity-type=spot --watch

在另一个终端中运行以下命令:

ini 复制代码
alias kl='kubectl -n karpenter logs -l app.kubernetes.io/name=karpenter --all-containers=true -f --tail=20';
kl

在第三个终端里,运行命令发送 Spot 中断:

css 复制代码
FIS_EXP_TEMP_ID=$(aws cloudformation describe-stacks --stack-name fis-spot-and-karpenter --query "Stacks[0].Outputs[?OutputKey=='FISExperimentID'].OutputValue" --output text)
aws fis start-experiment --experiment-template-id $FIS_EXP_TEMP_ID --no-cli-pager

查看 Karpenter 的日志,观察 Spot 中断警告发出后会发生什么:

  • Karpenter 会立即将节点打上封锁标记(cordon)并开始驱逐节点(drain)。

  • 同时,它会启动一个替代实例以接管工作负载。

此行为确保即使发生 Spot 中断,工作负载也能快速转移到新的节点,从而减少服务中断的影响。

python 复制代码
{"level":"INFO","time":"2024-01-29T08:47:30.575Z","logger":"controller.interruption","message":"initiating delete from interruption message","commit":"1072d3b","queue":"karpenter-spot-and-karpenter","messageKind":"SpotInterruptionKind","nodeclaim":"default-4w54b","action":"CordonAndDrain","node":"ip-10-0-36-60.eu-west-2.compute.internal"}
{"level":"INFO","time":"2024-01-29T08:47:30.603Z","logger":"controller.node.termination","message":"tainted node","commit":"1072d3b","node":"ip-10-0-36-60.eu-west-2.compute.internal"}
{"level":"INFO","time":"2024-01-29T08:47:31.963Z","logger":"controller.provisioner","message":"found provisionable pod(s)","commit":"1072d3b","pods":"default/stateless-7956bd8d4c-48mj9, default/stateless-7956bd8d4c-spsqr, default/stateless-7956bd8d4c-sm4cp","duration":"18.833162ms"}
{"level":"INFO","time":"2024-01-29T08:47:31.963Z","logger":"controller.provisioner","message":"computed new nodeclaim(s) to fit pod(s)","commit":"1072d3b","nodeclaims":1,"pods":3}
{"level":"INFO","time":"2024-01-29T08:47:31.997Z","logger":"controller.provisioner","message":"created nodeclaim","commit":"1072d3b","nodepool":"default","nodeclaim":"default-6p2qb","requests":{"cpu":"1746m","memory":"1776Mi","pods":"7"},"instance-types":"c4.2xlarge, c4.4xlarge, {"level":"INFO","time":"2024-01-29T08:47:34.823Z","logger":"controller.nodeclaim.lifecycle","message":"launched nodeclaim","commit":"1072d3b","nodeclaim":"default-6p2qb","nodepool":"default","provider-id":"aws:///eu-west-2a/i-005ca44c327470d09","instance-type":"m7g.2xlarge","zone":"eu-west-2a","capacity-type":"spot","allocatable":{"cpu":"7810m","ephemeral-storage":"17Gi","memory":"29158Mi","pods":"58","vpc.amazonaws.com/pod-eni":"38"}}

您可以返回到监视所有节点的终端,观察以下变化:

  • 中断的实例被打上封锁标记(cordoned)。

  • 新的实例在替换旧实例后被启动。

为了更直观地查看整合过程,您还可以使用 eks-node-viewer 工具。eks-node-viewer 是一个用于可视化集群内动态节点使用情况的工具,最初由 AWS 开发,专门用于展示 Karpenter 的整合功能。

它能够显示节点上调度的 Pod 资源请求与可分配容量之间的对比。要启动 eks-node-viewer,在一个新的 Cloud9 终端选项卡中执行以下命令:

eks-node-viewer

💡 提示:

您可能最终只会看到一个或两个 Spot 节点在运行。如果您查看 Karpenter 日志,会发现这是由于整合(consolidation)过程导致的。

07/(可选)部署有状态工作负载

即使集群中运行了 Spot 实例,您仍然可以为那些不适合运行在 Spot 实例上的工作负载启动按需实例(On-Demand Instances)。继续使用之前创建的默认 Karpenter NodePool,但要确保正确配置工作负载。

一种实现方式是借鉴适用于 Spot 的工作负载的方法,使用 nodeSelector 来定义节点属性。

部署以下应用程序以模拟有状态工作负载:

yaml 复制代码
cat <<EOF > workload-stateful.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: stateful
spec:
  replicas: 7
  selector:
    matchLabels:
      app: stateful
  template:
    metadata:
      labels:
        app: stateful
    spec:
      nodeSelector:
        intent: apps
        karpenter.sh/capacity-type: on-demand
      containers:
      - name: app
        image: public.ecr.aws/eks-distro/kubernetes/pause:v1.29.0-eks-1-29-latest
        resources:
          requests:
            cpu: 512m
            memory: 512Mi
EOF
kubectl apply -f workload-stateful.yaml

与之前相同,请等待一分钟,您应该会看到所有 Pods 都已运行,并且有一个按需节点正在运行:

lua 复制代码
$ kubectl get nodes -L karpenter.sh/capacity-type,beta.kubernetes.io/instance-type,topology.kubernetes.io/zone -l karpenter.sh/capacity-type=on-demand
NAME                                         STATUS   ROLES    AGE   VERSION               CAPACITY-TYPE   INSTANCE-TYPE   ZONE
ip-10-0-107-229.eu-west-2.compute.internal   Ready    <none>   13s   v1.29.0-eks-5e0fdde   on-demand       c6g.2xlarge     eu-west-2c

您可以查看 Karpenter 的日志,您会发现其行为与之前处理 Spot 实例的工作负载类似。

08/清理

当您完成本教程后,需要移除2个您创建的 Deployment:

arduino 复制代码
kubectl delete deployment stateless
kubectl delete deployment stateful

Karpenter 的合并 (Consolidation) 功能会在资源需求减少时,优化集群内的节点利用率。等待 30 秒,观察节点被移除后,删除所有资源:

arduino 复制代码
export TF_VAR_region=$AWS_REGION
terraform destroy -target="module.eks_blueprints_addons" --auto-approve
terraform destroy -target="module.eks" --auto-approve
terraform destroy --auto-approve
aws cloudformation delete-stack --stack-name fis-spot-and-karpenter

总结

在 Kubernetes 数据平面节点中使用 Spot 实例可以显著降低计算成本。只要您的工作负载具备容错性、无状态,并且能够使用多种实例类型,就可以利用 Spot 实例的成本优势。

Karpenter 通过高效的实例类型多样化选择,简化了配置 Amazon EKS 集群的过程,并根据实际需求动态地调度计算资源。它不仅能在 Spot 实例的价格波动中找到最佳性价比,还能为非 Spot 友好的工作负载提供 On-Demand 实例支持。

通过 Karpenter,您可以有效管理集群的容量需求,同时充分利用 AWS 提供的弹性计算资源,从而实现更高的成本效益和资源优化。

推荐阅读

云从业者必读!2025年5个云成本管理趋势

15条 Karpenter 最佳实践,轻松掌握弹性伸缩

服务600+客户的3D生成AIGC公司如何实现GPU成本降低70%?

相关推荐
绝无仅有12 分钟前
GoZero 中 `make` 后返回数据与原数据不对齐的几种解决方案
后端·面试·程序员
编程小筑32 分钟前
C语言的语法
开发语言·后端·golang
我不是你的灯笼1 小时前
Go语言的 的输入/输出流(I/O Streams)基础知识
开发语言·后端·golang
Linux520小飞鱼1 小时前
Ruby语言的数据类型
开发语言·后端·golang
java熊猫1 小时前
Ruby语言的编程范式
开发语言·后端·golang
007php0072 小时前
GoZero项目中解决`go.mod`和`go.sum`校验和不匹配问题的解决方案
java·服务器·开发语言·后端·python·golang·php
BinaryBardC2 小时前
Elixir语言的面向对象编程
开发语言·后端·golang
C++小厨神2 小时前
Erlang语言的字符串处理
开发语言·后端·golang
SomeB1oody2 小时前
【Rust自学】11.1. 编写和运行测试
开发语言·后端·rust
海绵波波1073 小时前
flask后端开发(13):登录功能后端实现和钩子函数
后端·python·flask