1. 背景
Karpenter 是 AWS 2021 re:Invent 大会上正式发布的针对Kubernetes集群节点弹性伸缩的开源组件。在Kubernetes集群中该组件可以针对 Unscheduleable Pods 的需求,自动创建合适的新节点并加入集群中。一直以来我们在Kubernetes集群中会使用 Cluster AutoScaler (CA)组件来进行节点的弹性伸缩,通过对Node Group(节点组,在AWS上的实现即为EC2 Auto Scaling Group)的大小进行动态调整从而来实现节点的弹性伸缩。相比而言,Karpenter彻底抛弃了节点组的概念,利用EC2 Fleet API直接对节点进行管理,从而可以更为灵活地选择合适的EC2机型、可用区和购买选项(如On Demand或SPOT)等。同时,在大规模集群中,Karpenter在节点伸缩的效率上也会更加优化。
Karpenter目前已经是生产可用了,有不少用户开始利用Karpenter在AWS的EKS集群上进行节点管理。在这个博客中我们会以一个GPU推理的场景为示例,详细阐述Karpenter的工作原理、配置过程以及测试效果。
2. 架构描述
在这个博客里我们会使用EKS构建如下的一个Kubernets集群:
可以看到在Kubernetes集群中,我们会先创建一个节点组部署管理组件,部署包括 CoreDNS , AWS Load Balancer Controller和Karpenter等管理组件。一般来说这些管理组件所需要的资源比较固定,我们可以提前预估好相关组件所需要的资源,并考虑跨可用区、高可用等因素后,确定该节点组的实例类型和数量。
接下来我们会使用Karpenter来管理推理服务所需要的EC2实例。Karpenter的主要任务为Unsheduable Pod自动创建合适的Node,将Pod调度到这些新创建的Node上面,并在这些Node空闲的时候将其销毁以节省资源。在创建Node时,Karpenter会自动根据Pod的资源需求(如Pod Resource Request设置)、亲和性设置(如Node Affinity等)计算出符合要求的节点类型和数量,因此无须提前进行节点组的资源类型规划。在这个示例集群中我们不需要为GPU实例创建单独的节点组和配置Cluster Autoscaler来进行伸缩,这个工作会由Karpenter来自动完成。
最后我们会部署一个推理服务(resnet server),这个服务由多个Pod组成,每个Pod都可以独立进行工作,对外通过Service暴露到EKS集群外。通过一个Python客户端我们可以提交一张图片到resnet server并得到推理结果。后续生产部署的时候,可以配置前端负载的情况,结合HPA对Pod的数量进行自动的弹性伸缩。Karpenter会根据Pod数量的变化,来自动完成节点的创建和销毁,从而实现节点的弹性伸缩。
同时,考虑到我们会以Load Balancer方式对外暴露service,因此在示例集群中会部署AWS Load Balancer Controller,来自动根据service创建Network Load Balancer 。
3. 部署与测试
接下来我们会按照上述架构进行部署,整体的部署过程如下:
3.1 创建EKS GPU推理集群
接下来使用 eksctl 工具来进行EKS集群的创建。
首先将集群名称、区域、ID等信息配置到环境变量,以便后续命令行操作时使用:
export CLUSTER_NAME="karpenter-cluster"
export AWS_REGION="us-west-2"
export AWS_ACCOUNT_ID="$(aws sts get-caller-identity --query Account --output text)"
接着准备集群的配置文件以供eksctl工具使用:
cat << EOF > cluster.yaml
apiVersion: eksctl.io/v1alpha5
kind: ClusterConfig
metadata:
name: ${CLUSTER_NAME}
region: ${AWS_REGION}
version: "1.21"
tags:
karpenter.sh/discovery: ${CLUSTER_NAME}
iam:
withOIDC: true
managedNodeGroups:
- name: ng-1
privateNetworking: true
instanceType: m5.large
desiredCapacity: 3
EOF
通过以上配置文件创建 EKS 集群:
eksctl create cluster -f cluster.yaml
eksctl会按依次创建集群和托管节点组,在managedNodeGroups部分设置了使用m5机型配置并建立托管节点组,作为Karpenter、AWS Load Balancer Controller等功能的部署节点,以便与GPU任务区分开。
接着配置Endpoint环境变量以供后续安装 Karpenter 使用
export CLUSTER_ENDPOINT="$(aws eks describe-cluster --name ${CLUSTER_NAME} --query "cluster.endpoint" --output text)"
查看节点状态是否都已经处于 ready 状态:
3.2 安装 Karpenter
博客写作时Karpenter版本为0.6.3,这里简要对该版本的安装过程进行整理,读者可以根据需要在Karpenter官网查阅最新版本的安装部署指南:
首先需要创建 Instance Profile ,配置相应的权限,以便Karpenter启动的实例可以有足够的权限进行网络配置和进行镜像拉取等动作:
TEMPOUT=$(mktemp)
curl -fsSL https://karpenter.sh/v0.6.3/getting-started/cloudformation.yaml > $TEMPOUT \
&& aws cloudformation deploy \
--stack-name "Karpenter-${CLUSTER_NAME}" \
--template-file "${TEMPOUT}" \
--capabilities CAPABILITY_NAMED_IAM \
--parameter-overrides "ClusterName=${CLUSTER_NAME}"
接着配置访问权限,以便Karpenter创建的实例可以有权限连接到EKS集群:
eksctl create iamidentitymapping \
--username system:node:{{EC2PrivateDNSName}} \
--cluster "${CLUSTER_NAME}" \
--arn "arn:aws:iam::${AWS_ACCOUNT_ID}:role/KarpenterNodeRole-${CLUSTER_NAME}" \
--group system:bootstrappers \
--group system:nodes
接下来为Karpenter Controller 创建 IAM Role和对应的 Service Account :
eksctl utils associate-iam-oidc-provider --cluster ${CLUSTER_NAME} –approve
eksctl create iamserviceaccount \
--cluster "${CLUSTER_NAME}" --name karpenter --namespace karpenter \
--role-name "${CLUSTER_NAME}-karpenter" \
--attach-policy-arn "arn:aws:iam::${AWS_ACCOUNT_ID}:policy/KarpenterControllerPolicy-${CLUSTER_NAME}" \
--role-only \
--approve
配置环境变量以供后续安装 Karpenter 使用:
export KARPENTER_IAM_ROLE_ARN="arn:aws:iam::${AWS_ACCOUNT_ID}:role/${CLUSTER_NAME}-karpenter"
最后通过Helm来安装karpenter:
helm repo add karpenter https://charts.karpenter.sh/
helm repo update
helm upgrade --install --namespace karpenter --create-namespace \
karpenter karpenter/karpenter \
--version v0.6.3 \
--set serviceAccount.annotations."eks\.amazonaws\.com/role-arn"=${KARPENTER_IAM_ROLE_ARN} \
--set clusterName=${CLUSTER_NAME} \
--set clusterEndpoint=${CLUSTER_ENDPOINT} \
--set aws.defaultInstanceProfile=KarpenterNodeInstanceProfile-${CLUSTER_NAME} \
--set logLevel=debug \
--wait
检查Karpenter Controller是否已经正常运行:
3.3 配置 Karpenter Provisoner
当Kubernetes集群中出现 Unscheduleable Pod 的时候,Karpenter通过Provisioner来确定所需要创建的EC2实例规格和大小。Karpenter安装的时候会定义一个名为 Provisioner 的 Custom Resource 。单个 Karpenter Provisioner 可以处理多个Pod ,Karpenter根据Pod属性(如标签、污点等)做出调度和置备决策。换句话说,使用Karpenter就无需管理多个不同的节点组。
接下来创建Provisioner的配置文件:
cat << EOF > provisioner.yaml
apiVersion: karpenter.sh/v1alpha5
kind: Provisioner
metadata:
name: gpu
spec:
requirements:
- key: karpenter.sh/capacity-type
operator: In
values: ["on-demand"]
- key: node.kubernetes.io/instance-type
operator: In
values: ["g4dn.xlarge", "g4dn.2xlarge"]
taints:
- key: nvidia.com/gpu
effect: "NoSchedule"
limits:
resources:
gpu: 100
provider:
subnetSelector:
karpenter.sh/discovery: karpenter-cluster
securityGroupSelector:
kubernetes.io/cluster/karpenter-cluster: owned
ttlSecondsAfterEmpty: 30
EOF
在这个配置文件里,在spec.requirements
里我们指定了Provisioner创建的购买选项和实例类型。在这个博客里我们先基于On-Demand 实例进行演示,后续的博客我们会整理Spot实例的实践小结,因此我们在spec.requirements
中将 karpenter.sh/capacity-type
中指定为 on-demand 。同时,由于这是一个机器学习的推理应用,我们也将实例类型限定为 G4 , 这里我们选择 g4dn.xlarge
和 g4dn.2xlarge
两种大小。另外我们也设置了 taints nvidia.com/gpu
,后续部署的推理应用也需要容忍对应的 taints 才能部署在这个 Provisioner 提供的EC2实例上。也就是说,Karpenter 在看到 Unscheduable Pods 时,会检查这些 Pods 是否能够容忍这个 taint。如果可以,才会对应创建出 GPU 实例。
另外我们也可以设置这个 Provisioner 可以部署出来的资源的上限,在这个演示环境里我们设置 gpu 数量上限为 100 。同时我们也需要通过 subnetSelector 和 securityGroupSelector 定义的Tag来告诉 Povisioner 如何去找到新创建的实例所在的 VPC 子网和安全组。
最后通过 ttlSecondsAfterEmpty 来告诉 Provisioner 如何来清理掉闲置的EC2实例以便节省成本。这里配置了30,也就是说当创建的EC2实例上有30秒没有运行任何Pod时,Provisoner会判定这个实例为闲置资源并直接进行回收,也就是Terminate EC2实例。
关于Provisioner的详细配置可以参考官网上的文档。接着我们就使用这个配置文件创建 Provisioner:
kubectl apply -f provisioner.yaml
3.4 安装 AWS Loadbalancer Controller
在这个演示中我们会利用 AWS Loadbalancer Controller 来自动为 Service 创建对应的 NLB,可以参考AWS官网查看详细的安装步骤,这里简要记录下 2.4.0 版本的安装过程:
curl -o iam_policy.json https://raw.githubusercontent.com/kubernetes-sigs/aws-load-balancer-controller/v2.4.0/docs/install/iam_policy.json
aws iam create-policy \
--policy-name AWSLoadBalancerControllerIAMPolicy \
--policy-document file://iam_policy.json
eksctl create iamserviceaccount \
--cluster=${CLUSTER_NAME} \
--namespace=kube-system \
--name=aws-load-balancer-controller \
--attach-policy-arn=arn:aws:iam::${AWS_ACCOUNT_ID}:policy/AWSLoadBalancerControllerIAMPolicy \
--override-existing-serviceaccounts \
--approve
helm repo add eks https://aws.github.io/eks-charts
helm repo update
helm install aws-load-balancer-controller eks/aws-load-balancer-controller \
-n kube-system \
--set clusterName=${CLUSTER_NAME} \
--set serviceAccount.create=false \
--set serviceAccount.name=aws-load-balancer-controller
3.5 部署机器学习推理应用
接下来我们会部署一个机器学习推理应用做为示例。这个示例来自一个关于在EKS上进行vGPU部署的博客。该示例会部署一个基于 ResNet 的图片推理服务并通过负载均衡对外暴露一个服务地址,并使用一个Python编写的客户端将图片上传给这个推理服务并获得推理结果。在这里我们进行了简化,不讨论vGPU的配置,而是让这个推理服务直接使用G4实例的GPU卡。
这里是修改过后的推理服务的Deployment/Service的部署文件:
cat << EOF > resnet.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: resnet
spec:
replicas: 1
selector:
matchLabels:
app: resnet-server
template:
metadata:
labels:
app: resnet-server
spec:
# hostIPC is required for MPS communication
hostIPC: true
containers:
- name: resnet-container
image: seedjeffwan/tensorflow-serving-gpu:resnet
args:
- --per_process_gpu_memory_fraction=0.2
env:
- name: MODEL_NAME
value: resnet
ports:
- containerPort: 8501
# Use gpu resource here
resources:
requests:
nvidia.com/gpu: 1
limits:
nvidia.com/gpu: 1
volumeMounts:
- name: nvidia-mps
mountPath: /tmp/nvidia-mps
volumes:
- name: nvidia-mps
hostPath:
path: /tmp/nvidia-mps
tolerations:
- key: nvidia.com/gpu
effect: "NoSchedule"
---
apiVersion: v1
kind: Service
metadata:
name: resnet-service
annotations:
service.beta.kubernetes.io/aws-load-balancer-scheme: internet-facing
service.beta.kubernetes.io/aws-load-balancer-type: external
service.beta.kubernetes.io/aws-load-balancer-nlb-target-type: ip
spec:
type: LoadBalancer
selector:
app: resnet-server
ports:
- port: 8501
targetPort: 8501
EOF
可以看到在Deployment中我们设置了推理服务所需要的GPU资源,以及对污点 nvidia.com/gpu 的容忍,Karpenter Provisioner 会根据这些信息来进行G4实例的创建。另外在Service中我们也定义了相应的 annotation ,以便使用 AWS Load Balancer Controller 来自动创建一个公网可访问的 Network Load Balancer 。
部署完成后,可以检查相应资源是否都运行正常:
检查自动生成的NLB,该地址后续可以供客户端访问:
通过 kubectl exec -it <podname> -- nvidia-smi
查看GPU容器状态
接下来我们部署一个客户端,如下是相应的Python代码,保存为 resnet_client.py
:
from __future__ import print_function
import base64
import requests
import sys
assert (len(sys.argv) == 2), "Usage: resnet_client.py SERVER_URL"
# The server URL specifies the endpoint of your server running the ResNet
# model with the name "resnet" and using the predict interface.
SERVER_URL = f'http://{sys.argv[1]}:8501/v1/models/resnet:predict'
# The image URL is the location of the image we should send to the server
IMAGE_URL = 'https://tensorflow.org/images/blogs/serving/cat.jpg'
def main():
# Download the image
dl_request = requests.get(IMAGE_URL, stream=True)
dl_request.raise_for_status()
# Compose a JSON Predict request (send JPEG image in base64).
jpeg_bytes = base64.b64encode(dl_request.content).decode('utf-8')
predict_request = '{"instances" : [{"b64": "%s"}]}' % jpeg_bytes
# Send few requests to warm-up the model.
for _ in range(3):
response = requests.post(SERVER_URL, data=predict_request)
response.raise_for_status()
# Send few actual requests and report average latency.
total_time = 0
num_requests = 10
for _ in range(num_requests):
response = requests.post(SERVER_URL, data=predict_request)
response.raise_for_status()
total_time += response.elapsed.total_seconds()
prediction = response.json()['predictions'][0]
print('Prediction class: {}, avg latency: {} ms'.format(
prediction['classes'], (total_time*1000)/num_requests))
if __name__ == '__main__':
main()
这个Python客户端会通过命令行参数获取到服务端的地址,自动下载一张示例图片并上传至服务端进行推理,最后输出推理结果。运行这个客户端以便检查推理结果是否可以正常输出:
python resnet_client.py $(kubectl get svc resnet-service -o=jsonpath='{.status.loadBalancer.ingress[0].hostname}')
可以看到推理结果以及相应的延时:
3.6 Karpenter 节点扩缩容测试
Karpenter Controller会在后台一直循环监控Unscheduleable Pods的信息, 并计算Pod资源需求和相应的Tags/Taints等信息,自动生成符合需求的实例类型和数量。相比传统地在EKS中使用托管节点组的方式,用户不需要提前进行节点组的实例类型规划,而是由Karpenter来根据当时的资源需求自动匹配节点类型和数量,相对来说更加灵活。因此也不需要额外增加 Cluster AutoScaler 组件来进行节点扩缩容。
接下来我们增加Pod数量以便触发节点扩容:
kubectl scale deployment resnet --replicas 6
通过查询 Karpenter Controller 的日志我们可以看到扩容时的具体运行逻辑:
kubectl logs -f -n karpenter -l app.kubernetes.io/name=karpenter -c controller
由于每个Pod需求一块GPU卡,因此新增的5个Pod无法在原来的G4实例上创建而处于Pending状态。这时候Karpenter会汇总这5个Pod的资源需求,并判断使用哪种(或多种)类型的实例更加合适。结合我们前面的Provisioner的配置,Karpenter会自动额外创建5个g4.xlarge的On-Demand实例来运行这5个Pod。针对On-Demand实例,Provisioner会自动选择价格最低的可以满足需求的实例。
在实例创建出来后,Karpenter会立即将Pod绑定到相对应的节点,因此在节点启动后Pod即可开始拉取镜像等动作。传统使用托管节点组+Cluster AutoScaler的方式,需要等到节点处于Ready状态后 Kubernetes Scheduler 才会将 Pod 调度至目标节点并开始创建动作,相比之下 Karpenter 会更加高效。
接着我们删除所有的Pod,观察Karpenter如何处理节点缩容的场景:
kubectl scale deployment resnet --replicas 0
从前面的Provisioner的配置我们可以看到,在节点空闲30s后,Karpenter会自动删除空闲节点。
4. 小结
在这个博客里我们以一个机器学习的推理应用部署为例,介绍了在EKS中如何使用Karpenter来进行节点的弹性扩缩容管理。相比在EKS中使用托管节点组和Cluster AutoScaler 的方式,Karpenter 在处理节点扩缩时会更加灵活高效。Karpenter 是一个完全开源的组件,目前支持在AWS上对EKS和用户自建的Kubernetes集群进行节点扩缩容管理,通过其开放的Cloud Provider插件机制,后续可以实现对非AWS云的支持。
在后续的博客我们会进一步探讨如何利用Karpenter来对Spot实例进行管理,敬请关注!
本篇作者