Kubernetes 設(shè)計(jì)模式筆記 —— Automated Placement

Automated Placement 是 Kubernetes 中 scheduler 的核心功能,負(fù)責(zé)將新的 Pod 分配給合適的節(jié)點(diǎn),滿足容器的資源需求,同時(shí)遵守設(shè)定好的調(diào)度策略。

基于微服務(wù)的系統(tǒng)通常會(huì)包含數(shù)十個(gè)甚至數(shù)百個(gè)隔離的進(jìn)程,容器和 Pod 為它們提供了很好的打包和部署機(jī)制,但并沒有解決將眾多的進(jìn)程分配給適當(dāng)?shù)墓?jié)點(diǎn)這項(xiàng)工作。
容器之間存在依賴關(guān)系,有些還需要關(guān)聯(lián)到特定的節(jié)點(diǎn),容器自身也有一定的資源需求。這些都會(huì)隨著時(shí)間發(fā)生變化。同時(shí)集群本身的資源也不是恒定的,它會(huì)執(zhí)行縮容或者擴(kuò)容,其特定時(shí)刻下的容量也取決于已經(jīng)放置的容器數(shù)量。
這些因素都會(huì)左右容器的調(diào)度。

Available Node Resources

首先需要考慮的就是節(jié)點(diǎn)上是否有足夠的可用資源。Scheduler 會(huì)確保 Pod 申請(qǐng)的資源總和小于可分配節(jié)點(diǎn)上的可用容量。節(jié)點(diǎn)可用容量的計(jì)算公式:

Allocatable [capacity for application pods] =
    Node Capacity [available capacity on a node]
        - Kube-Reserved [Kubernetes daemons like kubelet, container runtime]
        - System-Reserved [OS system daemons like sshd, udev]

Container Resource Demands

Pod 在調(diào)度時(shí),另一個(gè)重要的考慮因素就是容器有著自己的運(yùn)行時(shí)依賴和資源需求。
比如:

apiVersion: v1
kind: Pod
metadata:
  name: random-generator
spec:
  containers:
  - image: k8spatterns/random-generator:1.0
    name: random-generator
    resources:
      requests:
        cpu: 100m
        memory: 100Mi
      limits:
        cpu: 200m
        memory: 200Mi

Placement Policies

Scheduler 配置了一組默認(rèn)的優(yōu)先級(jí)策略,適用于絕大多數(shù)場(chǎng)景。這個(gè)策略可以在 scheduler 啟動(dòng)時(shí)被替換掉。

scheduler 策略示例:

{
    "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}
    ]
}

其中 Predicate 規(guī)則用于過(guò)濾掉不合格的節(jié)點(diǎn)。比如 PodFitsHostsPorts 關(guān)注特定的固定主機(jī)端口,只有在這些端口可用時(shí)對(duì)應(yīng)的節(jié)點(diǎn)才會(huì)作為候選。
Priorities 用于根據(jù)一些偏好設(shè)置來(lái)對(duì)候選的節(jié)點(diǎn)進(jìn)行排序。比如 LeastRequestedPriority 會(huì)賦予請(qǐng)求了較少資源的節(jié)點(diǎn)更高的優(yōu)先級(jí)。

可以同時(shí)運(yùn)行多個(gè) scheduler,讓 Pod 自己去指定使用哪一個(gè)。只需要在 Pod 的配置中添加一條 .spec.schedulerName,其值為自定義 scheduler 的名字。

調(diào)度流程

A Pod-to-node assignment

只要 Pod 創(chuàng)建完成且還沒有被分配給任何節(jié)點(diǎn),scheduler 就會(huì)挑選出該 Pod,連同所有可用的節(jié)點(diǎn)及優(yōu)先級(jí)策略。第一階段借助過(guò)濾策略移除所有不滿足要求的節(jié)點(diǎn),剩余的節(jié)點(diǎn)在第二階段有權(quán)重地排序。最后一個(gè)階段得到最終的勝出節(jié)點(diǎn)。

