亚马逊AWS官方博客

在Amazon EKS上部署自定义scheduler实现binpack调度策略

Amazon EKS默认开启的资源调度策略是LeastRequestedPriority,意味着消耗资源最少的节点会优先被调度,这样使得集群的资源在所有节点之间分配的相对均匀。但在一些特定的批处理负载场景下(例如机器学习、数据分析),当集群配置了弹性伸缩,作业发起的Pod总是默认均匀的分布在所有集群节点上,导致很多节点运行着少量独立pod,无法被Cluster Autoscaler组件及时回收,从而造成集群资源的浪费。目前亚马逊云科技推出了 Karpenter开源伸缩组件,该组件默认采用了binpack的机制分配适当的计算资源可以解决类似的问题,具体Karpenter的实现机制本文不作涉及,本文采用另外一种方式,通过在Amazon EKS上部署自定义调度器的方式来实现binpack的调度。

所谓binpack调度,原理是调度器在调度pod到节点的时候,预期在节点上保留最少的未使用 CPU 或内存。此策略最大限度地减少了正在使用的集群节点的数量,也降低了资源碎片。

LeastRequestedPriority和binpack的调度区别可以参考下图。

通过上图可以看到,在binpack的调度策略下,pod会集中使用节点资源。这样闲置的节点(上图中的node3)会被及时的回收掉以避免浪费。

我们要想在Amazon EKS上实现上述调度策略,因为Amazon EKS当前不支持直接修改kube-scheduler的配置增加自定义的KubeSchedulerConfiguration对象配置,所以本文指导用户在Amazon EKS部署自定义的调度器来实现。

实现原理

调度框架是面向 Kubernetes 调度器的一种插件架构, 它为现有的调度器添加了一组新的“插件” API。插件会被编译到调度器之中。插件可以实现一些自定义的调度策略。同时我们可以传递KubeSchedulerConfiguration配置给到Scheduler,通过配置文件定义调度框架所支持的不同阶段执行不同的调度行为。

自定义KubeSchedulerConfiguration配置

在配置文件中,为调度器定义扩展点使用哪些插件,禁用哪些插件。

apiVersion: kubescheduler.config.k8s.io/v1beta1
kind: KubeSchedulerConfiguration
leaderElection:
  leaderElect: false
profiles:
  - schedulerName: my-scheduler

利用Kubernetes Scheduler Framework实现binpack

我们可以在Kubernetes Scheduler Framework中使用Score插件RequestedToCapacityRatio,用于优选阶段给节点打分。将节点按我们希望的方式打分。我们可以将资源利用率为0的时候,得分0分,资源利用率为100时,得分10分,资源利用率越高,得分越高,则这个行为是Binpack的资源分配方式。

如图:在Score阶段应用RequestedToCapacityRatio插件

 

配置多个scheduler共存

Kubernetes集群可以有多个调度器存在,默认调度器是default-scheduler,我们需要在调度器的配置文件中KubeSchedulerProfile 字段schedulerName 取个单独的名字,例如后面的my-scheduler

部署架构

如上文所述,Kubernetes支持在集群中部署多个scheduler,我们将自定义的scheduler以pod的形式部署在Amazon EKS的node上,有需要的pod部署的时候指定使用自定义scheduler实现调度,没有指定scheduler的pod将继续使用集群默认提供的default scheduler进行调度,这样我们可以将自定义的调度策略配置在单独部署的scheduler上以实现更复杂的调度来满足业务需求。

前提条件

本文使用Amazon EKS 1.21版本,不同版本配置会有差异

本地安装docker环境,用来进行镜像打包操作

方案部署步骤

构建自定义scheduler镜像

本文以官方scheduler镜像为例,首先下载Kubernetes对应版本的源码进行编译

git clone https://github.com/kubernetes/kubernetes.git
cd kubernetes
make

构建scheduler 的Docker file和image

FROM busybox
ADD ./_output/local/bin/linux/amd64/kube-scheduler /usr/local/bin/kube-scheduler
docker build -t 1074006*****.dkr.ecr.ap-southeast-1.amazonaws.com/my-kube-scheduler:1.0 .

