- Kubernetes權威指南:從Docker到Kubernetes實踐全接觸(第4版)
- 龔正等編著
- 12335字
- 2019-09-23 11:04:38
3.9 玩轉Pod調度
在Kubernetes平臺上,我們很少會直接創建一個Pod,在大多數情況下會通過RC、Deployment、DaemonSet、Job等控制器完成對一組Pod副本的創建、調度及全生命周期的自動控制任務。
在最早的Kubernetes版本里是沒有這么多Pod副本控制器的,只有一個Pod副本控制器RC(Replication Controller),這個控制器是這樣設計實現的:RC獨立于所控制的Pod,并通過Label標簽這個松耦合關聯關系控制目標Pod實例的創建和銷毀,隨著Kubernetes的發展,RC也出現了新的繼任者——Deployment,用于更加自動地完成Pod副本的部署、版本更新、回滾等功能。
嚴謹地說,RC的繼任者其實并不是Deployment,而是ReplicaSet,因為ReplicaSet進一步增強了RC標簽選擇器的靈活性。之前RC的標簽選擇器只能選擇一個標簽,而ReplicaSet擁有集合式的標簽選擇器,可以選擇多個Pod標簽,如下所示:
selector: matchLabels: tier: frontend matchExpressions: - {key: tier, operator: In, values: [frontend]}
與RC不同,ReplicaSet被設計成能控制多個不同標簽的Pod副本。一種常見的應用場景是,應用MyApp目前發布了v1與v2兩個版本,用戶希望MyApp的Pod副本數保持為3個,可以同時包含v1和v2版本的Pod,就可以用ReplicaSet來實現這種控制,寫法如下:
selector: matchLabels: version: v2 matchExpressions: - {key: version, operator: In, values: [v1,v2]}
其實,Kubernetes的滾動升級就是巧妙運用ReplicaSet的這個特性來實現的,同時,Deployment也是通過ReplicaSet來實現Pod副本自動控制功能的。我們不應該直接使用底層的ReplicaSet來控制Pod副本,而應該使用管理ReplicaSet的Deployment對象來控制副本,這是來自官方的建議。
在大多數情況下,我們希望Deployment創建的Pod副本被成功調度到集群中的任何一個可用節點,而不關心具體會調度到哪個節點。但是,在真實的生產環境中的確也存在一種需求:希望某種Pod的副本全部在指定的一個或者一些節點上運行,比如希望將MySQL數據庫調度到一個具有SSD磁盤的目標節點上,此時Pod模板中的NodeSelector屬性就開始發揮作用了,上述MySQL定向調度案例的實現方式可分為以下兩步。
(1)把具有SSD磁盤的Node都打上自定義標簽“disk=ssd”。
(2)在Pod模板中設定NodeSelector的值為“disk: ssd”。
如此一來,Kubernetes在調度Pod副本的時候,就會先按照Node的標簽過濾出合適的目標節點,然后選擇一個最佳節點進行調度。
上述邏輯看起來既簡單又完美,但在真實的生產環境中可能面臨以下令人尷尬的問題。
(1)如果NodeSelector選擇的Label不存在或者不符合條件,比如這些目標節點此時宕機或者資源不足,該怎么辦?
(2)如果要選擇多種合適的目標節點,比如SSD磁盤的節點或者超高速硬盤的節點,該怎么辦?Kubernates引入了NodeAffinity(節點親和性設置)來解決該需求。
在真實的生產環境中還存在如下所述的特殊需求。
(1)不同Pod之間的親和性(Affinity)。比如MySQL數據庫與Redis中間件不能被調度到同一個目標節點上,或者兩種不同的Pod必須被調度到同一個Node上,以實現本地文件共享或本地網絡通信等特殊需求,這就是PodAffinity要解決的問題。
(2)有狀態集群的調度。對于ZooKeeper、Elasticsearch、MongoDB、Kafka等有狀態集群,雖然集群中的每個Worker節點看起來都是相同的,但每個Worker節點都必須有明確的、不變的唯一ID(主機名或IP地址),這些節點的啟動和停止次序通常有嚴格的順序。此外,由于集群需要持久化保存狀態數據,所以集群中的Worker節點對應的Pod不管在哪個Node上恢復,都需要掛載原來的Volume,因此這些Pod還需要捆綁具體的PV。針對這種復雜的需求,Kubernetes提供了StatefulSet這種特殊的副本控制器來解決問題,在Kubernetes 1.9版本發布后,StatefulSet才可用于正式生產環境中。
(3)在每個Node上調度并且僅僅創建一個Pod副本。這種調度通常用于系統監控相關的Pod,比如主機上的日志采集、主機性能采集等進程需要被部署到集群中的每個節點,并且只能部署一個副本,這就是DaemonSet這種特殊Pod副本控制器所解決的問題。
(4)對于批處理作業,需要創建多個Pod副本來協同工作,當這些Pod副本都完成自己的任務時,整個批處理作業就結束了。這種Pod運行且僅運行一次的特殊調度,用常規的RC或者Deployment都無法解決,所以Kubernates引入了新的Pod調度控制器Job來解決問題,并繼續延伸了定時作業的調度控制器CronJob。
與單獨的Pod實例不同,由RC、ReplicaSet、Deployment、DaemonSet等控制器創建的Pod副本實例都是歸屬于這些控制器的,這就產生了一個問題:控制器被刪除后,歸屬于控制器的Pod副本該何去何從?在Kubernates 1.9之前,在RC等對象被刪除后,它們所創建的Pod副本都不會被刪除;在Kubernates 1.9以后,這些Pod副本會被一并刪除。如果不希望這樣做,則可以通過kubectl命令的--cascade=false參數來取消這一默認特性:
kubectl delete replicaset my-repset --cascade=false
接下來深入理解和實踐這些Pod調度控制器的各種功能和特性。
3.9.1 Deployment或RC:全自動調度
Deployment或RC的主要功能之一就是自動部署一個容器應用的多份副本,以及持續監控副本的數量,在集群內始終維持用戶指定的副本數量。
下面是一個Deployment配置的例子,使用這個配置文件可以創建一個ReplicaSet,這個ReplicaSet會創建3個Nginx應用的Pod:
nginx-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx-deployment
spec:
replicas: 3
template:
metadata:
labels:
app: nginx
spec:
containers:
- name: nginx
image: nginx:1.7.9
ports:
- containerPort: 80
運行kubectl create命令創建這個Deployment:
# kubectl create -f nginx-deployment.yaml deployment "nginx-deployment" created
查看Deployment的狀態:
# kubectl get deployments NAME DESIRED CURRENT UP-TO-DATE AVAILABLE AGE nginx-deployment 3 3 3 3 18s
該狀態說明Deployment已創建好所有3個副本,并且所有副本都是最新的可用的。
通過運行kubectl get rs和kubectl get pods可以查看已創建的ReplicaSet(RS)和Pod的信息。
# kubectl get rs NAME DESIRED CURRENT READY AGE nginx-deployment-4087004473 3 3 3 53s # kubectl get pods NAME READY STATUS RESTARTS AGE nginx-deployment-4087004473-9jqqs 1/1 Running 0 1m nginx-deployment-4087004473-cq0cf 1/1 Running 0 1m nginx-deployment-4087004473-vxn56 1/1 Running 0 1m
從調度策略上來說,這3個Nginx Pod由系統全自動完成調度。它們各自最終運行在哪個節點上,完全由Master的Scheduler經過一系列算法計算得出,用戶無法干預調度過程和結果。
除了使用系統自動調度算法完成一組Pod的部署,Kubernetes也提供了多種豐富的調度策略,用戶只需在Pod的定義中使用NodeSelector、NodeAffinity、PodAffinity、Pod驅逐等更加細粒度的調度策略設置,就能完成對Pod的精準調度。下面對這些策略進行說明。
3.9.2 NodeSelector:定向調度
Kubernetes Master上的Scheduler服務(kube-scheduler進程)負責實現Pod的調度,整個調度過程通過執行一系列復雜的算法,最終為每個Pod都計算出一個最佳的目標節點,這一過程是自動完成的,通常我們無法知道Pod最終會被調度到哪個節點上。在實際情況下,也可能需要將Pod調度到指定的一些Node上,可以通過Node的標簽(Label)和Pod的nodeSelector屬性相匹配,來達到上述目的。
(1)首先通過kubectl label命令給目標Node打上一些標簽:
kubectl label nodes <node-name> <label-key>=<label-value>
這里,我們為k8s-node-1節點打上一個zone=north標簽,表明它是“北方”的一個節點:
$ kubectl label nodes k8s-node-1 zone=north
NAME LABELS STATUS
k8s-node-1 kubernetes.io/hostname=k8s-node-1,zone=north Ready
上述命令行操作也可以通過修改資源定義文件的方式,并執行kubectl replace -f xxx.yaml命令來完成。
(2)然后,在Pod的定義中加上nodeSelector的設置,以redis-master-controller.yaml為例:
apiVersion: v1 kind: ReplicationController metadata: name: redis-master labels: name: redis-master spec: replicas: 1 selector: name: redis-master template: metadata: labels: name: redis-master spec: containers: - name: master image: kubeguide/redis-master ports: - containerPort: 6379 nodeSelector: zone: north
運行kubectl create -f命令創建Pod,scheduler就會將該Pod調度到擁有zone=north標簽的Node上。
使用kubectl get pods -o wide命令可以驗證Pod所在的Node:
# kubectl get pods -o wide NAME READY STATUS RESTARTS AGE NODE redis-master-f0rqj 1/1 Running 0 19s k8s-node-1
如果我們給多個Node都定義了相同的標簽(例如zone=north),則scheduler會根據調度算法從這組Node中挑選一個可用的Node進行Pod調度。
通過基于Node標簽的調度方式,我們可以把集群中具有不同特點的Node都貼上不同的標簽,例如“role=frontend”“role=backend”“role=database”等標簽,在部署應用時就可以根據應用的需求設置NodeSelector來進行指定Node范圍的調度。
需要注意的是,如果我們指定了Pod的nodeSelector條件,且在集群中不存在包含相應標簽的Node,則即使在集群中還有其他可供使用的Node,這個Pod也無法被成功調度。
除了用戶可以自行給Node添加標簽,Kubernetes也會給Node預定義一些標簽,包括:
◎ kubernetes.io/hostname
◎ beta.kubernetes.io/os(從1.14版本開始更新為穩定版,到1.18版本刪除)
◎ beta.kubernetes.io/arch(從1.14版本開始更新為穩定版,到1.18版本刪除)
◎ kubernetes.io/os(從1.14版本開始啟用)
◎ kubernetes.io/arch(從1.14版本開始啟用)
用戶也可以使用這些系統標簽進行Pod的定向調度。
NodeSelector通過標簽的方式,簡單實現了限制Pod所在節點的方法。親和性調度機制則極大擴展了Pod的調度能力,主要的增強功能如下。
◎ 更具表達力(不僅僅是“符合全部”的簡單情況)。
◎ 可以使用軟限制、優先采用等限制方式,代替之前的硬限制,這樣調度器在無法滿足優先需求的情況下,會退而求其次,繼續運行該Pod。
◎ 可以依據節點上正在運行的其他Pod的標簽來進行限制,而非節點本身的標簽。這樣就可以定義一種規則來描述Pod之間的親和或互斥關系。
親和性調度功能包括節點親和性(NodeAffinity)和Pod親和性(PodAffinity)兩個維度的設置。節點親和性與NodeSelector類似,增強了上述前兩點優勢;Pod的親和與互斥限制則通過Pod標簽而不是節點標簽來實現,也就是上面第4點內容所陳述的方式,同時具有前兩點提到的優點。
NodeSelector將會繼續使用,隨著節點親和性越來越能夠表達nodeSelector的功能,最終NodeSelector會被廢棄。
3.9.3 NodeAffinity:Node親和性調度
NodeAffinity意為Node親和性的調度策略,是用于替換NodeSelector的全新調度策略。目前有兩種節點親和性表達。
◎ RequiredDuringSchedulingIgnoredDuringExecution:必須滿足指定的規則才可以調度Pod到Node上(功能與nodeSelector很像,但是使用的是不同的語法),相當于硬限制。
◎ PreferredDuringSchedulingIgnoredDuringExecution:強調優先滿足指定規則,調度器會嘗試調度Pod到Node上,但并不強求,相當于軟限制。多個優先級規則還可以設置權重(weight)值,以定義執行的先后順序。
IgnoredDuringExecution的意思是:如果一個Pod所在的節點在Pod運行期間標簽發生了變更,不再符合該Pod的節點親和性需求,則系統將忽略Node上Label的變化,該Pod能繼續在該節點運行。
下面的例子設置了NodeAffinity調度的如下規則。
◎ requiredDuringSchedulingIgnoredDuringExecution要求只運行在amd64的節點上(beta.kubernetes.io/arch In amd64)。
◎ preferredDuringSchedulingIgnoredDuringExecution的要求是盡量運行在磁盤類型為ssd(disk-type In ssd)的節點上。
代碼如下:
apiVersion: v1 kind: Pod metadata: name: with-node-affinity spec: affinity: nodeAffinity: requiredDuringSchedulingIgnoredDuringExecution: nodeSelectorTerms: - matchExpressions: - key: beta.kubernetes.io/arch operator: In values: - amd64 preferredDuringSchedulingIgnoredDuringExecution: - weight: 1 preference: matchExpressions: - key: disk-type operator: In values: - ssd containers: - name: with-node-affinity image: gcr.io/google_containers/pause:2.0
從上面的配置中可以看到In操作符,NodeAffinity語法支持的操作符包括In、NotIn、Exists、DoesNotExist、Gt、Lt。雖然沒有節點排斥功能,但是用NotIn和DoesNotExist就可以實現排斥的功能了。
NodeAffinity規則設置的注意事項如下。
◎ 如果同時定義了nodeSelector和nodeAffinity,那么必須兩個條件都得到滿足,Pod才能最終運行在指定的Node上。
◎ 如果nodeAffinity指定了多個nodeSelectorTerms,那么其中一個能夠匹配成功即可。
◎ 如果在nodeSelectorTerms中有多個matchExpressions,則一個節點必須滿足所有matchExpressions才能運行該Pod。
3.9.4 PodAffinity:Pod親和與互斥調度策略
Pod間的親和與互斥從Kubernetes 1.4版本開始引入。這一功能讓用戶從另一個角度來限制Pod所能運行的節點:根據在節點上正在運行的Pod的標簽而不是節點的標簽進行判斷和調度,要求對節點和Pod兩個條件進行匹配。這種規則可以描述為:如果在具有標簽X的Node上運行了一個或者多個符合條件Y的Pod,那么Pod應該(如果是互斥的情況,那么就變成拒絕)運行在這個Node上。
這里X指的是一個集群中的節點、機架、區域等概念,通過Kubernetes內置節點標簽中的key來進行聲明。這個key的名字為topologyKey,意為表達節點所屬的topology范圍。
◎ kubernetes.io/hostname
◎ failure-domain.beta.kubernetes.io/zone
◎ failure-domain.beta.kubernetes.io/region
與節點不同的是,Pod是屬于某個命名空間的,所以條件Y表達的是一個或者全部命名空間中的一個Label Selector。
和節點親和相同,Pod親和與互斥的條件設置也是requiredDuringSchedulingIgnoredDuringExecution和preferredDuringSchedulingIgnoredDuringExecution。Pod的親和性被定義于PodSpec的affinity字段下的podAffinity子字段中。Pod間的互斥性則被定義于同一層次的podAntiAffinity子字段中。
下面通過實例來說明Pod間的親和性和互斥性策略設置。
1.參照目標Pod
首先,創建一個名為pod-flag的Pod,帶有標簽security=S1和app=nginx,后面的例子將使用pod-flag作為Pod親和與互斥的目標Pod:
apiVersion: v1 kind: Pod metadata: name: pod-flag labels: security: "S1" app: "nginx" spec: containers: - name: nginx image: nginx
2.Pod的親和性調度
下面創建第2個Pod來說明Pod的親和性調度,這里定義的親和標簽是security=S1,對應上面的Pod“pod-flag”,topologyKey的值被設置為“kubernetes.io/hostname”:
apiVersion: v1 kind: Pod metadata: name: pod-affinity spec: affinity: podAffinity: requiredDuringSchedulingIgnoredDuringExecution: - labelSelector: matchExpressions: - key: security operator: In values: - S1 topologyKey: kubernetes.io/hostname containers: - name: with-pod-affinity image: gcr.io/google_containers/pause:2.0
創建Pod之后,使用kubectl get pods -o wide命令可以看到,這兩個Pod在同一個Node上運行。
有興趣的讀者還可以測試一下,在創建這個Pod之前,刪掉這個節點的kubernetes.io/hostname標簽,重復上面的創建步驟,將會發現Pod一直處于Pending狀態,這是因為找不到滿足條件的Node了。
3.Pod的互斥性調度
創建第3個Pod,我們希望它不與目標Pod運行在同一個Node上:
apiVersion: v1 kind: Pod metadata: name: anti-affinity spec: affinity: podAffinity: requiredDuringSchedulingIgnoredDuringExecution: - labelSelector: matchExpressions: - key: security operator: In values: - S1 topologyKey: failure-domain.beta.kubernetes.io/zone podAntiAffinity: requiredDuringSchedulingIgnoredDuringExecution: - labelSelector: matchExpressions: - key: app operator: In values: - nginx topologyKey: kubernetes.io/hostname containers: - name: anti-affinity image: gcr.io/google_containers/pause:2.0
這里要求這個新Pod與security=S1的Pod為同一個zone,但是不與app=nginx的Pod為同一個Node。創建Pod之后,同樣用kubectl get pods -o wide來查看,會看到新的Pod被調度到了同一Zone內的不同Node上。
與節點親和性類似,Pod親和性的操作符也包括In、NotIn、Exists、DoesNotExist、Gt、Lt。
原則上,topologyKey可以使用任何合法的標簽Key賦值,但是出于性能和安全方面的考慮,對topologyKey有如下限制。
◎ 在Pod親和性和RequiredDuringScheduling的Pod互斥性的定義中,不允許使用空的topologyKey。
◎ 如果Admission controller包含了LimitPodHardAntiAffinityTopology,那么針對Required DuringScheduling的Pod互斥性定義就被限制為kubernetes.io/hostname,要使用自定義的topologyKey,就要改寫或禁用該控制器。
◎ 在PreferredDuringScheduling類型的Pod互斥性定義中,空的topologyKey會被解釋為kubernetes.io/hostname 、 failure-domain.beta.kubernetes.io/zone及failure-domain.beta.kubernetes.io/region的組合。
◎ 如果不是上述情況,就可以采用任意合法的topologyKey了。
PodAffinity規則設置的注意事項如下。
◎ 除了設置Label Selector和topologyKey,用戶還可以指定Namespace列表來進行限制,同樣,使用Label Selector對Namespace進行選擇。Namespace的定義和Label Selector及topologyKey同級。省略Namespace的設置,表示使用定義了affinity/anti-affinity的Pod所在的Namespace。如果Namespace被設置為空值(""),則表示所有Namespace。
◎ 在所有關聯requiredDuringSchedulingIgnoredDuringExecution的matchExpressions全都滿足之后,系統才能將Pod調度到某個Node上。
關于Pod親和性和互斥性調度的更多信息可以參考其設計文檔,網址為https://github.com/kubernetes/kubernetes/blob/master/docs/design/podaffinity.md。
3.9.5 Taints和Tolerations(污點和容忍)
前面介紹的NodeAffinity節點親和性,是在Pod上定義的一種屬性,使得Pod能夠被調度到某些Node上運行(優先選擇或強制要求)。Taint則正好相反,它讓Node拒絕Pod的運行。
Taint需要和Toleration配合使用,讓Pod避開那些不合適的Node。在Node上設置一個或多個Taint之后,除非Pod明確聲明能夠容忍這些污點,否則無法在這些Node上運行。Toleration是Pod的屬性,讓Pod能夠(注意,只是能夠,而非必須)運行在標注了Taint的Node上。
可以用kubectl taint命令為Node設置Taint信息:
$ kubectl taint nodes node1 key=value:NoSchedule
這個設置為node1加上了一個Taint。該Taint的鍵為key,值為value,Taint的效果是NoSchedule。這意味著除非Pod明確聲明可以容忍這個Taint,否則就不會被調度到node1上。
然后,需要在Pod上聲明Toleration。下面的兩個Toleration都被設置為可以容忍(Tolerate)具有該Taint的Node,使得Pod能夠被調度到node1上:
tolerations: - key: "key" operator: "Equal" value: "value" effect: "NoSchedule"
或
tolerations: - key: "key" operator: "Exists" effect: "NoSchedule"
Pod的Toleration聲明中的key和effect需要與Taint的設置保持一致,并且滿足以下條件之一。
◎ operator的值是Exists(無須指定value)。
◎ operator的值是Equal并且value相等。
如果不指定operator,則默認值為Equal。
另外,有如下兩個特例。
◎ 空的key配合Exists操作符能夠匹配所有的鍵和值。
◎ 空的effect匹配所有的effect。
在上面的例子中,effect的取值為NoSchedule,還可以取值為PreferNoSchedule,這個值的意思是優先,也可以算作NoSchedule的軟限制版本——一個Pod如果沒有聲明容忍這個Taint,則系統會盡量避免把這個Pod調度到這一節點上,但不是強制的。后面還會介紹另一個effect“NoExecute”。
系統允許在同一個Node上設置多個Taint,也可以在Pod上設置多個Toleration。Kubernetes調度器處理多個Taint和Toleration的邏輯順序為:首先列出節點中所有的Taint,然后忽略Pod的Toleration能夠匹配的部分,剩下的沒有忽略的Taint就是對Pod的效果了。下面是幾種特殊情況。
◎ 如果在剩余的Taint中存在effect=NoSchedule,則調度器不會把該Pod調度到這一節點上。
◎ 如果在剩余的Taint中沒有NoSchedule效果,但是有PreferNoSchedule效果,則調度器會嘗試不把這個Pod指派給這個節點。
◎ 如果在剩余的Taint中有NoExecute效果,并且這個Pod已經在該節點上運行,則會被驅逐;如果沒有在該節點上運行,則也不會再被調度到該節點上。
例如,我們這樣對一個節點進行Taint設置:
$ kubectl taint nodes node1 key1=value1:NoSchedule $ kubectl taint nodes node1 key1=value1:NoExecute $ kubectl taint nodes node1 key2=value2:NoSchedule
然后在Pod上設置兩個Toleration:
tolerations: - key: "key1" operator: "Equal" value: "value1" effect: "NoSchedule" - key: "key1" operator: "Equal" value: "value1" effect: "NoExecute"
這樣的結果是該Pod無法被調度到node1上,這是因為第3個Taint沒有匹配的Toleration。但是如果該Pod已經在node1上運行了,那么在運行時設置第3個Taint,它還能繼續在node1上運行,這是因為Pod可以容忍前兩個Taint。
一般來說,如果給Node加上effect=NoExecute的Taint,那么在該Node上正在運行的所有無對應Toleration的Pod都會被立刻驅逐,而具有相應Toleration的Pod永遠不會被驅逐。不過,系統允許給具有NoExecute效果的Toleration加入一個可選的tolerationSeconds字段,這個設置表明Pod可以在Taint添加到Node之后還能在這個Node上運行多久(單位為s):
tolerations: - key: "key1" operator: "Equal" value: "value1" effect: "NoExecute" tolerationSeconds: 3600
上述定義的意思是,如果Pod正在運行,所在節點都被加入一個匹配的Taint,則這個Pod會持續在這個節點上存活3600s后被逐出。如果在這個寬限期內Taint被移除,則不會觸發驅逐事件。
Taint和Toleration是一種處理節點并且讓Pod進行規避或者驅逐Pod的彈性處理方式,下面列舉一些常見的用例。
1.獨占節點
如果想要拿出一部分節點專門給一些特定應用使用,則可以為節點添加這樣的Taint:
$ kubectl taint nodes nodename dedicated=groupName:NoSchedule
然后給這些應用的Pod加入對應的Toleration。這樣,帶有合適Toleration的Pod就會被允許同使用其他節點一樣使用有Taint的節點。
通過自定義Admission Controller也可以實現這一目標。如果希望讓這些應用獨占一批節點,并且確保它們只能使用這些節點,則還可以給這些Taint節點加入類似的標簽dedicated=groupName,然后Admission Controller需要加入節點親和性設置,要求Pod只會被調度到具有這一標簽的節點上。
2.具有特殊硬件設備的節點
在集群里可能有一小部分節點安裝了特殊的硬件設備(如GPU芯片),用戶自然會希望把不需要占用這類硬件的Pod排除在外,以確保對這類硬件有需求的Pod能夠被順利調度到這些節點。
可以用下面的命令為節點設置Taint:
$ kubectl taint nodes nodename special=true:NoSchedule $ kubectl taint nodes nodename special=true:PreferNoSchedule
然后在Pod中利用對應的Toleration來保障特定的Pod能夠使用特定的硬件。
和上面的獨占節點的示例類似,使用Admission Controller來完成這一任務會更方便。例如,Admission Controller使用Pod的一些特征來判斷這些Pod,如果可以使用這些硬件,就添加Toleration來完成這一工作。要保障需要使用特殊硬件的Pod只被調度到安裝這些硬件的節點上,則還需要一些額外的工作,比如將這些特殊資源使用opaque-int-resource的方式對自定義資源進行量化,然后在PodSpec中進行請求;也可以使用標簽的方式來標注這些安裝有特別硬件的節點,然后在Pod中定義節點親和性來實現這個目標。
3.定義Pod驅逐行為,以應對節點故障(為Alpha版本的功能)
前面提到的NoExecute這個Taint效果對節點上正在運行的Pod有以下影響。
◎ 沒有設置Toleration的Pod會被立刻驅逐。
◎ 配置了對應Toleration的Pod,如果沒有為tolerationSeconds賦值,則會一直留在這一節點中。
◎ 配置了對應Toleration的Pod且指定了tolerationSeconds值,則會在指定時間后驅逐。
◎ Kubernetes從1.6版本開始引入一個Alpha版本的功能,即把節點故障標記為Taint(目前只針對node unreachable及node not ready,相應的NodeCondition "Ready"的值分別為Unknown和False)。激活TaintBasedEvictions功能后(在--feature-gates參數中加入TaintBasedEvictions=true),NodeController會自動為Node設置Taint,而在狀態為Ready的Node上,之前設置過的普通驅逐邏輯將會被禁用。注意,在節點故障的情況下,為了保持現存的Pod驅逐的限速(rate-limiting)設置,系統將會以限速的模式逐步給Node設置Taint,這就能避免在一些特定情況下(比如Master暫時失聯)大量的Pod被驅逐。這一功能兼容于tolerationSeconds,允許Pod定義節點故障時持續多久才被逐出。
例如,一個包含很多本地狀態的應用可能需要在網絡發生故障時,還能持續在節點上運行,期望網絡能夠快速恢復,從而避免被從這個節點上驅逐。
Pod的Toleration可以這樣定義:
tolerations: - key: "node.alpha.kubernetes.io/unreachable" operator: "Exists" effect: "NoExecute" tolerationSeconds: 6000
對于Node未就緒狀態,可以把Key設置為node.alpha.kubernetes.io/notReady。
如果沒有為Pod指定node.alpha.kubernetes.io/notReady的Toleration,那么Kubernetes會自動為Pod加入tolerationSeconds=300的node.alpha.kubernetes.io/notReady類型的Toleration。
同樣,如果Pod沒有定義node.alpha.kubernetes.io/unreachable的Toleration,那么系統會自動為其加入tolerationSeconds=300的node.alpha.kubernetes.io/unreachable類型的Toleration。
這些系統自動設置的toleration在Node發現問題時,能夠為Pod確保驅逐前再運行5min。這兩個默認的Toleration由Admission Controller“DefaultTolerationSeconds”自動加入。
3.9.6 Pod Priority Preemption:Pod優先級調度
對于運行各種負載(如Service、Job)的中等規模或者大規模的集群來說,出于各種原因,我們需要盡可能提高集群的資源利用率。而提高資源利用率的常規做法是采用優先級方案,即不同類型的負載對應不同的優先級,同時允許集群中的所有負載所需的資源總量超過集群可提供的資源,在這種情況下,當發生資源不足的情況時,系統可以選擇釋放一些不重要的負載(優先級最低的),保障最重要的負載能夠獲取足夠的資源穩定運行。
在Kubernetes 1.8版本之前,當集群的可用資源不足時,在用戶提交新的Pod創建請求后,該Pod會一直處于Pending狀態,即使這個Pod是一個很重要(很有身份)的Pod,也只能被動等待其他Pod被刪除并釋放資源,才能有機會被調度成功。Kubernetes 1.8版本引入了基于Pod優先級搶占(Pod Priority Preemption)的調度策略,此時Kubernetes會嘗試釋放目標節點上低優先級的Pod,以騰出空間(資源)安置高優先級的Pod,這種調度方式被稱為“搶占式調度”。在Kubernetes 1.11版本中,該特性升級為Beta版本,默認開啟,在后繼的Kubernetes 1.14版本中正式Release。如何聲明一個負載相對其他負載“更重要”?我們可以通過以下幾個維度來定義:
◎ Priority,優先級;
◎ QoS,服務質量等級;
◎ 系統定義的其他度量指標。
優先級搶占調度策略的核心行為分別是驅逐(Eviction)與搶占(Preemption),這兩種行為的使用場景不同,效果相同。Eviction是kubelet進程的行為,即當一個Node發生資源不足(under resource pressure)的情況時,該節點上的kubelet進程會執行驅逐動作,此時Kubelet會綜合考慮Pod的優先級、資源申請量與實際使用量等信息來計算哪些Pod需要被驅逐;當同樣優先級的Pod需要被驅逐時,實際使用的資源量超過申請量最大倍數的高耗能Pod會被首先驅逐。對于QoS等級為“Best Effort”的Pod來說,由于沒有定義資源申請(CPU/Memory Request),所以它們實際使用的資源可能非常大。Preemption則是Scheduler執行的行為,當一個新的Pod因為資源無法滿足而不能被調度時,Scheduler可能(有權決定)選擇驅逐部分低優先級的Pod實例來滿足此Pod的調度目標,這就是Preemption機制。
需要注意的是,Scheduler可能會驅逐Node A上的一個Pod以滿足Node B上的一個新Pod的調度任務。比如下面的這個例子:
一個低優先級的Pod A在Node A(屬于機架R)上運行,此時有一個高優先級的Pod B等待調度,目標節點是同屬機架R的Node B,他們中的一個或全部都定義了anti-affinity規則,不允許在同一個機架上運行,此時Scheduler只好“丟車保帥”,驅逐低優先級的Pod A以滿足高優先級的Pod B的調度。
Pod優先級調度示例如下。
首先,由集群管理員創建PriorityClasses,PriorityClass不屬于任何命名空間:
apiVersion: scheduling.k8s.io/v1beta1
kind: PriorityClass
metadata:
name: high-priority
value: 1000000
globalDefault: false
description: "This priority class should be used for XYZ service pods only."
上述YAML文件定義了一個名為high-priority的優先級類別,優先級為100000,數字越大,優先級越高,超過一億的數字被系統保留,用于指派給系統組件。
我們可以在任意Pod中引用上述Pod優先級類別:
apiVersion: v1
kind: Pod
metadata:
name: nginx
labels:
env: test
spec:
containers:
- name: nginx
image: nginx
imagePullPolicy: IfNotPresent
priorityClassName: high-priority
如果發生了需要搶占的調度,高優先級Pod就可能搶占節點N,并將其低優先級Pod驅逐出節點N,高優先級Pod的status信息中的nominatedNodeName字段會記錄目標節點N的名稱。需要注意,高優先級Pod仍然無法保證最終被調度到節點N上,在節點N上低優先級Pod被驅逐的過程中,如果有新的節點滿足高優先級Pod的需求,就會把它調度到新的Node上。而如果在等待低優先級的Pod退出的過程中,又出現了優先級更高的Pod,調度器將會調度這個更高優先級的Pod到節點N上,并重新調度之前等待的高優先級Pod。
優先級搶占的調度方式可能會導致調度陷入“死循環”狀態。當Kubernetes集群配置了多個調度器(Scheduler)時,這一行為可能就會發生,比如下面這個例子:
Scheduler A為了調度一個(批)Pod,特地驅逐了一些Pod,因此在集群中有了空余的空間可以用來調度,此時Scheduler B恰好搶在Scheduler A之前調度了一個新的Pod,消耗了相應的資源,因此,當Scheduler A清理完資源后正式發起Pod的調度時,卻發現資源不足,被目標節點的kubelet進程拒絕了調度請求!這種情況的確無解,因此最好的做法是讓多個Scheduler相互協作來共同實現一個目標。
最后要指出一點:使用優先級搶占的調度策略可能會導致某些Pod永遠無法被成功調度。因此優先級調度不但增加了系統的復雜性,還可能帶來額外不穩定的因素。因此,一旦發生資源緊張的局面,首先要考慮的是集群擴容,如果無法擴容,則再考慮有監管的優先級調度特性,比如結合基于Namespace的資源配額限制來約束任意優先級搶占行為。
3.9.7 DaemonSet:在每個Node上都調度一個Pod
DaemonSet是Kubernetes 1.2版本新增的一種資源對象,用于管理在集群中每個Node上僅運行一份Pod的副本實例,如圖3.3所示。

