「自動化配置」是 Kubernetes 排程器的核心功能:把新的 Pod 指派到能滿足容器資源請求、且遵守排程政策的節點上。本模式描述 Kubernetes 排程演算法的原則,以及如何從外部影響配置決策

問題#

一個規模合理的微服務系統,包含數十甚至數百個彼此隔離的行程。容器與 Pod 為封裝與部署提供了良好的抽象,卻沒有解決「把這些行程放到合適節點上」的問題。當微服務數量龐大且持續成長,逐一指派與配置根本不是可管理的活動。

更麻煩的是,這是一個移動中的靶:

  • 容器彼此之間有依賴、對節點有依賴、還有資源需求,而這一切都隨時間改變
  • 叢集可用資源也隨時間變動——叢集擴大或縮小、或被已配置的容器吃掉。
  • 配置容器的方式,還會影響分散式系統的可用性、效能與容量

解法#

在 Kubernetes 中,把 Pod 指派給節點是排程器的工作。這個領域高度可配置,撰寫本書時仍在快速演進與變動。Kubernetes 排程器是強大且省時的工具,在整個平台中扮演根本角色;但與其他 Kubernetes 元件(API Server、kubelet)一樣,它可以獨立執行,也可以完全不用。

宏觀來看,排程器的主要操作是:從 API Server 取得每個新建立的 Pod 定義,並把它指派給一個節點。只要存在合適的節點,它就會為每個 Pod 找到落點——不論那是初次應用配置、擴大規模,或是把應用從不健康的節點搬到較健康的節點。它在做這件事時會考量執行期依賴、資源需求、以及高可用性的引導政策:一方面把 Pod 水平分散,另一方面為了效能與低延遲互動而把 Pod 共置在一起。

但排程器要正確運作、實現宣告式配置,需要三個前提:有可用容量的節點已宣告資源輪廓的容器、以及到位的引導政策

節點的可用資源#

首先,叢集必須有資源容量足夠的節點來執行新 Pod。每個節點都有可供執行 Pod 的容量,排程器會確保「某 Pod 所請求的資源總和」小於「節點可配置的可用容量」。以一台專供 Kubernetes 使用的節點為例,容量計算公式為:

Allocatable [可供應用 Pod 使用的容量] =
    Node Capacity [節點上的可用容量]
        - Kube-Reserved [kubelet、容器執行期等 Kubernetes 常駐程式]
        - System-Reserved [sshd、udev 等作業系統常駐程式]

若你不為支撐作業系統與 Kubernetes 本身的系統常駐程式保留資源,Pod 可能被排程到吃滿節點全部容量,導致 Pod 與系統常駐程式互搶資源,在節點上引發資源匱乏問題。

另外要留意:若節點上跑著不由 Kubernetes 管理的容器,這部分不會反映在 Kubernetes 的節點容量計算中。

針對這個限制的變通做法:執行一個什麼都不做的佔位 Pod,只設定 CPU 與記憶體的資源請求,數量對應那些未被追蹤的容器實際用量。這種 Pod 純粹用來代表並保留未追蹤容器的資源消耗,協助排程器建立更準確的節點資源模型。

容器的資源需求#

高效配置的另一項重要前提,是容器已定義好自己的執行期依賴與資源需求(詳見「可預期的需求」)。歸結起來就是:容器要宣告自己的資源輪廓(request 與 limit)與環境依賴(如儲存或連接埠)。唯有如此,Pod 才能被合理地指派到節點,並在尖峰時段互不干擾地執行。

配置政策#

最後一塊拼圖,是符合你應用需求的過濾政策(predicate)與優先政策(priority)。排程器預設配置的一組政策已能應付多數使用案例,但可在啟動時以另一組政策覆寫。

排程器政策與自訂排程器只能由管理員作為叢集組態的一部分來定義。一般使用者只能引用預先定義好的排程器。