在絕大多數(shù)情況下,最好都只讓 scheduler 去做 Pod-to-Node 的分配工作,不要去嘗試“微操”調(diào)度邏輯。
在某些特殊場(chǎng)景下,如果需要強(qiáng)制某個(gè) Pod 只能分配給特定的一個(gè)或一組節(jié)點(diǎn),可以借助 Pod 的 .spec.nodeSelector 字段。
該字段可以指定一些鍵值對(duì),對(duì)應(yīng)節(jié)點(diǎn)身上的標(biāo)簽。比如想要 Pod 運(yùn)行在擁有 SSD 磁盤的硬件上:

apiVersion: v1
kind: Pod
metadata:
  name: random-generator
spec:
  containers:
  - image: k8spatterns/random-generator:1.0
    name: random-generator
  nodeSelector:
    disktype: ssd

除了通過(guò)自定義標(biāo)簽指定節(jié)點(diǎn),還可以通過(guò)每個(gè)節(jié)點(diǎn)上都有的默認(rèn)標(biāo)簽來(lái)篩選,比如 kubernetes.io/hostname

Node Affinity

Kubernetes 還支持更為靈活的配置調(diào)度流程的方式,比如 node affinity。其實(shí)它相當(dāng)于 nodeSelector 機(jī)制的泛化,其規(guī)則可以被指定為“必需”或者“優(yōu)先”。
“必需”表示相應(yīng)的規(guī)則必須被滿足,否則節(jié)點(diǎn)無(wú)法作為候選;“優(yōu)先”則并不強(qiáng)制,只是提高匹配節(jié)點(diǎn)的權(quán)重。
此外,node affinity 支持多種操作符,如 InNotInExistsDoesNotExistGtLt 等,從而獲得更強(qiáng)的表達(dá)能力。

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:
          - key: metadata.name
            operator: NotIn
            values: [ "master" ]
  containers:
  - image: k8spatterns/random-generator:1.0
    name: random-generator

其中 requiredDuringSchedulingIgnoredDuringExecution 用來(lái)指定節(jié)點(diǎn)必須具備的條件,此規(guī)則不會(huì)在執(zhí)行過(guò)程中重新計(jì)算。結(jié)合后面的 nodeSelectorTerms 配置,篩選出核心數(shù)大于 3 的節(jié)點(diǎn)。
preferredDuringSchedulingIgnoredDuringExecution 用于指定非必須的條件,表現(xiàn)為一個(gè)帶有權(quán)重的 selector 列表。對(duì)于每一個(gè)節(jié)點(diǎn),計(jì)算出所有匹配項(xiàng)的權(quán)重總和,結(jié)果最高的節(jié)點(diǎn)被選中,只要該節(jié)點(diǎn)已經(jīng)滿足了前面的“必需”條件。

PS:matchFields 只支持 InNotIn 操作符,values 指定的列表中也只允許有一個(gè)值。

誠(chéng)然,node affinity 相比于 nodeSelector 功能更為強(qiáng)大。它允許通過(guò)標(biāo)簽或者字段為 Pod 選擇合適的節(jié)點(diǎn),但不能夠用來(lái)表達(dá) Pod 之間的依賴關(guān)系,比如無(wú)法根據(jù)某個(gè)節(jié)點(diǎn)上已經(jīng)部署的 Pod 判斷某個(gè)新 Pod 是否也應(yīng)該部署到該節(jié)點(diǎn)。這類需求可以通過(guò) Pod affinity 實(shí)現(xiàn)。

Pod Affinity

Node affinity 工作在節(jié)點(diǎn)層級(jí)上,Pod affinity 則可以在多個(gè)拓?fù)鋵蛹?jí)上表達(dá)規(guī)則,達(dá)到粒度更細(xì)的控制。

apiVersion: v1
kind: Pod
metadata:
  name: random-generator
spec:
  affinity:
    podAffinity:
      requiredDuringSchedulingIgnoredDuringExecution:
      - labelSelector:
          matchLabels:
            confidential: high
        topologyKey: security-zone
    podAntiAffinity:
      preferredDuringSchedulingIgnoredDuringExecution:
      - weight: 100
        podAffinityTerm:
          labelSelector:
            matchLabels:
              confidential: none
          topologyKey: kubernetes.io/hostname
  containers:
  - image: k8spatterns/random-generator:1.0
    name: random-generator

