背景简介

什么是 Kubernetes 中的调度(Scheduling)?

Kubernetes 中的调度,是将处于待定(Pending)的 pod 绑定到某个节点的过程,由 Kubernetes 中名为 kube-scheduler 的组件执行。

调度器决定一个 pod 是否或在哪能否被调度,由调度器的可配置策略来指导,该策略包括一组规则,称为预判和优选。调度器的决定受到新 pod 第一次被调度时调度器在 Kubernetes 集群中所看到的资源情况的影响。

k8s 自身提供了很多默认调度器和一些高级的调度策略(如 nodeAffinity 等),基本上可以满足大部分业务需求,对于特定业务,我们还可以自定义调度器(使用 python/shell/go 等实现)。

由于 Kubernetes 集群是动态的且状态会发生变化,有时候我们希望将已经运行的 pod 调度到其他节点,原因如下:

这些问题可能引起当前调度器分配 pod 不均衡,造成节点性能瓶颈掉线,最终引发集群雪崩。
我们大致从如下可能的点进行优化:

调研文档

应用实践

调度器在调度过程中是如何计算哪些节点是满足可调度条件的呢?

首先是判断节点是否有可分配的资源,如果有多台满足可调度条件,则调度至资源最优的节点上。

Kubernetes 每个节点上的 kubelet 会监控该节点的 资源容量可用资源,我们可以通过下面的命令查看:

# kubectl describe node "node01" | grep -A16 "Addresses:"
Addresses:
  InternalIP:  172.xx.xxx.xx
  Hostname:    node01
Capacity:
 cpu:                4
 ephemeral-storage:  41926416Ki
 hugepages-1Gi:      0
 hugepages-2Mi:      0
 memory:             8010544Ki
 pods:               110
Allocatable:
 cpu:                3800m
 ephemeral-storage:  38639384922
 hugepages-1Gi:      0
 hugepages-2Mi:      0
 memory:             7408144Ki
 pods:               110

其中,Capacity 表示节点的资源容量,即机器的 CPU、内存、磁盘总量。本地磁盘被定义为 短暂存储,生命周期与 pod 一致。它与 持久存储 相对应。
Allocatable 则表示节点的可用资源,它是除开 系统预留k8s 预留 以外的资源,是可以分配给集群创建 pod 使用的资源。

每个 pod 或容器在创建时都应该声明自己所需资源的 请求值(requests) 和 上限值(limits),以便 kubelet 计算和统计整个节点资源的使用量和百分比。可以使用下面命令来查看当前节点的资源使用情况:

# kubectl describe node "node03" | sed '1,/Non-terminated Pods/d'
  Namespace                  Name                   CPU Requests  CPU Limits  Memory Requests  Memory Limits
  ---------                  ----                   ------------  ----------  ---------------  -------------
  default                    c3-registercenter-0    0 (0%)        0 (0%)      0 (0%)           0 (0%)
  kube-system                calico-node-dvmgv      150m (3%)     300m (7%)   64M (0%)         500M (6%)
  kube-system                kube-proxy-node03      150m (3%)     500m (12%)  64M (0%)         2G (25%)
  kube-system                nginx-proxy-node03     25m (0%)      300m (7%)   32M (0%)         512M (6%)
Allocated resources:
  (Total limits may be over 100 percent, i.e., overcommitted.)
  CPU Requests  CPU Limits   Memory Requests  Memory Limits
  ------------  ----------   ---------------  -------------
  325m (8%)     1100m (28%)  160M (2%)        3012M (38%)

这里可以看到每个 pod 有单独的资源使用统计,最后还有汇总。它是如何统计的呢?

可以发现它只是简单的相加,这些统计是针对 limitsrequests 的,并不是 pod 运行中实际使用的资源。也就是说如果一个 pod 没有设置 limits 和 requests(参考上面的 0 (0%) ),那么它就可以无限使用资源,还不会被纳入统计,这就使得统计结果不准确,可能节点已经超负荷运行了,但 Sheduler 还认为它有可用资源,直至节点被拖死,集群发生雪崩。
因此,我们应该采取一些合理的手段来避免这种情况:

下面介绍一下资源预留和应用场景。

      Node Capacity
---------------------------
|     kube-reserved       |
|-------------------------|
|     system-reserved     |
|-------------------------|
|    eviction-threshold   |
|-------------------------|
|                         |
|      allocatable        |
|   (available for pods)  |
|                         |
---------------------------

上图表示了一个节点上资源的分配关系。

关于资源预留,参考文档:
https://kubernetes.io/docs/tasks/administer-cluster/reserve-compute-resources

重平衡工具 Descheduler

本文着重介绍一下 Kubernetes 孵化器中的一个工具:Descheduler

工具简介

Descheduler 的出现就是为了解决 Kubernetes 自身调度(一次性调度)不足的问题。它以定时任务方式运行,根据已实现的策略,重新去平衡 pod 在集群中的分布。

截止目前,Descheduler 已实现的策略和计划中的功能点如下:

已实现的调度策略

路线图中计划实现的功能点

策略介绍

遵循机制

当 Descheduler 调度器决定于驱逐 pod 时,它将遵循下面的机制:

工具使用

Descheduler 会以 Job 形式在 pod 内运行,因为 Job 具有多次运行而无需人为介入的优势。为了避免被自己驱逐 Descheduler 将会以 关键型 pod 运行,因此它只能被创建建到 kube-system namespace 内。
关于 Critical pod 的介绍请参考:Guaranteed Scheduling For Critical Add-On Pods

要使用 Descheduler,我们需要编译该工具并构建 Docker 镜像,创建 ClusterRole、ServiceAccount、ClusterRoleBinding、ConfigMap 以及 Job。

由于文档中有一些小问题,手动执行这些步骤不会很顺利,我们推荐使用有人维护的现成 helm charts。
项目地址:https://github.com/komljen/helm-charts/tree/master/descheduler
使用方式:

helm repo add akomljen-charts \
     https://raw.githubusercontent.com/komljen/helm-charts/master/charts/

helm install \
     --name ds \
     --namespace kube-system \
     akomljen-charts/descheduler

该 Chart 默认设置如下:

值得注意的是,Descheduler 项目文档中是以 Job 方式运行,属于一次性任务。

问题处理

在手动编译构建 Descheduler 过程中,我们发现官方文档有个小问题。
比如,文档中 Job 的启动方式如下:

     command:
     - "/bin/sh"
     - "-ec"
     - |
    /bin/descheduler --policy-config-file /policy-dir/policy.yaml

即,指定使用 sh 来运行 descheduler,然而该工具的 Dockerfile 却是以白手起家(FROM scratch)方式添加 descheduler 的编译产物到容器内,新容器并不包含工具sh, 这就导致 Job 在运行后因为没有 sh 而失败并不断拉起新的 Job,短时间内大量 Job 被创建, node 节点资源也逐渐被消耗。

关于该问题我提交了一个 isusse #133,解决办法有2种:

然后重新构建并创建 Job。

参考文档

https://akomljen.com/meet-a-kubernetes-descheduler