{
  "kind": "Policy",
  "apiVersion": "v1",
  "predicates": [
    { "name": "PodFitsHostPorts" },
    { "name": "PodFitsResources" },
    { "name": "NoDiskConflict" },
    { "name": "NoVolumeZoneConflict" },
    { "name": "MatchNodeSelector" },
    { "name": "HostName" }
  ],
  "priorities": [
    { "name": "LeastRequestedPriority", "weight": 2 },
    { "name": "BalancedResourceAllocation", "weight": 1 },
    { "name": "ServiceSpreadingPriority", "weight": 2 },
    { "name": "EqualPriority", "weight": 1 }
  ]
}
  • Predicates(過濾規則):篩掉不合格的節點。例如 PodFitsHostPorts 只會把「請求特定固定主機埠」的 Pod 排到那些該埠仍可用的節點上。
  • Priorities(優先規則):依偏好為可用節點排序。例如 LeastRequestedPriority 給予「已請求資源較少」的節點更高的優先權。

除了調整預設排程器的政策之外,你也可以同時執行多個排程器,並讓 Pod 指定要由誰來配置:給另一個排程器實例一個獨特名稱、以不同方式配置後啟動,接著在 Pod 規格中加上 .spec.schedulerName 欄位指向你的自訂排程器,該 Pod 就只會被那個排程器接手。

排程流程#

圖 6-1:Pod 到節點的指派流程

當一個尚未指派節點的 Pod 被建立,排程器就會連同所有可用節點與整組過濾/優先政策一起接手:

  1. 第一階段:套用過濾政策,移除所有不符合該 Pod 條件的節點。
  2. 第二階段:剩餘節點依權重排序。
  3. 第三階段:Pod 被指派到一個節點——這正是排程流程的主要產出。

多數情況下,最好讓排程器自行完成 Pod 到節點的指派,不要微觀管理配置邏輯。但某些場合你可能想強制把 Pod 指派到特定節點或節點群組,這時可以用節點選擇器(node selector).spec.nodeSelector 指定一組鍵值對,節點必須具備這些標籤才有資格執行該 Pod:

apiVersion: v1
kind: Pod
metadata:
  name: random-generator
spec:
  containers:
    - image: k8spatterns/random-generator:1.0
      name: random-generator
  nodeSelector:
    disktype: ssd # 節點必須具備的標籤集合,才會被考慮為此 Pod 的落點

例如你想強制 Pod 跑在有 SSD 儲存或 GPU 加速硬體的特定節點上,上例只有標記 disktype=ssd 的節點才有資格。

除了自訂標籤,你也可以使用每個節點都有的預設標籤。每個節點都有獨一無二的 kubernetes.io/hostname 標籤,可用主機名稱指定落點;其他指出作業系統、架構與實例型別的預設標籤,同樣可用於配置。

節點親和性(Node Affinity)#

Kubernetes 還支援更有彈性的排程配置方式,節點親和性就是其一。它是節點選擇器的一般化,允許把規則指定為「必要」或「偏好」:

  • 必要規則:必須滿足,Pod 才會被排程到該節點。
  • 偏好規則:只表達偏好,為相符節點加權,但不強制。

此外,節點親和性大幅擴展了你能表達的約束種類,語言更具表現力,支援 InNotInExistsDoesNotExistGtLt 等運算子:

apiVersion: v1
kind: Pod
metadata:
  name: random-generator
spec:
  affinity:
    nodeAffinity:
      # 硬性要求:節點必須有超過三顆核心(由節點標籤指出)才會進入排程考量。
      # 執行期間節點條件若改變,此規則不會被重新評估。
      requiredDuringSchedulingIgnoredDuringExecution:
        nodeSelectorTerms:
          - matchExpressions: # 以標籤比對
              - key: numberCores
                operator: Gt
                values: ["3"]
      # 軟性要求:一串帶權重的選擇器。每個節點會加總所有相符選擇器的權重,
      # 在滿足硬性要求的前提下,選出總分最高的節點。
      preferredDuringSchedulingIgnoredDuringExecution:
        - weight: 1
          preference:
            matchFields: # 以欄位比對(以 jsonpath 指定)
              - key: metadata.name
                operator: NotIn
                values: ["master"]
  containers:
    - image: k8spatterns/random-generator:1.0
      name: random-generator

