# 调度
- kube-scheduler 默认可能将 Pod 调度到任一节点上。可以配置 nodeSelector、Affinity、Taint 等条件,只将 Pod 部署到满足条件的节点上。
# nodeSelector
:节点选择器,表示只允许将 Pod 调度到某些节点。
- 用法:先给节点添加 Label ,然后在 Pod spec 中配置该 Pod 需要的 Node Label 。
- 例:
kind: Pod spec: nodeSelector: # 根据节点标签,筛选出可用节点,可能有多个 project: test # nodeName: node-1 # 也可以用 nodeName 直接指定一个节点
# nodeAffinity
:节点的亲和性,表示将 Pod 优先调度到某些节点,比 nodeSelector 更灵活。
- 亲和性的几种类型:
preferredDuringScheduling # 为 Pod 调度节点时,优先调度到满足条件的节点上。如果没有这样的节点,则调度到其它节点上(软性要求) requiredDuringScheduling # 为 Pod 调度节点时,只能调度到满足条件的节点上。如果没有这样的节点,则不可调度(硬性要求) IgnoredDuringExecution # 当 Pod 已调度之后,不考虑亲和性,因此不怕节点的标签变化(软性要求) RequiredDuringExecution # 当 Pod 已调度之后,依然考虑亲和性。如果节点的标签变得不满足条件,则可能驱逐 Pod (硬性要求,尚不支持)
- 例:
kind: Pod spec: affinity: nodeAffinity: # nodeAffinity 之下可以定义多种亲和性,需要同时满足 requiredDuringSchedulingIgnoredDuringExecution: nodeSelectorTerms: # nodeSelectorTerms 之下可以定义多个条件,只需满足一个即可 - matchExpressions: - key: kubernetes.io/os operator: In values: - linux preferredDuringSchedulingIgnoredDuringExecution: - weight: 1 # preferredDuringScheduling 必须设置 weight 权重。这会遍历所有节点,如果满足一个条件,则累计 weight 作为得分,最后选出总得分最高的节点用于调度 preference: matchExpressions: # matchExpressions 之下可以定义多个条件,需要全部满足 - key: kubernetes.io/hostname operator: In values: - node-1 - weight: 50 preference: ...
- operator 可以是以下类型:
Exists # 节点存在该 key DoesNotExist # 与 Exists 相反,可实现反亲和性 In # 节点存在该 key ,且其值在指定的列表中 NotIn Gt # 节点存在该 key ,且其值大于指定值 Lt # 小于
- operator 可以是以下类型:
# podAffinity
:Pod 间的亲和性,表示将某些 Pod 优先调度到同一区域。
- 例:
kind: Pod spec: affinity: podAffinity: # Pod 间的亲和性 requiredDuringSchedulingIgnoredDuringExecution: - labelSelector: matchLabels: project: test topologyKey: kubernetes.io/hostname # 根据指定 label 划分节点的拓扑域,然后将满足亲和性的多个 Pod 集中调度到同一个拓扑域 # namespaces: # 默认将当前 Pod 与所有命名空间的 Pod 考虑亲和性,可以指定命名空间 # - default # namespaceSelector: # matchLabels: # project: test podAntiAffinity: # Pod 间的反亲和性 preferredDuringSchedulingIgnoredDuringExecution: - weight: 100 podAffinityTerm: labelSelector: matchLabels: k8s-app: nginx topologyKey: kubernetes.io/hostname
# topology
:根据某个 label 取值的不同,将所有节点划分为多个拓扑域。
- 例:
kind: Pod spec: topologySpreadConstraints: - labelSelector: # 筛选出某类型的 Pod (当前命名空间下) matchLabels: k8s-app: nginx topologyKey: kubernetes.io/zone # 根据指定 label 划分节点的拓扑域,然后将该类型的 Pod 分散调度到不同的拓扑域,使得数量尽量均衡 maxSkew: 1 # 计算该类型的 Pod 调度到各个拓扑域的数量,取最大值与最小值之差,称为 maxSkew ,表示分布不均的程度 whenUnsatisfiable: DoNotSchedule # 如果 Pod 不满足 maxSkew 条件,默认不会调度。取值为 ScheduleAnyway 则依然调度,但尽量减小 maxSkew # - labelSelector: ... # 一个 Pod 可定义多个 topologySpreadConstraints 条件,调度时需要同时满足
- 如果某个节点不存在指定 label ,则不属于拓扑域,不会用于调度上述 Pod ,除非设置了 ScheduleAnyway 。
- 上述条件只会在调度 Pod 时生效。如果终止已调度的 Pod ,则可能导致不满足 maxSkew 条件。
# Taint、Tolerations
:污点、容忍度。
- 用途与亲和性相反。kube-scheduler 不会将 Pod 调度到有污点的节点上,除非 Pod 能容忍该污点。
- 例:
- 给节点添加污点:或者修改节点的配置:
kubectl taint nodes node1 k1=v1:NoSchedule
spec: taints: - effect: NoSchedule key: k1 value: v1
- 给 Pod 配置容忍度:
kind: Pod spec: tolerations: - key: k1 operator: Equal value: v1 effect: NoSchedule - key: k2 operator: Exists effect: PreferNoSchedule - key: k3 operator: Exists effect: NoExecute # tolerationSeconds: 3600
- 给节点添加污点:
- Taint 的效果分为三种:
- PreferNoSchedule :如果 Pod 不容忍该污点,则尽量不调度到该节点上,除非其它节点不可用,或者已经调度到该节点。
- NoSchedule :如果 Pod 不容忍该污点,则不调度到该节点上。如果已经调度了,则继续运行该 Pod 。
- NoExecute :如果 Pod 不容忍该污点,则不调度到该节点上。如果已经调度了,则驱逐该 Pod 。
- 可以额外设置 tolerationSeconds ,表示即使 Pod 容忍该污点,也最多只能保留指定秒数,超时之后就会被驱逐,除非在此期间污点消失。
- 在 Tolerations 中:
- 当 operator 为 Equal 时,如果 effect、key、value 与 Taint 的相同,才匹配该 Taint 。
- 当 operator 为 Exists 时,如果 effect、key 与 Taint 的相同,才匹配该 Taint 。
- 如果不指定 key ,则匹配 Taint 的所有 key 。
- 如果不指定 effect ,则匹配 Taint 的所有 effect 。
- 当节点在某方面异常时,node controller 可能自动添加一些污点。例如:
taints: - effect: NoExecute key: node.kubernetes.io/not-ready # 节点不处于 ready 状态 - effect: NoExecute key: node.kubernetes.io/unreachable # 节点与 apiserver 断开连接 - effect: NoSchedule key: node.kubernetes.io/unschedulable # 节点不可用于调度 Pod 。例如执行 kubectl cordon 命令时会添加该污点 - effect: NoSchedule key: node.kubernetes.io/disk-pressure # 节点磁盘不足,触发压力驱逐 - effect: NoSchedule key: node.kubernetes.io/memory-pressure # 节点内存不足,触发压力驱逐 - effect: NoSchedule key: node.kubernetes.io/pid-pressure # 节点 PID 不足,触发压力驱逐
- DaemontSet 类型的 Pod 默认会添加对上述 6 种污点的容忍度,因此在这些情况下并不会被驱逐。
# 节点压力驱逐
节点压力驱逐(Node-pressure Eviction):当节点可用资源低于阈值时,kubelet 会自动驱逐该节点上的 Pod 。
- kube-scheduler 会限制每个节点上所有 pod.request 的资源之和不超过 allocatable 。但 Pod 实际占用的资源可能更多,用 pod.limit 限制也不可靠,因此需要驱逐 Pod ,避免整个主机的资源耗尽。
- 决定驱逐的资源主要是内存、磁盘,并不考虑 CPU 。
- 驱逐过程:
- 给节点添加一个 NoSchedule 类型的污点,避免调度新 Pod 。
- 驱逐该节点上已调度的 Pod 。一次驱逐一个 Pod ,直到不满足节点压力驱逐的阈值。
- 优先驱逐这些 Pod :
- 实际占用内存、磁盘多于 requests 的 Pod ,按差值排序。
- Priority 较低的 Pod 。
几种驱逐信号:
memory.available # 节点可用内存不足 nodefs.available # 节点可用磁盘不足 nodefs.inodesFree # 节点的 inode 数量达到最大值 imagefs.available imagefs.inodesFree pid.available # 节点的 PID 数量达到最大值
- 计算关系:
memory.capacity # 节点内存的硬件资源量,取自 /proc/meminfo 文件中的 MemTotal memory.allocatable = memory.capacity - kube-reserved - system-reserved # 总共可分配的资源量,等于硬件量减去保留量 memory.allocated = sum(pod.requests.memory) # 已分配的资源量,等于当前调度到该节点的所有 Pod 的 requests 总和 memory.unallocated = memory.allocatable - memory.allocated # 未分配的资源量 memory.available = memory.capacity - memory.workingSet # 实际可用的资源量,这是根据 Cgroup 监控数据计算的,与 pod.requests 无关
- nodefs、imagefs 一般都位于主机的主文件系统上(俗称系统盘)。
- nodefs :用于存放 /var/lib/kubelet/ 目录。
- imagefs :用于存放 /var/lib/docker/ 目录,主要包括 docker images、container rootfs、container log 数据。
- 修改了节点的系统盘大小之后,可以重启 kubelet ,让它更新 capacity 。
- kubelet 发出驱逐信号时,会给节点添加以下几种 condition 状态:
MemoryPressure DiskPressure PIDPressure
- 计算关系:
节点压力驱逐的阈值分为两种:
- 硬驱逐:满足阈值时,立即强制杀死 Pod 。
- 软驱逐:满足阈值且持续一段时间之后,在宽限期内优雅地终止 Pod ,不会考虑 Pod 的 terminationGracePeriodSeconds 。
kubelet 默认没启用软驱逐,只启用了硬驱逐,配置如下:
memory.available<100Mi # 如果节点可用内存低于阈值,则发出该驱逐信号 nodefs.available<10% imagefs.available<15% nodefs.inodesFree<5%
- 阈值可以是绝对值、百分比。百分比是指 available / capacity 。
- 同一指标同时只能配置一个阈值。
分析 kubelet 的源代码:
- kubelet 启动时,先后启动 cAdvisor、containerManager、evictionManager 等模块。
- 调用
evictionManager.Start()
函数即可启动 evictionManager 模块,它会运行一个主循环:for { if evictedPods := m.synchronize(diskInfoProvider, podFunc); evictedPods != nil { klog.InfoS("Eviction manager: pods evicted, waiting for pod to be cleaned up", "pods", klog.KObjSlice(evictedPods)) m.waitForPodsCleanup(podCleanedUpFunc, evictedPods) // 如果有 Pod 被驱逐,则等待该 Pod 被清理,最多等待 podCleanupTimeout=30s } else { time.Sleep(monitoringInterval) // 间隔时间默认为 10s ,与 housekeeping-interval 相等 } }
- 每次循环的主要任务是执行 synchronize() ,判断是否要驱逐 Pod ,并返回 evictedPods 数组。流程如下:
- 检查当前节点的所有 Pod ,筛选出非 terminated 状态的 Pod ,记录在 activePods 数组中。
- 检查 activePods ,驱逐占用 ephemeral-storage 超过限制的所有 Pod 。如果有这样的 Pod 被驱逐,则停止执行 synchronize() 。
- 如果 Pod 的 emptyDir 占用的磁盘空间超过 emptyDir.sizeLimit 限制,则驱逐该 Pod 。
- 如果 Pod 中某个容器占用的磁盘空间超过 limits.ephemeral-storage 限制,则驱逐该 Pod 。
- 检查节点的监控指标,如果低于需要驱逐的阈值 thresholds ,则停止执行 synchronize() 。
- 将 activePods 按需要驱逐的优先级排序,然后从前到后逐个驱逐 Pod ,只要有一个 Pod 驱逐成功,则停止执行 synchronize() 。这样会尽量少地驱逐 Pod 。
- cAdvisor 每隔 housekeeping-interval=10s 采集一次监控数据,因此 synchronize() 最迟会读取到 10s 前的监控数据,做出驱逐的决策时不一定准确。
- kubelet 每隔 1 分钟监控一次 Pod 占用的磁盘空间,因此驱逐有一定延迟。
- 如果有 Pod 被驱逐,则需要等待该 Pod 被清理,直到满足以下条件:
- Pod 处于 terminated 状态
- Pod 的所有 volume 已被清理(包括删除、回收)
- 每次循环的主要任务是执行 synchronize() ,判断是否要驱逐 Pod ,并返回 evictedPods 数组。流程如下:
减少节点压力驱逐的措施:
- 让 Pod 的 requests 资源贴近实际开销,并且 requests 与 limits 的差距尽量小,有利于 k8s 预测 Pod 开销、分散 Pod 负载到各个节点。
- 给一些重要的应用配置 Guaranteed QoS ,或者单独部署到一些节点,避免与其它 Pod 抢占资源。