圖3.3 DaemonSet示例
這種用法適合有這種需求的應用。
◎ 在每個Node上都運行一個GlusterFS存儲或者Ceph存儲的Daemon進程。
◎ 在每個Node上都運行一個日志采集程序,例如Fluentd或者Logstach。
◎ 在每個Node上都運行一個性能監控程序,采集該Node的運行性能數據,例如Prometheus Node Exporter、collectd、New Relic agent或者Ganglia gmond等。
DaemonSet的Pod調度策略與RC類似,除了使用系統內置的算法在每個Node上進行調度,也可以在Pod的定義中使用NodeSelector或NodeAffinity來指定滿足條件的Node范圍進行調度。
下面的例子定義為在每個Node上都啟動一個fluentd容器,配置文件fluentd-ds.yaml的內容如下,其中掛載了物理機的兩個目錄“/var/log”和“/var/lib/docker/containers”:
apiVersion: apps/v1 kind: DaemonSet metadata: name: fluentd-cloud-logging namespace: kube-system labels: k8s-app: fluentd-cloud-logging spec: template: metadata: namespace: kube-system labels: k8s-app: fluentd-cloud-logging spec: containers: - name: fluentd-cloud-logging image: gcr.io/google_containers/fluentd-elasticsearch:1.17 resources: limits: cpu: 100m memory: 200Mi env: - name: FLUENTD_ARGS value: -q volumeMounts: - name: varlog mountPath: /var/log readOnly: false - name: containers mountPath: /var/lib/docker/containers readOnly: false volumes: - name: containers hostPath: path: /var/lib/docker/containers - name: varlog hostPath: path: /var/log
使用kubectl create命令創建該DaemonSet:
# kubectl create -f fluentd-ds.yaml daemonset "fluentd-cloud-logging" created
查看創建好的DaemonSet和Pod,可以看到在每個Node上都創建了一個Pod:
# kubectl get daemonset --namespace=kube-system NAME DESIRED CURRENT NODE-SELECTOR AGE fluentd-cloud-logging 2 2 <none> 3s # kubectl get pods --namespace=kube-system NAME READY STATUS RESTARTS AGE fluentd-cloud-logging-7tw9z 1/1 Running 0 1h fluentd-cloud-logging-aqdn1 1/1 Running 0 1h
在Kubernetes 1.6以后的版本中,DaemonSet也能執行滾動升級了,即在更新一個DaemonSet模板的時候,舊的Pod副本會被自動刪除,同時新的Pod副本會被自動創建,此時DaemonSet的更新策略(updateStrategy)為RollingUpdate,如下所示:
apiVersion: apps/v1 kind: DaemonSet metadata: name: goldpinger spec: updateStrategy: type: RollingUpdate
updateStrategy的另外一個值是OnDelete,即只有手工刪除了DaemonSet創建的Pod副本,新的Pod副本才會被創建出來。如果不設置updateStrategy的值,則在Kubernetes 1.6之后的版本中會被默認設置為RollingUpdate。
3.9.8 Job:批處理調度
Kubernetes從1.2版本開始支持批處理類型的應用,我們可以通過Kubernetes Job資源對象來定義并啟動一個批處理任務。批處理任務通常并行(或者串行)啟動多個計算進程去處理一批工作項(work item),處理完成后,整個批處理任務結束。按照批處理任務實現方式的不同,批處理任務可以分為如圖3.4所示的幾種模式。

