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/
 
       本篇作者