其中 podAffinity 部分的配置表示,符合條件的節(jié)點(diǎn)上必須有帶有 confidential=high 標(biāo)簽的 Pod 在運(yùn)行,且該節(jié)點(diǎn)有 security-zone 標(biāo)簽。
podAntiAffinity 定義的規(guī)則用于過(guò)濾掉匹配的節(jié)點(diǎn)。結(jié)合其配置,即節(jié)點(diǎn)上有帶 confidential=none 標(biāo)簽的 Pod 在運(yùn)行時(shí),該節(jié)點(diǎn)不會(huì)用來(lái)部署當(dāng)前 Pod。

Taints and Tolerations

Taints 和 Tolerations 是一類更高級(jí)的用于控制調(diào)度策略的特性。簡(jiǎn)單來(lái)說(shuō),node affinity 允許 Pod 根據(jù)規(guī)則選擇合適的節(jié)點(diǎn),taints 和 tolerations 則正相反,它允許節(jié)點(diǎn)自身去控制 Pod 是否應(yīng)該分配給自己。
Taint 是節(jié)點(diǎn)自身的一種屬性,當(dāng)它存在時(shí),會(huì)阻止 Pod 分配給自己,除非該 Pod 擁有針對(duì) taint 的 tolerations。

Taint 可以使用 kubectl 命令添加。如 kubectl taint nodes master noderole.kubernetes.io/master="true":NoSchedule,等效于下面的配置。

Tainted 節(jié)點(diǎn):

apiVersion: v1
kind: Node
metadata:
  name: master
spec:
  taints:
  - effect: NoSchedule
    key: node-role.kubernetes.io/master

擁有此 taint 的節(jié)點(diǎn)不會(huì)被分配任何 Pod,除非有 Pod 指定了對(duì)應(yīng)的 toleration。比如:

apiVersion: v1
kind: Pod
metadata:
  name: random-generator
spec:
  containers:
  - image: k8spatterns/random-generator:1.0
    name: random-generator
  tolerations:
  - key: node-role.kubernetes.io/master
    operator: Exists
    effect: NoSchedule

在生產(chǎn)級(jí)別的集群中,帶有 noderole.kubernetes.io/master 配置的 taint 一般會(huì)指定給 master 節(jié)點(diǎn),阻止 Pod 部署到 master 上。
這里給 Pod 添加的 toleration 會(huì)覆蓋 taint 的 NoSchedule 效果,即無(wú)論如何都允許此 Pod 分配給 master 節(jié)點(diǎn)。

Taint 可以是硬性的,阻止節(jié)點(diǎn)作為候選(effect=NoSchedule),也可以是軟性的,嘗試避免節(jié)點(diǎn)作為候選(effect=PreferNoSchedule),還可以強(qiáng)制移除節(jié)點(diǎn)上已經(jīng)在運(yùn)行的 Pod(effect=NoExecute)。

當(dāng) Pod 已經(jīng)分配給某個(gè)節(jié)點(diǎn),scheduler 的工作就已經(jīng)算完成了,它不會(huì)再對(duì)完成的分配進(jìn)行調(diào)整。除非該 Pod 被刪除或者重建。隨著時(shí)間的推移,這一定會(huì)導(dǎo)致資源的碎片化,集群利用率降低。
另一個(gè)潛在的問題是,Pod 被創(chuàng)建后具體分配給哪一個(gè)節(jié)點(diǎn),依賴于當(dāng)時(shí)集群的狀態(tài)。而集群本身是動(dòng)態(tài)的,節(jié)點(diǎn)的資源配置會(huì)更改,或者有新的節(jié)點(diǎn)加入進(jìn)來(lái),scheduler 并不會(huì)糾正已經(jīng)存在的部署。此外,節(jié)點(diǎn)上的標(biāo)簽也有可能會(huì)變動(dòng),影響到之后的調(diào)度,但之前已經(jīng)完成的調(diào)度依舊保持不變。