在Amazon ECR上创建repository,并将镜像push到Amazon ECR

aws ecr create-repository \
    --repository-name my-kube-scheduler \
    --image-scanning-configuration scanOnPush=true \
    --region ap-southeast-1

aws ecr get-login-password --region ap-southeast-1 | docker login --username AWS --password-stdin 1074006*****.dkr.ecr.ap-southeast-1.amazonaws.com

docker push 1074006*****.dkr.ecr.ap-southeast-1.amazonaws.com/my-kube-scheduler:1.0

在Amazon EKS上部署自定义scheduler

创建my-scheduler.yaml文件,修改对应的image url路径

apiVersion: v1
kind: ServiceAccount
metadata:
  name: my-scheduler
  namespace: kube-system
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
  name: my-scheduler-as-kube-scheduler
subjects:
- kind: ServiceAccount
  name: my-scheduler
  namespace: kube-system
roleRef:
  kind: ClusterRole
  name: system:kube-scheduler
  apiGroup: rbac.authorization.k8s.io
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
  name: my-scheduler-as-volume-scheduler
subjects:
- kind: ServiceAccount
  name: my-scheduler
  namespace: kube-system
roleRef:
  kind: ClusterRole
  name: system:volume-scheduler
  apiGroup: rbac.authorization.k8s.io
---
apiVersion: v1
kind: ConfigMap
metadata:
  name: my-scheduler-config
  namespace: kube-system
data:
  my-scheduler-config.yaml: |
    apiVersion: kubescheduler.config.k8s.io/v1beta1
    kind: KubeSchedulerConfiguration
    leaderElection:
      leaderElect: false
    profiles:
      - schedulerName: my-scheduler
        plugins:
          score:
            disabled:
            - name: NodeResourcesLeastAllocated
            enabled:
            - name: RequestedToCapacityRatio
              weight: 100
        pluginConfig:
        - name: RequestedToCapacityRatio
          args:
            shape:
            - utilization: 0
              score: 0
            - utilization: 100
              score: 10
            resources:
            - name: cpu
              weight: 1
            - name: memory
              weight: 1
---
apiVersion: apps/v1
kind: Deployment
metadata:
  labels:
    component: scheduler
    tier: control-plane
  name: my-scheduler
  namespace: kube-system
spec:
  selector:
    matchLabels:
      component: scheduler
      tier: control-plane
  replicas: 1
  template:
    metadata:
      labels:
        component: scheduler
        tier: control-plane
        version: second
    spec:
      serviceAccountName: my-scheduler
      containers:
      - command:
        - /usr/local/bin/kube-scheduler
        - --leader-elect=false
        - --address=0.0.0.0
        - --scheduler-name=my-scheduler
        - --config=/etc/kubernetes/my-scheduler/my-scheduler-config.yaml
        image: 1074006*****.dkr.ecr.ap-southeast-1.amazonaws.com/my-kube-scheduler:1.0
        livenessProbe:
          httpGet:
            path: /healthz
            port: 10259
            scheme: HTTPS
          initialDelaySeconds: 15
        name: kube-second-scheduler
        readinessProbe:
          httpGet:
            path: /healthz
            port: 10259
            scheme: HTTPS
        resources:
          requests:
            cpu: '1'
        securityContext:
          privileged: false
        volumeMounts:
          - name: config-volume
            mountPath: /etc/kubernetes/my-scheduler
      hostNetwork: false
      hostPID: false
      volumes:
        - name: config-volume
          configMap:
            name: my-scheduler-config

其中我们在调度器的配置文件中,关闭了NodeResourcesLeastAllocated调度策略,开启了RequestedToCapacityRatio调度策略,从而实现binpack调度。

在Amazon EKS集群中应用yaml文件

kubectl create -f my-scheduler.yaml

观察scheduler是否正确部署在集群中

kubectl get pods -n kube-system | grep my-scheduler

当显示my-scheduler的pod已经正常显示running状态为正常

NAME                                 READY     STATUS    RESTARTS   AGE
my-scheduler-567cf5487f-x8qwq     1/1       Running   0          2m 