以欄位比對時,運算子只允許 InNotIn,且 values 清單中只能給一個值。

Pod 親和性與反親和性#

節點親和性是更強大的排程方式,當 nodeSelector 不夠用時應優先採用。但它只能基於標籤或欄位比對來限制 Pod 可以跑在哪些節點上,無法表達 Pod 之間的依賴——也就是「某 Pod 相對於其他 Pod 該放在哪」。要表達「如何分散 Pod 以達成高可用」或「如何打包共置以改善延遲」,就要用 Pod 親和性與反親和性

節點親和性運作在節點粒度,Pod 親和性則不限於節點,可以在多個拓撲層級表達規則。透過 topologyKey 欄位與相符標籤,可以組合節點、機架、雲端供應商可用區、地區等領域的規則:

apiVersion: v1
kind: Pod
metadata:
  name: random-generator
spec:
  affinity:
    podAffinity:
      # 針對「目標節點上正在執行的其他 Pod」的必要配置規則
      requiredDuringSchedulingIgnoredDuringExecution:
        - labelSelector: # 用來找出要共置對象的標籤選擇器
            matchLabels:
              confidential: high
          # 跑著 confidential=high 這類 Pod 的節點,應帶有 security-zone 標籤;
          # 此處定義的 Pod 會被排程到帶有相同標籤與值的節點上。
          topologyKey: security-zone
    podAntiAffinity: # 反親和性規則:找出 Pod 不該被放置的節點
      preferredDuringSchedulingIgnoredDuringExecution:
        - weight: 100
          podAffinityTerm:
            # 此規則表示:本 Pod 不應(但仍可能)被放到任何跑著
            # confidential=none 標籤 Pod 的節點上。
            labelSelector:
              matchLabels:
                confidential: none
            topologyKey: kubernetes.io/hostname
  containers:
    - image: k8spatterns/random-generator:1.0
      name: random-generator

如同節點親和性,Pod 親和性與反親和性也有硬性與軟性要求,分別是 requiredDuringSchedulingIgnoredDuringExecutionpreferredDuringSchedulingIgnoredDuringExecution

欄位名稱中的 IgnoredDuringExecution 後綴是為了未來擴充而保留。目前若節點標籤改變、使親和性規則不再成立,既有 Pod 仍會繼續執行;未來可能會把執行期變化也納入考量。

反過來說,若節點標籤改變後,讓原本未被排程的 Pod 得以符合其節點親和性選擇器,這些 Pod 就會被排到該節點上。

污點與容忍(Taints and Tolerations)#

還有一項更進階的功能,用來控制 Pod 可以被排到哪、又被允許在哪執行:污點與容忍

方向剛好相反:節點親和性是 Pod 的屬性,讓 Pod 挑節點;污點與容忍則讓節點控制哪些 Pod 該(或不該)被排到自己身上。

污點是節點的特性,一旦存在,就會阻止 Pod 被排到該節點——除非 Pod 對該污點有容忍。因此污點與容忍可視為一種 opt-in:允許排程到那些預設不可用的節點;而親和性規則則是 opt-out:明確選出要跑在哪些節點,從而排除所有未被選中的節點。

用 kubectl 為節點加上污點:

kubectl taint nodes master node-role.kubernetes.io/master="true":NoSchedule

效果如下:

apiVersion: v1
kind: Node
metadata:
  name: master
spec:
  taints:
    # 節點 spec 上的污點,標記此節點不可被排程,除非 Pod 容忍此污點
    - effect: NoSchedule
      key: node-role.kubernetes.io/master