以上所有的場(chǎng)景都可以通過(guò) descheduler 去解決。Kubernetes 的 descheduler 是一個(gè)可選的特性,通常作為 Job 執(zhí)行,當(dāng)管理員覺得是時(shí)候通過(guò)重新調(diào)度 Pod 來(lái)整理集群的碎片。
Descheduler 有一些預(yù)先定義的策略,可以被啟用或者禁用:

  • RemoveDuplicates:該策略會(huì)確保 ReplicaSet 或 Deployment 關(guān)聯(lián)的單一 Pod 只運(yùn)行在唯一一個(gè)節(jié)點(diǎn)上。當(dāng)某個(gè)節(jié)點(diǎn)不健康時(shí),controller 會(huì)在其他健康的節(jié)點(diǎn)上啟動(dòng)新的 Pod。此時(shí)若之前不健康的節(jié)點(diǎn)恢復(fù)正常重新加入集群,正在運(yùn)行的 Pod 就會(huì)大于需要的數(shù)量。此策略就可以應(yīng)用于這類的場(chǎng)景。同時(shí) RemoveDuplicates 還可以在策略或集群架構(gòu)發(fā)生變化后,將 Pod 更均勻地分散在更多的節(jié)點(diǎn)上
  • LowNodeUtilization:該策略會(huì)找到使用率低的節(jié)點(diǎn),并將高使用率節(jié)點(diǎn)上的 Pod 移除掉,希望這些移除的 Pod 可以重新分配到未充分利用的節(jié)點(diǎn)上。使用率低指 CPU、內(nèi)存或 Pod 數(shù)量小于 thresholds 配置;使用率高指的是 CPU、內(nèi)存或 Pod 數(shù)量大于 targetThresholds 配置
  • RemovePodsViolatingInterPodAntiAffinity:該策略會(huì)移除違反了 pod antiaffinity 規(guī)則的 Pod。這種情況可能發(fā)生在,添加規(guī)則時(shí)一些不符合規(guī)則的 Pod 就已經(jīng)存在了
  • RemovePodsViolatingNodeAffinity:移除違反了 node affinity 規(guī)則的 Pod

不管使用何種配置的策略,descheduler 會(huì)避免移除如下類型的 Pod:

  • 在 annotation 中標(biāo)記為 scheduler.alpha.kubernetes.io/criticalpod 的關(guān)鍵 Pod
  • 不由 ReplicaSet、Deployment 或 Job 管理的 Pod
  • 由 DaemonSet 管理的 Pod
  • 擁有本地存儲(chǔ)的 Pod
  • 配置了 PodDisruptionBudget 的 Pod,且移除時(shí)會(huì)違反此規(guī)則
  • Descheduler Pod 本身

總結(jié)

容器調(diào)度是一個(gè)我們希望盡可能少干預(yù)的領(lǐng)域。從簡(jiǎn)單到復(fù)雜,以下方法控制著調(diào)度的具體策略:

  • nodeName:最簡(jiǎn)單的分配方式,將 Pod 到節(jié)點(diǎn)的關(guān)系硬編碼到配置中。理想的情況下,此字段應(yīng)該由 scheduler 填充,策略去驅(qū)動(dòng),而不是手動(dòng)指定
  • nodeSelector:鍵值對(duì)映射。符合條件的節(jié)點(diǎn)必須包含此鍵值對(duì)指向的標(biāo)簽。在控制調(diào)度策略的可接受的方式中,最簡(jiǎn)單的一種
  • Default scheduling alteration:必要的情況下,可以修改 default scheduler 的過(guò)濾規(guī)則和優(yōu)先級(jí)策略、順序、權(quán)重等
  • Pod affinity 和 antiaffinity:此機(jī)制允許 Pod 表達(dá)自身對(duì)其他 Pod 的依賴關(guān)系
  • Node affinity:允許 Pod 表達(dá)自身對(duì)節(jié)點(diǎn)的依賴關(guān)系,比如節(jié)點(diǎn)的硬件配置、地理位置等
  • Taints 和 tolerations:允許節(jié)點(diǎn)去控制哪些 Pod 允許哪些不允許分配給自己。比如為一組 Pod 分配一個(gè)專用節(jié)點(diǎn),甚至在運(yùn)行時(shí)移除 Pod
  • Custom scheduler:若上述方案都不能符合需求,還可以編寫自定義的 scheduler。自定義 scheduler 可以替換掉標(biāo)準(zhǔn)的 Kubernetes scheduler,也可以兩者一起運(yùn)行

參考資料

Kubernetes Patterns

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

推薦閱讀更多精彩內(nèi)容