Kubernetes 节点弹性伸缩开源组件 Amazon Karpenter 实践:部署GPU推理应用

20fa9e80b84e379020e8a1e517c7699c.gif

背景

Amazon Karpenter 是亚马逊云科技 2021 re:Invent 大会上正式发布的针对Kubernetes集群节点弹性伸缩的开源组件。在Kubernetes集群中该组件可以针对 Unscheduleable Pods 的需求,自动创建合适的新节点并加入集群中。一直以来我们在Kubernetes集群中会使用 Cluster AutoScaler (CA) 组件来进行节点的弹性伸缩,通过对Node Group (节点组,在亚马逊云科技上的实现即为 EC2 Auto Scaling Group) 的大小进行动态调整从而来实现节点的弹性伸缩。相比而言,Amazon Karpenter彻底抛弃了节点组的概念,利用 EC2 Fleet API直接对节点进行管理,从而可以更为灵活地选择合适的 Amazon EC2 机型、可用区和购买选项(如On Demand或SPOT)等。同时,在大规模集群中,Amazon Karpenter 在节点伸缩的效率上也会更加优化。

Amazon Karpenter 目前已经是生产可用了,有不少用户开始利用 Amazon Karpenter 在亚马逊云科技的 EKS 集群上进行节点管理。在这个博客中我们会以一个 GPU 推理的场景为示例,详细阐述 Amazon Karpenter 的工作原理、配置过程以及测试效果。

架构描述

在这个博客里我们会使用EKS构建如下的一个 Kubernets 集群:

f1e15e1244fa73f8884a303874a9f58e.png

可以看到在Kubernetes集群中,我们会先创建一个节点组部署管理组件,部署包括 CoreDNS , Amazon Load Balancer Controller和Amazon Karpenter等管理组件。一般来说这些管理组件所需要的资源比较固定,我们可以提前预估好相关组件所需要的资源,并考虑跨可用区、高可用等因素后,确定该节点组的实例类型和数量。

接下来我们会使用Amazon Karpenter来管理推理服务所需要的Amazon EC2实例。Amazon Karpenter的主要任务为Unsheduable Pod自动创建合适的Node,将Pod调度到这些新创建的Node上面,并在这些Node空闲的时候将其销毁以节省资源。在创建Node时,Amazon Karpenter会自动根据Pod的资源需求(如Pod Resource Request设置)、亲和性设置(如Node Affinity等)计算出符合要求的节点类型和数量,因此无须提前进行节点组的资源类型规划。在这个示例集群中我们不需要为GPU实例创建单独的节点组和配置Cluster Autoscaler来进行伸缩,这个工作会由Karpenter来自动完成。

最后我们会部署一个推理服务(resnet server),这个服务由多个Pod组成,每个Pod都可以独立进行工作,对外通过Service暴露到EKS集群外。通过一个Python客户端我们可以提交一张图片到resnet server并得到推理结果。后续生产部署的时候,可以配置前端负载的情况,结合HPA对Pod的数量进行自动的弹性伸缩。Amazon Karpenter会根据Pod数量的变化,来自动完成节点的创建和销毁,从而实现节点的弹性伸缩。

同时,考虑到我们会以Load Balancer方式对外暴露service,因此在示例集群中会部署Amazon Load Balancer Controller,来自动根据service创建Network Load Balancer 。

部署与测试

接下来我们会按照上述架构进行部署,整体的部署过程如下:

2afd12801ac6d611005ad72e2817940e.png

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、Amazon Load Balancer Controller等功能的部署节点,以便与GPU任务区分开。

接着配置 Endpoint 环境变量以供后续安装 Amazon Karpenter 使用

export CLUSTER_ENDPOINT="$(aws eks describe-cluster --name ${CLUSTER_NAME} --query "cluster.endpoint" --output text)"

*左滑查看更多

查看节点状态是否都已经处于 ready 状态:

d74de49a475ee2fd859cbf50b2078548.png

84da07089ef81b3fc425f3aefd4edc34.png

2

安装 Amazon Karpenter

博客写作时Amazon Karpenter版本为0.6.3,这里简要对该版本的安装过程进行整理,读者可以根据需要在Amazon Karpenter官网查阅最新版本的安装部署指南:

首先需要创建 Instance Profile ,配置相应的权限,以便Amazon 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}"

*左滑查看更多

接着配置访问权限,以便Amazon 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

*左滑查看更多

配置环境变量以供后续安装 Amazon 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是否已经正常运行:

ed74f9b7820c1dcab0a3909a0532f9af.png

3

配置 Karpenter Provisoner

当Kubernetes集群中出现 Unscheduleable Pod 的时候,Amazon Karpenter通过Provisioner来确定所需要创建的EC2实例规格和大小。Amazon Karpenter安装的时候会定义一个名为 Provisioner 的 Custom Resource  。单个 Karpenter Provisioner 可以处理多个Pod ,Amazon Karpenter根据Pod属性(如标签、污点等)做出调度和置备决策。换句话说,使用Amazon 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 提供的 Amazon EC2 实例上。也就是说,Karpenter 在看到 Unscheduable Pods 时,会检查这些 Pods 是否能够容忍这个 taint。如果可以,才会对应创建出 GPU 实例。

