# 调度

  • 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            # 小于
      

# 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 能容忍该污点。
  • 例:
    1. 给节点添加污点:
      kubectl taint nodes node1 k1=v1:NoSchedule
      
      或者修改节点的配置:
      spec:
        taints:
        - effect: NoSchedule
          key: k1
          value: v1
      
    2. 给 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 。
    • 驱逐过程:
      1. 给节点添加一个 NoSchedule 类型的污点,避免调度新 Pod 。
      2. 驱逐该节点上已调度的 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 数组。流程如下:
        1. 检查当前节点的所有 Pod ,筛选出非 terminated 状态的 Pod ,记录在 activePods 数组中。
        2. 检查 activePods ,驱逐占用 ephemeral-storage 超过限制的所有 Pod 。如果有这样的 Pod 被驱逐,则停止执行 synchronize() 。
          • 如果 Pod 的 emptyDir 占用的磁盘空间超过 emptyDir.sizeLimit 限制,则驱逐该 Pod 。
          • 如果 Pod 中某个容器占用的磁盘空间超过 limits.ephemeral-storage 限制,则驱逐该 Pod 。
        3. 检查节点的监控指标,如果低于需要驱逐的阈值 thresholds ,则停止执行 synchronize() 。
        4. 将 activePods 按需要驱逐的优先级排序,然后从前到后逐个驱逐 Pod ,只要有一个 Pod 驱逐成功,则停止执行 synchronize() 。这样会尽量少地驱逐 Pod 。
      • cAdvisor 每隔 housekeeping-interval=10s 采集一次监控数据,因此 synchronize() 最迟会读取到 10s 前的监控数据,做出驱逐的决策时不一定准确。
        • kubelet 每隔 1 分钟监控一次 Pod 占用的磁盘空间,因此驱逐有一定延迟。
      • 如果有 Pod 被驱逐,则需要等待该 Pod 被清理,直到满足以下条件:
        • Pod 处于 terminated 状态
        • Pod 的所有 volume 已被清理(包括删除、回收)
  • 减少节点压力驱逐的措施:

    • 让 Pod 的 requests 资源贴近实际开销,并且 requests 与 limits 的差距尽量小,有利于 k8s 预测 Pod 开销、分散 Pod 负载到各个节点。
    • 给一些重要的应用配置 Guaranteed QoS ,或者单独部署到一些节点,避免与其它 Pod 抢占资源。