Pod 端加上相符的容忍(注意 keyeffect 的值必須與污點一致):

apiVersion: v1
kind: Pod
metadata:
  name: random-generator
spec:
  containers:
    - image: k8spatterns/random-generator:1.0
      name: random-generator
  tolerations:
    # 容忍(即:仍納入排程考量)帶有 node-role.kubernetes.io/master 污點的節點。
    # 正式叢集上,master 節點會設此污點以阻止 Pod 被排上去;
    # 這樣的容忍讓本 Pod 仍能被安裝到 master 上。
    - key: node-role.kubernetes.io/master
      operator: Exists
      # 只在污點指定 NoSchedule 效果時才容忍。此欄位可留空,
      # 留空時容忍適用於所有效果。
      effect: NoSchedule

污點分三種效果:

  • 硬性effect=NoSchedule):阻止排程到該節點。
  • 軟性effect=PreferNoSchedule):盡量避免排程到該節點。
  • 驅逐effect=NoExecute):可把已在執行的 Pod 從節點上逐出。

污點與容忍能支撐複雜的使用案例,例如為一組專屬 Pod 保留專用節點,或藉由為問題節點加污點來強制逐出其上的 Pod。

你可以依應用的高可用與效能需求來影響配置,但別把排程器限制得太死,以免把自己逼進死角——再也排不進新 Pod,卻留下大量擱淺資源。例如當容器的資源需求粒度太粗、或節點太小,就可能在節點上留下用不掉的擱淺資源。

圖 6-2:排程到節點的行程與擱淺資源——節點 A 有 4 GB 記憶體無法利用,因為已無 CPU 可容納其他容器

改善方式之一是建立資源需求較小的容器;另一個解法是使用 Kubernetes descheduler,協助把節點「重組碎片」以提升使用率。

Descheduler:處理配置後的漂移#

一旦 Pod 被指派到節點,排程器的工作就結束了——除非該 Pod 被刪除、並在未指派節點的狀態下重建,否則它不會改變 Pod 的落點。這會帶來兩個長期問題:

  • 資源碎片化,叢集資源使用率變差。
  • 排程決策基於當下的叢集視角。若叢集是動態的、節點資源輪廓改變或加入新節點,排程器不會修正先前的配置;改變節點標籤同樣不會修正過去的配置。

Kubernetes descheduler 就是為這些情境而生。它是選用功能,通常在叢集管理員認為適合整理與重組叢集時,以 Job 形式執行、重新排程 Pod。它帶有若干預先定義的政策,可啟用、調校或停用(政策以檔案傳入 descheduler Pod):

descheduler 的四種策略

RemoveDuplicates

確保同一節點上只執行「與某個 ReplicaSet 或 Deployment 相關聯」的單一 Pod;超出一個的多餘 Pod 會被逐出。這在以下情境有用:某節點變得不健康,管理控制器在其他健康節點上啟動了新 Pod;當不健康節點恢復並重新加入叢集時,執行中的 Pod 數就超過期望值,descheduler 可以把數量拉回期望的複本數。當排程政策與叢集拓撲在初次配置後發生變化時,移除節點上的重複 Pod 也有助於讓 Pod 更平均地分散到更多節點。

LowNodeUtilization

找出使用率過低的節點,並從使用率過高的節點逐出 Pod,期待這些 Pod 落到低使用率節點上,達成更好的分散與資源利用。「低使用率節點」定義為 CPU、記憶體或 Pod 數低於設定的 thresholds;「高使用率節點」則是高於設定的 targetThresholds。介於兩者之間的節點屬於使用率適當,不受此策略影響。

RemovePodsViolatingInterPodAntiAffinity

逐出違反 Pod 間反親和性規則的 Pod——這種情況會在「Pod 已被放到節點之後才加上反親和性規則」時發生。

RemovePodsViolatingNodeAffinity

逐出違反節點親和性規則的 Pod。