圖3.4 批處理任務的幾種模式
◎ Job Template Expansion模式:一個Job對象對應一個待處理的Work item,有幾個Work item就產生幾個獨立的Job,通常適合Work item數量少、每個Work item要處理的數據量比較大的場景,比如有一個100GB的文件作為一個Work item,總共有10個文件需要處理。
◎ Queue with Pod Per Work Item模式:采用一個任務隊列存放Work item,一個Job對象作為消費者去完成這些Work item,在這種模式下,Job會啟動N個Pod,每個Pod都對應一個Work item。
◎ Queue with Variable Pod Count模式:也是采用一個任務隊列存放Work item,一個Job對象作為消費者去完成這些Work item,但與上面的模式不同,Job啟動的Pod數量是可變的。
還有一種被稱為Single Job with Static Work Assignment的模式,也是一個Job產生多個Pod,但它采用程序靜態方式分配任務項,而不是采用隊列模式進行動態分配。
如表3.4所示是這幾種模式的一個對比。
表3.4 批處理任務的模式對比

考慮到批處理的并行問題,Kubernetes將Job分以下三種類型。
1.Non-parallel Jobs
通常一個Job只啟動一個Pod,除非Pod異常,才會重啟該Pod,一旦此Pod正常結束,Job將結束。
2.Parallel Jobs with a fixed completion count
并行Job會啟動多個Pod,此時需要設定Job的.spec.completions參數為一個正數,當正常結束的Pod數量達至此參數設定的值后,Job結束。此外,Job的.spec.parallelism參數用來控制并行度,即同時啟動幾個Job來處理Work Item。
3.Parallel Jobs with a work queue
任務隊列方式的并行Job需要一個獨立的Queue,Work item都在一個Queue中存放,不能設置Job的.spec.completions參數,此時Job有以下特性。
◎ 每個Pod都能獨立判斷和決定是否還有任務項需要處理。
◎ 如果某個Pod正常結束,則Job不會再啟動新的Pod。
◎ 如果一個Pod成功結束,則此時應該不存在其他Pod還在工作的情況,它們應該都處于即將結束、退出的狀態。
◎ 如果所有Pod都結束了,且至少有一個Pod成功結束,則整個Job成功結束。
下面分別講解常見的三種批處理模型在Kubernetes中的應用例子。
首先是Job Template Expansion模式,由于在這種模式下每個Work item對應一個Job實例,所以這種模式首先定義一個Job模板,模板里的主要參數是Work item的標識,因為每個Job都處理不同的Work item。如下所示的Job模板(文件名為job.yaml.txt)中的$ITEM可以作為任務項的標識:
apiVersion: batch/v1 kind: Job metadata: name: process-item-$ITEM labels: jobgroup: jobexample spec: template: metadata: name: jobexample labels: jobgroup: jobexample spec: containers: - name: c image: busybox command: ["sh", "-c", "echo Processing item $ITEM&& sleep 5"] restartPolicy: Never
通過下面的操作,生成了3個對應的Job定義文件并創建Job:
# for i in apple banana cherry > do > cat job.yaml.txt | sed "s/\$ITEM/$i/" > ./jobs/job-$i.yaml > done # ls jobs job-apple.yaml job-banana.yaml job-cherry.yaml # kubectl create -f jobs job "process-item-apple" created job "process-item-banana" created job "process-item-cherry" created
首先,觀察Job的運行情況:
# kubectl get jobs -l jobgroup=jobexample NAME DESIRED SUCCESSFUL AGE process-item-apple 1 1 4m process-item-banana 1 1 4m process-item-cherry 1 1 4m
其次,我們看看Queue with Pod Per Work Item模式,在這種模式下需要一個任務隊列存放Work item,比如RabbitMQ,客戶端程序先把要處理的任務變成Work item放入任務隊列,然后編寫Worker程序、打包鏡像并定義成為Job中的Work Pod。Worker程序的實現邏輯是從任務隊列中拉取一個Work item并處理,在處理完成后即結束進程。并行度為2的Demo示意圖如圖3.5所示。