验证自定义scheduler

创建 my-app.yaml文件,添加字段schedulerName: my-scheduler,指定使用my-scheduler作为调度器

## nginx deployment yaml:
apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx-deployment
  namespace: ops-test
  labels:
    app: ops-test
spec:
  replicas: 60
  selector:
    matchLabels:
      app: ops-test
  template:
    metadata:
      labels:
        app: ops-test
        app2: ops-test1
    spec:
      schedulerName: my-scheduler
      containers:
      - name: nginx
        image: nginx
        ports:
        - containerPort: 80
        resources:
          requests:
            cpu: 0.5
            memory: 1Gi

在集群中创建deployment

kubectl create -f my-app.yaml

观察调度效果(当前集群中有10台m5.4xlarge作为node,上面没有其它负载)。

ec2-user@ip-172-31-21-1:~/eks/sche$kubectl -n ops-test get pods -o jsonpath="{range .items[*]}{.metadata.labels.app}{'\t'}{.spec.nodeName}{'\n'}{end}" | sort| uniq -c
     29 ops-test    ip-192-168-42-120.ap-southeast-1.compute.internal
     29 ops-test    ip-192-168-57-79.ap-southeast-1.compute.internal
      2 ops-test    ip-192-168-61-90.ap-southeast-1.compute.internal

删除schedulerName: my-scheduler字段,重新部署后观察效果

ec2-user@ip-172-31-21-1:~/eks/sche$kubectl -n ops-test get pods -o jsonpath="{range .items[*]}{.metadata.labels.app}{'\t'}{.spec.nodeName}{'\n'}{end}" | sort| uniq -c
      6 ops-test    ip-192-168-32-80.ap-southeast-1.compute.internal
      6 ops-test    ip-192-168-35-23.ap-southeast-1.compute.internal
      6 ops-test    ip-192-168-39-133.ap-southeast-1.compute.internal
      6 ops-test    ip-192-168-42-120.ap-southeast-1.compute.internal
      6 ops-test    ip-192-168-53-166.ap-southeast-1.compute.internal
      6 ops-test    ip-192-168-57-79.ap-southeast-1.compute.internal
      6 ops-test    ip-192-168-58-66.ap-southeast-1.compute.internal
      6 ops-test    ip-192-168-59-58.ap-southeast-1.compute.internal
      6 ops-test    ip-192-168-61-90.ap-southeast-1.compute.internal
      6 ops-test    ip-192-168-62-151.ap-southeast-1.compute.internal

我们可以发现,在应用了自定义调度器的binpack调度策略后,pod会优先占满部分节点资源,而Kubernetes默认的调度策略,会将pod按最小资源使用率的方式均匀的分配在每个节点上。

需要留意的问题

维护scheduler增加了集群的复杂度,并且scheduler是个单点

EKS集群升级的时候scheduler也需要手工进行维护升级以避免兼容性问题

总结

目前Amazon EKS不支持直接修改kube-scheduler配置添加自定义的调度插件或配置,本文介绍了通过部署自定义scheduler的方式实现自定义的调度策略,针对本文中提到的binpack调度策略的需求,除了安装自定义scheduler的方式,我们也可以通过安装Karpenter、Apache yunikorn、Volcano等弹性伸缩或调度增强组件实现类似功能,具体可以参考相应的官方文档。

参考链接

https://v1-21.docs.kubernetes.io/docs/tasks/extend-kubernetes/configure-multiple-schedulers/

https://v1-21.docs.kubernetes.io/docs/concepts/scheduling-eviction/resource-bin-packing/

https://karpenter.sh/v0.11.1/

https://blog.cloudera.com/spark-on-kubernetes-gang-scheduling-with-yunikorn/

本篇作者

柳向全

亚马逊云科技解决方案架构师,负责基于AWS的云计算方案架构的咨询和设计,目前主要专注于容器和大数据技术领域研究和AWS云服务在国内和全球的应用和推广。

陈嘉俊

亚马逊云科技解决方案架构师,目前主要负责帮客户进行云架构设计和技术咨询,对容器化等技术方向有深入的了解。