無論採用哪種策略,descheduler 都避免逐出以下 Pod:

  • 標記了 scheduler.alpha.kubernetes.io/critical-pod 註解的關鍵 Pod。
  • 不由 ReplicaSet、Deployment 或 Job 管理的 Pod。
  • 由 DaemonSet 管理的 Pod。
  • 具有本地儲存的 Pod。
  • 設有 PodDisruptionBudget 且逐出會違反其規則的 Pod。
  • descheduler Pod 自己(透過把自己標記為關鍵 Pod 達成)。

所有逐出都尊重 Pod 的 QoS 等級,依序挑選 Best-Effort、Burstable,最後才是 Guaranteed 作為逐出候選。QoS 等級的詳細說明見「可預期的需求」。

討論#

配置是一個你該盡量少介入的領域。只要遵循「可預期的需求」的指引、宣告好容器的所有資源需求,排程器就會做好本分,把 Pod 放到最合適的節點上。當這樣仍不夠時,才有多種手段可以把排程器導向你要的部署拓撲。

由簡入繁,控制 Pod 排程的做法如下(撰寫本書時,這份清單幾乎每隔一個 Kubernetes 版本就會變動):

  • nodeName:把 Pod 硬綁到節點最單純的形式。這個欄位理想上應由排程器依政策填入,而非手動指派——手動指派會大幅限制 Pod 能被排到哪,等於把我們打回 Kubernetes 之前那個「明確指定應用跑在哪台機器」的年代。
  • nodeSelector:指定一組鍵值對;Pod 要有資格跑在某節點上,該節點就必須帶有這些鍵值對作為標籤。只要你在 Pod 與節點上放了有意義的標籤(本來就該這麼做),節點選擇器就是控制排程器選擇最簡單、也可接受的機制之一。
  • 調整預設排程:預設排程器已把新 Pod 放得相當不錯,但必要時仍可改動它的過濾與優先政策清單、順序與權重。
  • Pod 親和性與反親和性:讓 Pod 表達對其他 Pod 的依賴,例如基於應用的延遲需求、高可用性、安全約束等。
  • 節點親和性:讓 Pod 表達對節點的依賴,例如考量節點的硬體、位置等。
  • 污點與容忍:讓節點控制哪些 Pod 該或不該被排到自己身上,例如為一組 Pod 保留專用節點,甚至在執行期逐出 Pod。另一個優點是:當你為叢集加入帶有新標籤的新節點時,不必為所有 Pod 加新標籤,只需在「該被放到新節點上」的 Pod 上加。
  • 自訂排程器:若上述都不夠好、或你有複雜的排程需求,可以自己寫一個。自訂排程器可以取代標準排程器,也可以與它並存。混合做法是寫一個「排程器擴充器(scheduler extender)」行程,讓標準排程器在做決策的最後一關呼叫它——這樣你不必實作完整排程器,只需提供過濾與排序節點的 HTTP API。自寫排程器的好處是:在指派 Pod 時可以納入 Kubernetes 叢集之外的因素,例如硬體成本、網路延遲與更好的利用率。你也可以讓多個自訂排程器與預設排程器並存,並為每個 Pod 設定要用哪一個——每個排程器可以擁有專屬於某子集 Pod 的政策。

本章的關鍵要點:為容器的資源輪廓訂好尺寸並宣告它、為 Pod 與節點打上對應的標籤,然後對 Kubernetes 排程器只做最低限度的介入。

更多資訊#

  • Automated Placement Example
  • Assigning Pods to Nodes
  • Node Placement and Scheduling Explained
  • Pod Disruption Budget
  • Guaranteed Scheduling for Critical Add-On Pods
  • The Kubernetes Scheduler
  • Scheduler Algorithm
  • Configuring Multiple Schedulers
  • Descheduler for Kubernetes
  • Keep Your Kubernetes Cluster Balanced: The Secret to High Availability
  • Everything You Ever Wanted to Know About Resource Scheduling, but Were Afraid to Ask