另外我们也可以设置这个 Provisioner 可以部署出来的资源的上限,在这个演示环境里我们设置 gpu 数量上限为 100 。同时我们也需要通过 subnetSelector 和 securityGroupSelector 定义的Tag来告诉 Povisioner 如何去找到新创建的实例所在的 VPC 子网和安全组。

最后通过 ttlSecondsAfterEmpty 来告诉 Provisioner 如何来清理掉闲置的 Amazon EC2 实例以便节省成本。这里配置了30,也就是说当创建的 Amazon EC2实例上有30秒没有运行任何Pod时,Provisoner会判定这个实例为闲置资源并直接进行回收,也就是Terminate EC2实例。

关于Provisioner的详细配置可以参考官网上的文档。

7958a4fece5090c8a799024c5f6a95c6.png

接着我们就使用这个配置文件创建 Provisioner:

kubectl apply -f provisioner.yaml

4

安装 Amazon Loadbalancer Controller

在这个演示中我们会利用 Amazon Loadbalancer Controller 来自动为 Service 创建对应的 NLB,可以参考亚马逊云科技官网查看详细的安装步骤,这里简要记录下 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

*左滑查看更多

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 ,以便使用 Amazon Load Balancer Controller 来自动创建一个公网可访问的 Network Load Balancer 。

部署完成后,可以检查相应资源是否都运行正常:

cb029d69e26e2ed7fe0ad6db5a08644b.png

检查自动生成的NLB,该地址后续可以供客户端访问:

42349042fbc0b60f8beb80bc530f51c3.png

通过kubectl exec -it <podname> -- nvidia-smi查看GPU容器状态

2e92fbcbbebfd08d612f0bdb63a5f827.png

接下来我们部署一个客户端,如下是相应的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}')

可以看到推理结果以及相应的延时:

e1b2392946405137e3b78b1a44d6f950.png

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

a3dd72ceedacc0b4a69bdf80159345ab.png

由于每个Pod需求一块GPU卡,因此新增的5个Pod无法在原来的G4实例上创建而处于Pending状态。这时候Amazon Karpenter会汇总这5个Pod的资源需求,并判断使用哪种(或多种)类型的实例更加合适。结合我们前面的Provisioner的配置,Amazon Karpenter会自动额外创建5个g4.xlarge的On-Demand实例来运行这5个Pod。针对On-Demand实例,Provisioner会自动选择价格最低的可以满足需求的实例。

在实例创建出来后,Amazon Karpenter会立即将Pod绑定到相对应的节点,因此在节点启动后Pod即可开始拉取镜像等动作。传统使用托管节点组+Cluster AutoScaler的方式,需要等到节点处于Ready状态后 Kubernetes Scheduler 才会将 Pod 调度至目标节点并开始创建动作,相比之下 Amazon Karpenter 会更加高效。

接着我们删除所有的Pod,观察Amazon Karpenter如何处理节点缩容的场景:

kubectl scale deployment resnet --replicas 0

adf598f61e2d6f43cd677103ac447f11.png

从前面的Provisioner的配置我们可以看到,在节点空闲30s后,Amazon Karpenter会自动删除空闲节点。

小结

在这个博客里我们以一个机器学习的推理应用部署为例,介绍了在EKS中如何使用Amazon Karpenter来进行节点的弹性扩缩容管理。相比在EKS中使用托管节点组和Cluster AutoScaler 的方式,Karpenter 在处理节点扩缩时会更加灵活高效。Amazon Karpenter 是一个完全开源的组件,目前支持在亚马逊云科技上对 EKS 和用户自建的 Kubernetes 集群进行节点扩缩容管理,通过其开放的Cloud Provider插件机制,后续可以实现对非亚马逊云科技云的支持。

在后续的博客我们会进一步探讨如何利用 Amazon Karpenter 来对 Spot 实例进行管理,敬请关注!

本篇作者

73396acf7f05da5ec2447d43d901b347.png

邱萌

亚马逊云科技解决方案架构师

负责基于亚马逊云科技方案架构的咨询和设计,推广亚马逊云科技平台技术和解决方案。在加入亚马逊云科技之前,曾在企业上云、互联网娱乐、媒体等行业耕耘多年,对公有云业务和架构有很深的理解。

5eeb465e3f9e6a40ba2b0e7b8e0bca0c.png

林俊

亚马逊云科技解决方案架构师

主要负责企业客户的解决方案咨询与架构设计优化。

b308412fdfae44ef4d220b5f132d150b.gif

a7392b01fa2247c45baf667292e5615e.gif

听说,点完下面4个按钮

就不会碰到bug了!

f18a76e3bdc0eecc0d5785f53daa3a1a.gif

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值