圖3.5 Queue with Pod Per Work Item示例
最后,我們看看Queue with Variable Pod Count模式,如圖3.6所示。由于這種模式下,Worker程序需要知道隊列中是否還有等待處理的Work item,如果有就取出來處理,否則就認為所有工作完成并結束進程,所以任務隊列通常要采用Redis或者數據庫來實現。

圖3.6 Queue with Variable Pod Count示例
3.9.9 Cronjob:定時任務
Kubernetes從1.5版本開始增加了一種新類型的Job,即類似Linux Cron的定時任務Cron Job,下面看看如何定義和使用這種類型的Job。
首先,確保Kubernetes的版本為1.8及以上。
其次,需要掌握Cron Job的定時表達式,它基本上照搬了Linux Cron的表達式,區別是第1位是分鐘而不是秒,格式如下:
Minutes Hours DayofMonth Month DayofWeek Year
其中每個域都可出現的字符如下。
◎ Minutes:可出現“,”“-”“*”“/”這4個字符,有效范圍為0~59的整數。
◎ Hours:可出現“,”“-”“*”“/”這4個字符,有效范圍為0~23的整數。
◎ DayofMonth:可出現“,”“-”“*”“/”“?”“L”“W”“C”這8個字符,有效范圍為0~31的整數。
◎ Month:可出現“,”“-”“*”“/”這4個字符,有效范圍為1~12的整數或JAN~DEC。
◎ DayofWeek:可出現“,”“-”“*”“/”“?”“L”“C”“#”這8個字符,有效范圍為1~7的整數或SUN~SAT。1表示星期天,2表示星期一,以此類推。
表達式中的特殊字符“*”與“/”的含義如下。
◎ *:表示匹配該域的任意值,假如在Minutes域使用“*”,則表示每分鐘都會觸發事件。
◎ /:表示從起始時間開始觸發,然后每隔固定時間觸發一次,例如在Minutes域設置為5/20,則意味著第1次觸發在第5min時,接下來每20min觸發一次,將在第25min、第45min等時刻分別觸發。
比如,我們要每隔1min執行一次任務,則Cron表達式如下:
*/1 * * * *
掌握這些基本知識后,就可以編寫一個Cron Job的配置文件了:
cron.yaml apiVersion: batch/v1 kind: CronJob metadata: name: hello spec: schedule: "*/1 * * * *" jobTemplate: spec: template: spec: containers: - name: hello image: busybox args: - /bin/sh - -c - date; echo Hello from the Kubernetes cluster restartPolicy: OnFailure
該例子定義了一個名為hello的Cron Job,任務每隔1min執行一次,運行的鏡像是busybox,執行的命令是Shell腳本,腳本執行時會在控制臺輸出當前時間和字符串“ Hello from the Kubernetes cluster”。
接下來執行kubectl create命令完成創建:
# kubectl create -f cron.yaml cronjob "hello" created
然后每隔1min執行kubectl get cronjob hello查看任務狀態,發現的確每分鐘調度了一次:
# kubectl get cronjob hello NAME SCHEDULE SUSPEND ACTIVE LAST-SCHEDULE hello */1 * * * * False 0 Thu, 29 Jun 2017 11:32:00-0700 ...... # kubectl get cronjob hello NAME SCHEDULE SUSPEND ACTIVE LAST-SCHEDULE hello */1 * * * * False 0 Thu, 29 Jun 2017 11:33:00-0700 ...... # kubectl get cronjob hello NAME SCHEDULE SUSPEND ACTIVE LAST-SCHEDULE hello */1 * * * * False 0 Thu, 29 Jun 2017 11:34:00-0700
還可以通過查找Cron Job對應的容器,驗證每隔1min產生一個容器的事實,如下所示:
# docker ps -a | grep busybox 83f7b86728ea busybox@sha256:be3c11fdba7cfe299214e46edc642e09514dbb9bbefcd0d3836c05a1e0cd0642 "/bin/sh -c 'date; ec" About a minute ago Exited (0) About a minute ago k8s_hello_hello-1498795860-qqwb4_default_207586cf-5d4a-11e7-86c1-000c2997487d_0 36aa3b991980 busybox@sha256:be3c11fdba7cfe299214e46edc642e09514dbb9bbefcd0d3836c05a1e0cd0642 "/bin/sh -c 'date; ec" 2 minutes ago Exited (0) 2 minutes ago k8s_hello_hello-1498795800-g92vx_default_fca21ec0-5d49-11e7-86c1-000c2997487d_0 3d762ae35172 busybox@sha256:be3c11fdba7cfe299214e46edc642e09514dbb9bbefcd0d3836c05a1e0cd0642 "/bin/sh -c 'date; ec" 3 minutes ago Exited (0) 3 minutes ago k8s_hello_hello-1498795740-3qxmd_default_d8c75d07-5d49-11e7-86c1-000c2997487d_0 8ee5eefa8cd3 busybox@sha256:be3c11fdba7cfe299214e46edc642e09514dbb9bbefcd0d3836c05a1e0cd0642 "/bin/sh -c 'date; ec" 4 minutes ago Exited (0) 4 minutes ago k8s_hello_hello-1498795680-mgb7h_default_b4f7aec5-5d49-11e7-86c1-000c2997487d_0
查看任意一個容器的日志,結果如下:
# docker logs 83f7b86728ea Thu Jun 29 18:33:07 UTC 2017 Hello from the Kubernetes cluster
運行下面的命令,可以更直觀地了解Cron Job定期觸發任務執行的歷史和現狀:
# kubectl get jobs --watch NAME DESIRED SUCCESSFUL AGE hello-1498761060 1 1 31m hello-1498761120 1 1 30m hello-1498761180 1 1 29m hello-1498761240 1 1 28m hello-1498761300 1 1 27m hello-1498761360 1 1 26m hello-1498761420 1 1 25m
其中SUCCESSFUL列為1的每一行都是一個調度成功的Job,以第1行的“hello-1498761060”的Job為例,它對應的Pod可以通過下面的方式得到:
# kubectl get pods --show-all | grep hello-1498761060 hello-1498761060-shpwx 0/1 Completed 0 39m
查看該Pod的日志:
# kubectl logs hello-1498761060-shpwx Thu Jun 29 18:31:07 UTC 2017 Hello from the Kubernetes cluster
最后,當不需要某個Cron Job時,可以通過下面的命令刪除它:
# kubectl delete cronjob hello cronjob "hello" deleted
在Kubernetes 1.9版本后,kubectrl命令增加了別名cj來表示cronjob,同時kubectl set image/env命令也可以作用在CronJob對象上了。
3.9.10 自定義調度器
如果Kubernetes調度器的眾多特性還無法滿足我們的獨特調度需求,則還可以用自己開發的調度器進行調度。從1.6版本開始,Kubernetes的多調度器特性也進入了快速發展階段。
一般情況下,每個新Pod都會由默認的調度器進行調度。但是如果在Pod中提供了自定義的調度器名稱,那么默認的調度器會忽略該Pod,轉由指定的調度器完成Pod的調度。
在下面的例子中為Pod指定了一個名為my-scheduler的自定義調度器:
apiVersion: v1
kind: Pod
metadata:
name: nginx
labels:
app: nginx
spec:
schedulerName: my-scheduler
containers:
- name: nginx
image: nginx
如果自定義的調度器還未在系統中部署,則默認的調度器會忽略這個Pod,這個Pod將會永遠處于Pending狀態。
下面看看如何創建一個自定義的調度器。
可以用任何語言來實現簡單或復雜的自定義調度器。下面的簡單例子使用Bash腳本進行實現,調度策略為隨機選擇一個Node(注意,這個調度器需要通過kubectl proxy來運行):
#!/bin/bash SERVER='localhost:8001' while true; do for PODNAME in $(kubectl --server $SERVER get pods -o json | jq '.items[] | select(.spec.schedulerName == "my-scheduler") | select(.spec.nodeName == null) | .metadata.name' | tr -d '"'); do NODES=($(kubectl --server $SERVER get nodes -o json | jq '.items[].metadata.name' | tr -d '"')) NUMNODES=${#NODES[@]} CHOSEN=${NODES[$[ $RANDOM % $NUMNODES ]]} curl --header "Content-Type:application/json" --request POST --data '{"apiVersion":"v1", "kind": "Binding", "metadata": {"name": "'$PODNAME'"}, "target": {"apiVersion": "v1", "kind": "Node", "name":"'$CHOSEN'"}}' http://$SERVER/api/v1/namespaces/default/pods/$PODNAME/binding/ echo "Assigned $PODNAME to $CHOSEN" done sleep 1 done
一旦這個自定義調度器成功啟動,前面的Pod就會被正確調度到某個Node上。