調度,搶占和驅逐
在Kubernetes中,調度 (scheduling) 指的是確保 Pods 匹配到合適的節點,以便 kubelet 能夠運行它們。搶占 (Preemption) 指的是終止低優先級的 Pods 以便高優先級的 Pods 可以調度運行的過程。驅逐 (Eviction) 是在資源匱乏的節點上,主動讓一個或多個 Pods 失效的過程。
1 - Kubernetes 調度器
在 Kubernetes 中,調度是指將 Pod 放置到合適的 Node 上,然后對應 Node 上的 Kubelet 才能夠運行這些 pod。
調度概覽
調度器通過 kubernetes 的監測(Watch)機制來發現集群中新創建且尚未被調度到 Node 上的 Pod。調度器會將發現的每一個未調度的 Pod 調度到一個合適的 Node 上來運行。調度器會依據下文的調度原則來做出調度選擇。
如果你想要理解 Pod 為什么會被調度到特定的 Node 上,或者你想要嘗試實現一個自定義的調度器,這篇文章將幫助你了解調度。
kube-scheduler
kube-scheduler 是 Kubernetes 集群的默認調度器,並且是集群控制面的一部分。如果你真的希望或者有這方面的需求,kube-scheduler 在設計上是允許你自己寫一個調度組件並替換原有的 kube-scheduler。
對每一個新創建的 Pod 或者是未被調度的 Pod,kube-scheduler 會選擇一個最優的 Node 去運行這個 Pod。然而,Pod 內的每一個容器對資源都有不同的需求,而且 Pod 本身也有不同的資源需求。因此,Pod 在被調度到 Node 上之前,根據這些特定的資源調度需求,需要對集群中的 Node 進行一次過濾。
在一個集群中,滿足一個 Pod 調度請求的所有 Node 稱之為可調度節點。如果沒有任何一個 Node 能滿足 Pod 的資源請求,那么這個 Pod 將一直停留在未調度狀態直到調度器能夠找到合適的 Node。
調度器先在集群中找到一個 Pod 的所有可調度節點,然后根據一系列函數對這些可調度節點打分,選出其中得分最高的 Node 來運行 Pod。之后,調度器將這個調度決定通知給 kube-apiserver,這個過程叫做綁定。
在做調度決定時需要考慮的因素包括:單獨和整體的資源請求、硬件/軟件/策略限制、親和以及反親和要求、數據局域性、負載間的干擾等等。
kube-scheduler 調度流程
kube-scheduler 給一個 pod 做調度選擇包含兩個步驟:
- 過濾
- 打分
過濾階段會將所有滿足 Pod 調度需求的 Node 選出來。例如,PodFitsResources 過濾函數會檢查候選 Node 的可用資源能否滿足 Pod 的資源請求。在過濾之后,得出一個 Node 列表,里面包含了所有可調度節點;通常情況下,這個 Node 列表包含不止一個 Node。如果這個列表是空的,代表這個 Pod 不可調度。
在打分階段,調度器會為 Pod 從所有可調度節點中選取一個最合適的 Node。根據當前啟用的打分規則,調度器會給每一個可調度節點進行打分。
最后,kube-scheduler 會將 Pod 調度到得分最高的 Node 上。如果存在多個得分最高的 Node,kube-scheduler 會從中隨機選取一個。
支持以下兩種方式配置調度器的過濾和打分行為:
- 調度策略:允許你配置過濾的斷言(Predicates) 和打分的優先級(Priorities) 。
- 調度配置:允許你配置實現不同調度階段的插件,包括:QueueSort,Filter,Score,Bind,Reserve,Permit 等等。你也可以配置 kube-scheduler 運行不同的配置文件。
2 - 將 Pod 分配給節點
你可以約束一個 Pod 只能在特定的節點上運行。有幾種方法可以實現這點,推薦的方法都是用標簽選擇算符來進行選擇。通常這樣的約束不是必須的,因為調度器將自動進行合理的放置(比如,將 Pod 分散到節點上,而不是將 Pod 放置在可用資源不足的節點上等等)。但在某些情況下,你可能需要進一步控制 Pod 停靠的節點,例如,確保 Pod 最終落在連接了 SSD 的機器上,或者將來自兩個不同的服務且有大量通信的 Pods 被放置在同一個可用區。
nodeSelector
nodeSelector 是節點選擇約束的最簡單推薦形式。nodeSelector 是 PodSpec 的一個字段。它包含鍵值對的映射。為了使 pod 可以在某個節點上運行,該節點的標簽中必須包含這里的每個鍵值對(它也可以具有其他標簽)。最常見的用法的是一對鍵值對。
讓我們來看一個使用 nodeSelector 的例子。
步驟零:先決條件
本示例假設你已基本了解 Kubernetes 的 Pod 並且已經建立一個 Kubernetes 集群。
步驟一:添加標簽到節點
執行 kubectl get nodes 命令獲取集群的節點名稱。選擇一個你要增加標簽的節點,然后執行 kubectl label nodes <node-name> <label-key>=<label-value>
命令將標簽添加到你所選擇的節點上。例如,如果你的節點名稱為 'kubernetes-foo-node-1.c.a-robinson.internal' 並且想要的標簽是 'disktype=ssd',則可以執行 kubectl label nodes kubernetes-foo-node-1.c.a-robinson.internal disktype=ssd
命令。
你可以通過重新運行 kubectl get nodes --show-labels
,查看節點當前具有了所指定的標簽來驗證它是否有效。你也可以使用 kubectl describe node "nodename" 命令查看指定節點的標簽完整列表。
步驟二:添加 nodeSelector 字段到 Pod 配置中
選擇任何一個你想運行的 Pod 的配置文件,並且在其中添加一個 nodeSelector 部分。例如,如果下面是我的 pod 配置:
apiVersion: v1
kind: Pod
metadata:
name: nginx
labels:
env: test
spec:
containers:
- name: nginx
image: nginx
然后像下面這樣添加 nodeSelector:
apiVersion: v1
kind: Pod
metadata:
name: nginx
labels:
env: test
spec:
containers:
- name: nginx
image: nginx
imagePullPolicy: IfNotPresent
nodeSelector:
disktype: ssd
當你之后運行 kubectl apply -f https://k8s.io/examples/pods/pod-nginx.yaml
命令,Pod 將會調度到將標簽添加到的節點上。你可以通過運行 kubectl get pods -o wide
並查看分配給 pod 的 “NODE” 來驗證其是否有效。
插曲:內置的節點標簽
除了你添加的標簽外,節點還預制了一組標准標簽。參見這些常用的標簽,注解以及污點:
- kubernetes.io/hostname
- failure-domain.beta.kubernetes.io/zone
- failure-domain.beta.kubernetes.io/region
- topology.kubernetes.io/zone
- topology.kubernetes.io/region
- beta.kubernetes.io/instance-type
- node.kubernetes.io/instance-type
- kubernetes.io/os
- kubernetes.io/arch
說明:
這些標簽的值是特定於雲供應商的,因此不能保證可靠。例如,kubernetes.io/hostname 的值在某些環境中可能與節點名稱相同,但在其他環境中可能是一個不同的值。
節點隔離/限制
向 Node 對象添加標簽可以將 pod 定位到特定的節點或節點組。這可以用來確保指定的 Pod 只能運行在具有一定隔離性,安全性或監管屬性的節點上。當為此目的使用標簽時,強烈建議選擇節點上的 kubelet 進程無法修改的標簽鍵。這可以防止受感染的節點使用其 kubelet 憑據在自己的 Node 對象上設置這些標簽,並影響調度器將工作負載調度到受感染的節點。
NodeRestriction 准入插件防止 kubelet 使用 node-restriction.kubernetes.io/ 前綴設置或修改標簽。要使用該標簽前綴進行節點隔離:
- 檢查是否在使用 Kubernetes v1.11+,以便 NodeRestriction 功能可用。
- 確保你在使用節點授權並且已經啟用 NodeRestriction 准入插件。
- 將 node-restriction.kubernetes.io/ 前綴下的標簽添加到 Node 對象,然后在節點選擇器中使用這些標簽。例如,example.com.node-restriction.kubernetes.io/fips=true 或 example.com.node-restriction.kubernetes.io/pci-dss=true。
親和性與反親和性
nodeSelector 提供了一種非常簡單的方法來將 Pod 約束到具有特定標簽的節點上。親和性/反親和性功能極大地擴展了你可以表達約束的類型。關鍵的增強點包括:
- 語言更具表現力(不僅僅是“對完全匹配規則的 AND”)
- 你可以發現規則是“軟需求”/“偏好”,而不是硬性要求,因此,如果調度器無法滿足該要求,仍然調度該 Pod
- 你可以使用節點上(或其他拓撲域中)的 Pod 的標簽來約束,而不是使用節點本身的標簽,來允許哪些 pod 可以或者不可以被放置在一起。
親和性功能包含兩種類型的親和性,即“節點親和性”和“Pod 間親和性/反親和性”。節點親和性就像現有的 nodeSelector(但具有上面列出的前兩個好處),然而 Pod 間親和性/反親和性約束 Pod 標簽而不是節點標簽(在上面列出的第三項中描述,除了具有上面列出的第一和第二屬性)。
節點親和性
節點親和性概念上類似於 nodeSelector,它使你可以根據節點上的標簽來約束 Pod 可以調度到哪些節點。
目前有兩種類型的節點親和性,分別為 requiredDuringSchedulingIgnoredDuringExecution 和 preferredDuringSchedulingIgnoredDuringExecution。你可以視它們為“硬需求”和“軟需求”,意思是,前者指定了將 Pod 調度到一個節點上必須滿足的規則(就像 nodeSelector 但使用更具表現力的語法),后者指定調度器將嘗試執行但不能保證的偏好。名稱的“IgnoredDuringExecution”部分意味着,類似於 nodeSelector 的工作原理,如果節點的標簽在運行時發生變更,從而不再滿足 Pod 上的親和性規則,那么 Pod 將仍然繼續在該節點上運行。將來我們計划提供 requiredDuringSchedulingRequiredDuringExecution,它將與 requiredDuringSchedulingIgnoredDuringExecution 完全相同,只是它會將 Pod 從不再滿足 Pod 的節點親和性要求的節點上驅逐。
因此,requiredDuringSchedulingIgnoredDuringExecution 的示例將是 “僅將 Pod 運行在具有 Intel CPU 的節點上”,而 preferredDuringSchedulingIgnoredDuringExecution 的示例為 “嘗試將這組 Pod 運行在 XYZ 故障區域,如果這不可能的話,則允許一些 Pod 在其他地方運行”。
節點親和性通過 PodSpec 的 affinity 字段下的 nodeAffinity 字段進行指定。
下面是一個使用節點親和性的 Pod 的實例:
apiVersion: v1
kind: Pod
metadata:
name: with-node-affinity
spec:
affinity:
nodeAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
nodeSelectorTerms:
- matchExpressions:
- key: kubernetes.io/e2e-az-name
operator: In
values:
- e2e-az1
- e2e-az2
preferredDuringSchedulingIgnoredDuringExecution:
- weight: 1
preference:
matchExpressions:
- key: another-node-label-key
operator: In
values:
- another-node-label-value
containers:
- name: with-node-affinity
image: k8s.gcr.io/pause:2.0
此節點親和性規則表示,Pod 只能放置在具有標簽鍵 kubernetes.io/e2e-az-name 且標簽值為 e2e-az1 或 e2e-az2 的節點上。另外,在滿足這些標准的節點中,具有標簽鍵為 another-node-label-key 且標簽值為 another-node-label-value 的節點應該優先使用。
你可以在上面的例子中看到 In 操作符的使用。新的節點親和性語法支持下面的操作符:In,NotIn,Exists,DoesNotExist,Gt,Lt。你可以使用 NotIn 和 DoesNotExist 來實現節點反親和性行為,或者使用節點污點將 Pod 從特定節點中驅逐。
如果你同時指定了 nodeSelector 和 nodeAffinity,兩者必須都要滿足,才能將 Pod 調度到候選節點上。
如果你指定了多個與 nodeAffinity 類型關聯的 nodeSelectorTerms,則如果其中一個nodeSelectorTerms 滿足的話,pod將可以調度到節點上。
如果你指定了多個與 nodeSelectorTerms 關聯的 matchExpressions,則只有當所有matchExpressions 滿足的話,Pod 才會可以調度到節點上。
如果你修改或刪除了 pod 所調度到的節點的標簽,Pod 不會被刪除。換句話說,親和性選擇只在 Pod 調度期間有效。
preferredDuringSchedulingIgnoredDuringExecution 中的 weight 字段值的范圍是 1-100。對於每個符合所有調度要求(資源請求、RequiredDuringScheduling 親和性表達式等)的節點,調度器將遍歷該字段的元素來計算總和,並且如果節點匹配對應的 MatchExpressions,則添加“權重”到總和。然后將這個評分與該節點的其他優先級函數的評分進行組合。總分最高的節點是最優選的。
逐個調度方案中設置節點親和性
FEATURE STATE: Kubernetes v1.20 [beta]
在配置多個調度方案時,你可以將某個方案與節點親和性關聯起來,如果某個調度方案僅適用於某組特殊的節點時,這樣做是很有用的。要實現這點,可以在調度器配置中為 NodeAffinity 插件添加 addedAffinity 參數。例如:
apiVersion: kubescheduler.config.k8s.io/v1beta1
kind: KubeSchedulerConfiguration
profiles:
- schedulerName: default-scheduler
- schedulerName: foo-scheduler
pluginConfig:
- name: NodeAffinity
args:
addedAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
nodeSelectorTerms:
- matchExpressions:
- key: scheduler-profile
operator: In
values:
- foo
這里的 addedAffinity 除遵從 Pod 規約中設置的節點親和性之外,還適用於將 .spec.schedulerName 設置為 foo-scheduler。
說明: DaemonSet 控制器為 DaemonSet 創建 Pods,但該控制器不理會調度方案。因此,建議你保留一個調度方案,例如 default-scheduler,不要在其中設置 addedAffinity。這樣,DaemonSet 的 Pod 模板將會使用此調度器名稱。否則,DaemonSet 控制器所創建的某些 Pods 可能持續處於不可調度狀態。
pod 間親和性與反親和性
Pod 間親和性與反親和性使你可以基於已經在節點上運行的 Pod 的標簽來約束 Pod 可以調度到的節點,而不是基於節點上的標簽。規則的格式為“如果 X 節點上已經運行了一個或多個滿足規則 Y 的 Pod,則這個 Pod 應該(或者在反親和性的情況下不應該)運行在 X 節點”。Y 表示一個具有可選的關聯命令空間列表的 LabelSelector;與節點不同,因為 Pod 是命名空間限定的(因此 Pod 上的標簽也是命名空間限定的),因此作用於 Pod 標簽的標簽選擇算符必須指定選擇算符應用在哪個命名空間。從概念上講,X 是一個拓撲域,如節點、機架、雲供應商可用區、雲供應商地理區域等。你可以使用 topologyKey 來表示它,topologyKey 是節點標簽的鍵以便系統用來表示這樣的拓撲域。請參閱上面插曲:內置的節點標簽部分中列出的標簽鍵。
說明:
Pod 間親和性與反親和性需要大量的處理,這可能會顯著減慢大規模集群中的調度。我們不建議在超過數百個節點的集群中使用它們。
說明:
Pod 反親和性需要對節點進行一致的標記,即集群中的每個節點必須具有適當的標簽能夠匹配 topologyKey。如果某些或所有節點缺少指定的 topologyKey 標簽,可能會導致意外行為。
與節點親和性一樣,當前有兩種類型的 Pod 親和性與反親和性,即 requiredDuringSchedulingIgnoredDuringExecution 和 preferredDuringSchedulingIgnoredDuringExecution,分別表示“硬性”與“軟性”要求。請參閱前面節點親和性部分中的描述。
requiredDuringSchedulingIgnoredDuringExecution 親和性的一個示例是 “將服務 A 和服務 B 的 Pod 放置在同一區域,因為它們之間進行大量交流”,而 preferredDuringSchedulingIgnoredDuringExecution 反親和性的示例將是 “將此服務的 pod 跨區域分布”(硬性要求是說不通的,因為你可能擁有的 Pod 數多於區域數)。
Pod 間親和性通過 PodSpec 中 affinity 字段下的 podAffinity 字段進行指定。而 Pod 間反親和性通過 PodSpec 中 affinity 字段下的 podAntiAffinity 字段進行指定。
Pod 使用 pod 親和性 的示例:
apiVersion: v1
kind: Pod
metadata:
name: with-pod-affinity
spec:
affinity:
podAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
- labelSelector:
matchExpressions:
- key: security
operator: In
values:
- S1
topologyKey: topology.kubernetes.io/zone
podAntiAffinity:
preferredDuringSchedulingIgnoredDuringExecution:
- weight: 100
podAffinityTerm:
labelSelector:
matchExpressions:
- key: security
operator: In
values:
- S2
topologyKey: topology.kubernetes.io/zone
containers:
- name: with-pod-affinity
image: k8s.gcr.io/pause:2.0
在這個 Pod 的親和性配置定義了一條 Pod 親和性規則和一條 Pod 反親和性規則。在此示例中,podAffinity 配置為 requiredDuringSchedulingIgnoredDuringExecution,然而 podAntiAffinity 配置為 preferredDuringSchedulingIgnoredDuringExecution。Pod 親和性規則表示,僅當節點和至少一個已運行且有鍵為“security”且值為“S1”的標簽 的 Pod 處於同一區域時,才可以將該 Pod 調度到節點上。(更確切的說,如果節點 N 具有帶有鍵 topology.kubernetes.io/zone 和某個值 V 的標簽,則 Pod 有資格在節點 N 上運行,以便集群中至少有一個節點具有鍵 topology.kubernetes.io/zone 和值為 V 的節點正在運行具有鍵“security”和值 “S1”的標簽的 pod。)
Pod 反親和性規則表示,如果節點處於 Pod 所在的同一可用區且具有鍵“security”和值“S2”的標簽,則該 pod 不應將其調度到該節點上。(如果 topologyKey 為 topology.kubernetes.io/zone,則意味着當節點和具有鍵 “security”和值“S2”的標簽的 Pod 處於相同的區域,Pod 不能被調度到該節點上。)查閱設計文檔以獲取 Pod 親和性與反親和性的更多樣例,包括 requiredDuringSchedulingIgnoredDuringExecution 和 preferredDuringSchedulingIgnoredDuringExecution 兩種配置。
Pod 親和性與反親和性的合法操作符有 In,NotIn,Exists,DoesNotExist。
原則上,topologyKey 可以是任何合法的標簽鍵。然而,出於性能和安全原因,topologyKey 受到一些限制:
- 對於 Pod 親和性而言,在 requiredDuringSchedulingIgnoredDuringExecution 和 preferredDuringSchedulingIgnoredDuringExecution 中,topologyKey 不允許為空。
- 對於 Pod 反親和性而言,requiredDuringSchedulingIgnoredDuringExecution 和 preferredDuringSchedulingIgnoredDuringExecution 中,topologyKey 都不可以為空。
- 對於 requiredDuringSchedulingIgnoredDuringExecution 要求的 Pod 反親和性,准入控制器 LimitPodHardAntiAffinityTopology 被引入以確保 topologyKey 只能是 kubernetes.io/hostname。如果你希望 topologyKey 也可用於其他定制拓撲邏輯,你可以更改准入控制器或者禁用之。
- 除上述情況外,topologyKey 可以是任何合法的標簽鍵。
除了 labelSelector 和 topologyKey,你也可以指定表示命名空間的 namespaces 隊列,labelSelector 也應該匹配它(這個與 labelSelector 和 topologyKey 的定義位於相同的級別)。如果忽略或者為空,則默認為 Pod 親和性/反親和性的定義所在的命名空間。
所有與 requiredDuringSchedulingIgnoredDuringExecution 親和性與反親和性關聯的 matchExpressions 必須滿足,才能將 pod 調度到節點上。
名字空間選擇算符
FEATURE STATE: Kubernetes v1.22 [beta]
用戶也可以使用 namespaceSelector 選擇匹配的名字空間,namespaceSelector 是對名字空間集合進行標簽查詢的機制。親和性條件會應用到 namespaceSelector 所選擇的名字空間和 namespaces 字段中所列舉的名字空間之上。注意,空的 namespaceSelector({})會匹配所有名字空間,而 null 或者空的 namespaces 列表以及 null 值 namespaceSelector 意味着“當前 Pod 的名字空間”。
此功能特性是 Beta 版本的,默認是被啟用的。你可以通過針對 kube-apiserver 和 kube-scheduler 設置特性門控 PodAffinityNamespaceSelector 來禁用此特性。
更實際的用例
Pod 間親和性與反親和性在與更高級別的集合(例如 ReplicaSets、StatefulSets、Deployments 等)一起使用時,它們可能更加有用。可以輕松配置一組應位於相同定義拓撲(例如,節點)中的工作負載。
始終放置在相同節點上
在三節點集群中,一個 web 應用程序具有內存緩存,例如 redis。我們希望 web 服務器盡可能與緩存放置在同一位置。
下面是一個簡單 redis Deployment 的 YAML 代碼段,它有三個副本和選擇器標簽 app=store。Deployment 配置了 PodAntiAffinity,用來確保調度器不會將副本調度到單個節點上。
apiVersion: apps/v1
kind: Deployment
metadata:
name: redis-cache
spec:
selector:
matchLabels:
app: store
replicas: 3
template:
metadata:
labels:
app: store
spec:
affinity:
podAntiAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
- labelSelector:
matchExpressions:
- key: app
operator: In
values:
- store
topologyKey: "kubernetes.io/hostname"
containers:
- name: redis-server
image: redis:3.2-alpine
下面 webserver Deployment 的 YAML 代碼段中配置了 podAntiAffinity 和 podAffinity。這將通知調度器將它的所有副本與具有 app=store 選擇器標簽的 Pod 放置在一起。這還確保每個 web 服務器副本不會調度到單個節點上。
apiVersion: apps/v1
kind: Deployment
metadata:
name: web-server
spec:
selector:
matchLabels:
app: web-store
replicas: 3
template:
metadata:
labels:
app: web-store
spec:
affinity:
podAntiAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
- labelSelector:
matchExpressions:
- key: app
operator: In
values:
- web-store
topologyKey: "kubernetes.io/hostname"
podAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
- labelSelector:
matchExpressions:
- key: app
operator: In
values:
- store
topologyKey: "kubernetes.io/hostname"
containers:
- name: web-app
image: nginx:1.16-alpine
如果我們創建了上面的兩個 Deployment,我們的三節點集群將如下表所示。
node-1
|
node-2
|
node-3
|
webserver-1
|
webserver-2
|
webserver-3
|
cache-1
|
cache-2
|
cache-3
|
如你所見,web-server 的三個副本都按照預期那樣自動放置在同一位置。
kubectl get pods -o wide
輸出類似於如下內容:
NAME READY STATUS RESTARTS AGE IP NODE
redis-cache-1450370735-6dzlj 1/1 Running 0 8m 10.192.4.2 kube-node-3
redis-cache-1450370735-j2j96 1/1 Running 0 8m 10.192.2.2 kube-node-1
redis-cache-1450370735-z73mh 1/1 Running 0 8m 10.192.3.1 kube-node-2
web-server-1287567482-5d4dz 1/1 Running 0 7m 10.192.2.3 kube-node-1
web-server-1287567482-6f7v5 1/1 Running 0 7m 10.192.4.3 kube-node-3
web-server-1287567482-s330j 1/1 Running 0 7m 10.192.3.2 kube-node-2
永遠不放置在相同節點
上面的例子使用 PodAntiAffinity 規則和 topologyKey: "kubernetes.io/hostname" 來部署 redis 集群以便在同一主機上沒有兩個實例。參閱 ZooKeeper 教程,以獲取配置反親和性來達到高可用性的 StatefulSet 的樣例(使用了相同的技巧)。
nodeName
nodeName 是節點選擇約束的最簡單方法,但是由於其自身限制,通常不使用它。nodeName 是 PodSpec 的一個字段。如果它不為空,調度器將忽略 Pod,並且給定節點上運行的 kubelet 進程嘗試執行該 Pod。因此,如果 nodeName 在 PodSpec 中指定了,則它優先於上面的節點選擇方法。
使用 nodeName 來選擇節點的一些限制:
- 如果指定的節點不存在。
- 如果指定的節點沒有資源來容納 Pod,Pod 將會調度失敗並且其原因將顯示為,比如 OutOfmemory 或 OutOfcpu。
- 雲環境中的節點名稱並非總是可預測或穩定的。
下面的是使用 nodeName 字段的 Pod 配置文件的例子:
apiVersion: v1
kind: Pod
metadata:
name: nginx
spec:
containers:
- name: nginx
image: nginx
nodeName: kube-01
上面的 pod 將運行在 kube-01 節點上。
3 - Pod 開銷
FEATURE STATE: Kubernetes v1.18 [beta]
在節點上運行 Pod 時,Pod 本身占用大量系統資源。這些資源是運行 Pod 內容器所需資源的附加資源。POD 開銷是一個特性,用於計算 Pod 基礎設施在容器請求和限制之上消耗的資源。
Pod 開銷
在 Kubernetes 中,Pod 的開銷是根據與 Pod 的 RuntimeClass 相關聯的開銷在准入時設置的。
如果啟用了 Pod Overhead,在調度 Pod 時,除了考慮容器資源請求的總和外,還要考慮 Pod 開銷。類似地,kubelet 將在確定 Pod cgroups 的大小和執行 Pod 驅逐排序時也會考慮 Pod 開銷。
啟用 Pod 開銷
您需要確保在集群中啟用了 PodOverhead 特性門控(在 1.18 默認是開啟的),以及一個用於定義 overhead 字段的 RuntimeClass。
使用示例
要使用 PodOverhead 特性,需要一個定義 overhead 字段的 RuntimeClass。作為例子,可以在虛擬機和寄宿操作系統中通過一個虛擬化容器運行時來定義 RuntimeClass 如下,其中每個 Pod 大約使用 120MiB:
---
kind: RuntimeClass
apiVersion: node.k8s.io/v1
metadata:
name: kata-fc
handler: kata-fc
overhead:
podFixed:
memory: "120Mi"
cpu: "250m"
通過指定 kata-fc RuntimeClass 處理程序創建的工作負載會將內存和 cpu 開銷計入資源配額計算、節點調度以及 Pod cgroup 分級。
假設我們運行下面給出的工作負載示例 test-pod:
apiVersion: v1
kind: Pod
metadata:
name: test-pod
spec:
runtimeClassName: kata-fc
containers:
- name: busybox-ctr
image: busybox
stdin: true
tty: true
resources:
limits:
cpu: 500m
memory: 100Mi
- name: nginx-ctr
image: nginx
resources:
limits:
cpu: 1500m
memory: 100Mi
在准入階段 RuntimeClass 准入控制器更新工作負載的 PodSpec 以包含 RuntimeClass 中定義的 overhead。如果 PodSpec 中該字段已定義,該 Pod 將會被拒絕。在這個例子中,由於只指定了 RuntimeClass 名稱,所以准入控制器更新了 Pod, 包含了一個 overhead。
在 RuntimeClass 准入控制器之后,可以檢驗一下已更新的 PodSpec:
kubectl get pod test-pod -o jsonpath='{.spec.overhead}'
輸出:
map[cpu:250m memory:120Mi]
如果定義了 ResourceQuata,則容器請求的總量以及 overhead 字段都將計算在內。
當 kube-scheduler 決定在哪一個節點調度運行新的 Pod 時,調度器會兼顧該 Pod 的 overhead 以及該 Pod 的容器請求總量。在這個示例中,調度器將資源請求和開銷相加,然后尋找具備 2.25 CPU 和 320 MiB 內存可用的節點。
一旦 Pod 調度到了某個節點,該節點上的 kubelet 將為該 Pod 新建一個 cgroup。底層容器運行時將在這個 pod 中創建容器。
如果該資源對每一個容器都定義了一個限制(定義了受限的 Guaranteed QoS 或者 Bustrable QoS),kubelet 會為與該資源(CPU 的 cpu.cfs_quota_us 以及內存的 memory.limit_in_bytes)相關的 pod cgroup 設定一個上限。該上限基於容器限制總量與 PodSpec 中定義的 overhead 之和。
對於 CPU,如果 Pod 的 QoS 是 Guaranteed 或者 Burstable,kubelet 會基於容器請求總量與 PodSpec 中定義的 overhead 之和設置 cpu.shares。
請看這個例子,驗證工作負載的容器請求:
kubectl get pod test-pod -o jsonpath='{.spec.containers[*].resources.limits}'
容器請求總計 2000m CPU 和 200MiB 內存:
map[cpu: 500m memory:100Mi] map[cpu:1500m memory:100Mi]
對照從節點觀察到的情況來檢查一下:
kubectl describe node | grep test-pod -B2
該輸出顯示請求了 2250m CPU 以及 320MiB 內存,包含了 PodOverhead 在內:
Namespace Name CPU Requests CPU Limits Memory Requests Memory Limits AGE
--------- ---- ------------ ---------- --------------- ------------- ---
default test-pod 2250m (56%) 2250m (56%) 320Mi (1%) 320Mi (1%) 36m
驗證 Pod cgroup 限制
在工作負載所運行的節點上檢查 Pod 的內存 cgroups。在接下來的例子中,將在該節點上使用具備 CRI 兼容的容器運行時命令行工具 crictl。
首先在特定的節點上確定該 Pod 的標識符:
# 在該 Pod 調度的節點上執行如下命令:
POD_ID="$(sudo crictl pods --name test-pod -q)"
可以依此判斷該 Pod 的 cgroup 路徑:
# 在該 Pod 調度的節點上執行如下命令:
sudo crictl inspectp -o=json $POD_ID | grep cgroupsPath
執行結果的 cgroup 路徑中包含了該 Pod 的 pause 容器。Pod 級別的 cgroup 即上面的一個目錄。
"cgroupsPath": "/kubepods/podd7f4b509-cf94-4951-9417-d1087c92a5b2/7ccf55aee35dd16aca4189c952d83487297f3cd760f1bbf09620e206e7d0c27a"
在這個例子中,該 pod 的 cgroup 路徑是 kubepods/podd7f4b509-cf94-4951-9417-d1087c92a5b2。驗證內存的 Pod 級別 cgroup 設置:
# 在該 Pod 調度的節點上執行這個命令。
# 另外,修改 cgroup 的名稱以匹配為該 pod 分配的 cgroup。
cat /sys/fs/cgroup/memory/kubepods/podd7f4b509-cf94-4951-9417-d1087c92a5b2/memory.limit_in_bytes
和預期的一樣是 320 MiB
335544320
可觀察性
在 kube-state-metrics 中可以通過 kube_pod_overhead 指標來協助確定何時使用 PodOverhead 以及協助觀察以一個既定開銷運行的工作負載的穩定性。該特性在 kube-state-metrics 的 1.9 發行版本中不可用,不過預計將在后續版本中發布。在此之前,用戶需要從源代碼構建 kube-state-metrics。
4 - 污點和容忍度
節點親和性 是 Pod 的一種屬性,它使 Pod 被吸引到一類特定的節點(這可能出於一種偏好,也可能是硬性要求)。污點(Taint)則相反——它使節點能夠排斥一類特定的 Pod。
容忍度(Toleration)是應用於 Pod 上的,允許(但並不要求)Pod 調度到帶有與之匹配的污點的節點上。
污點和容忍度(Toleration)相互配合,可以用來避免 Pod 被分配到不合適的節點上。每個節點上都可以應用一個或多個污點,這表示對於那些不能容忍這些污點的 Pod,是不會被該節點接受的。
概念
您可以使用命令 kubectl taint 給節點增加一個污點。比如,
kubectl taint nodes node1 key1=value1:NoSchedule
給節點 node1 增加一個污點,它的鍵名是 key1,鍵值是 value1,效果是 NoSchedule。這表示只有擁有和這個污點相匹配的容忍度的 Pod 才能夠被分配到 node1 這個節點。
若要移除上述命令所添加的污點,你可以執行:
kubectl taint nodes node1 key1=value1:NoSchedule-
您可以在 PodSpec 中定義 Pod 的容忍度。下面兩個容忍度均與上面例子中使用 kubectl taint 命令創建的污點相匹配,因此如果一個 Pod 擁有其中的任何一個容忍度都能夠被分配到 node1 :
tolerations:
- key: "key1"
operator: "Equal"
value: "value1"
effect: "NoSchedule"
tolerations:
- key: "key1"
operator: "Exists"
effect: "NoSchedule"
這里是一個使用了容忍度的 Pod:
apiVersion: v1
kind: Pod
metadata:
name: nginx
labels:
env: test
spec:
containers:
- name: nginx
image: nginx
imagePullPolicy: IfNotPresent
tolerations:
- key: "example-key"
operator: "Exists"
effect: "NoSchedule"
operator 的默認值是 Equal。
一個容忍度和一個污點相“匹配”是指它們有一樣的鍵名和效果,並且:
- 如果 operator 是 Exists(此時容忍度不能指定 value)
- 如果 operator 是 Equal,則它們的 value 應該相等
說明:
存在兩種特殊情況:如果一個容忍度的 key 為空且 operator 為 Exists,表示這個容忍度與任意的 key 、value 和 effect 都匹配,即這個容忍度能容忍任意 taint。
如果 effect 為空,則可以與所有鍵名 key1 的效果相匹配。
上述例子中 effect 使用的值為 NoSchedule,您也可以使用另外一個值 PreferNoSchedule。這是“優化”或“軟”版本的 NoSchedule —— 系統會盡量避免將 Pod 調度到存在其不能容忍污點的節點上,但這不是強制的。effect 的值還可以設置為 NoExecute,下文會詳細描述這個值。
您可以給一個節點添加多個污點,也可以給一個 Pod 添加多個容忍度設置。Kubernetes 處理多個污點和容忍度的過程就像一個過濾器:從一個節點的所有污點開始遍歷,過濾掉那些 Pod 中存在與之相匹配的容忍度的污點。余下未被過濾的污點的 effect 值決定了 Pod 是否會被分配到該節點,特別是以下情況:
- 如果未被過濾的污點中存在至少一個 effect 值為 NoSchedule 的污點,則 Kubernetes 不會將 Pod 分配到該節點。
- 如果未被過濾的污點中不存在 effect 值為 NoSchedule 的污點,但是存在 effect 值為 PreferNoSchedule 的污點,則 Kubernetes 會嘗試不將 Pod 分配到該節點。
- 如果未被過濾的污點中存在至少一個 effect 值為 NoExecute 的污點,則 Kubernetes 不會將 Pod 分配到該節點(如果 Pod 還未在節點上運行),或者將 Pod 從該節點驅逐(如果 Pod 已經在節點上運行)。
例如,假設您給一個節點添加了如下污點
kubectl taint nodes node1 key1=value1:NoSchedule
kubectl taint nodes node1 key1=value1:NoExecute
kubectl taint nodes node1 key2=value2:NoSchedule
假定有一個 Pod,它有兩個容忍度:
tolerations:
- key: "key1"
operator: "Equal"
value: "value1"
effect: "NoSchedule"
- key: "key1"
operator: "Equal"
value: "value1"
effect: "NoExecute"
在這種情況下,上述 Pod 不會被分配到上述節點,因為其沒有容忍度和第三個污點相匹配。但是如果在給節點添加上述污點之前,該 Pod 已經在上述節點運行,那么它還可以繼續運行在該節點上,因為第三個污點是三個污點中唯一不能被這個 Pod 容忍的。
通常情況下,如果給一個節點添加了一個 effect 值為 NoExecute 的污點,則任何不能忍受這個污點的 Pod 都會馬上被驅逐,任何可以忍受這個污點的 Pod 都不會被驅逐。但是,如果 Pod 存在一個 effect 值為 NoExecute 的容忍度指定了可選屬性 tolerationSeconds 的值,則表示在給節點添加了上述污點之后,Pod 還能繼續在節點上運行的時間。例如:
tolerations:
- key: "key1"
operator: "Equal"
value: "value1"
effect: "NoExecute"
tolerationSeconds: 3600
這表示如果這個 Pod 正在運行,同時一個匹配的污點被添加到其所在的節點,那么 Pod 還將繼續在節點上運行 3600 秒,然后被驅逐。如果在此之前上述污點被刪除了,則 Pod 不會被驅逐。
使用例子
通過污點和容忍度,可以靈活地讓 Pod 避開某些節點或者將 Pod 從某些節點驅逐。下面是幾個使用例子:
- 專用節點:如果您想將某些節點專門分配給特定的一組用戶使用,您可以給這些節點添加一個污點(即,
kubectl taint nodes nodename dedicated=groupName:NoSchedule
),然后給這組用戶的 Pod 添加一個相對應的 toleration(通過編寫一個自定義的准入控制器,很容易就能做到)。擁有上述容忍度的 Pod 就能夠被分配到上述專用節點,同時也能夠被分配到集群中的其它節點。如果您希望這些 Pod 只能被分配到上述專用節點,那么您還需要給這些專用節點另外添加一個和上述污點類似的 label(例如:dedicated=groupName),同時還要在上述准入控制器中給 Pod 增加節點親和性要求上述 Pod 只能被分配到添加了 dedicated=groupName 標簽的節點上。 - 配備了特殊硬件的節點:在部分節點配備了特殊硬件(比如 GPU)的集群中,我們希望不需要這類硬件的 Pod 不要被分配到這些特殊節點,以便為后繼需要這類硬件的 Pod 保留資源。要達到這個目的,可以先給配備了特殊硬件的節點添加 taint (例如 kubectl taint nodes nodename special=true:NoSchedule 或 kubectl taint nodes nodename special=true:PreferNoSchedule),然后給使用了這類特殊硬件的 Pod 添加一個相匹配的 toleration。和專用節點的例子類似,添加這個容忍度的最簡單的方法是使用自定義准入控制器。比如,我們推薦使用擴展資源來表示特殊硬件,給配置了特殊硬件的節點添加污點時包含擴展資源名稱,然后運行一個 ExtendedResourceToleration 准入控制器。此時,因為節點已經被設置污點了,沒有對應容忍度的 Pod 不會被調度到這些節點。但當你創建一個使用了擴展資源的 Pod 時,ExtendedResourceToleration 准入控制器會自動給 Pod 加上正確的容忍度,這樣 Pod 就會被自動調度到這些配置了特殊硬件件的節點上。這樣就能夠確保這些配置了特殊硬件的節點專門用於運行需要使用這些硬件的 Pod,並且您無需手動給這些 Pod 添加容忍度。
- 基於污點的驅逐: 這是在每個 Pod 中配置的在節點出現問題時的驅逐行為,接下來的章節會描述這個特性。
基於污點的驅逐
FEATURE STATE: Kubernetes v1.18 [stable]
前文提到過污點的 effect 值 NoExecute 會影響已經在節點上運行的 Pod
- 如果 Pod 不能忍受 effect 值為 NoExecute 的污點,那么 Pod 將馬上被驅逐。
- 如果 Pod 能夠忍受 effect 值為 NoExecute 的污點,但是在容忍度定義中沒有指定 tolerationSeconds,則 Pod 還會一直在這個節點上運行。
- 如果 Pod 能夠忍受 effect 值為 NoExecute 的污點,而且指定了 tolerationSeconds,則 Pod 還能在這個節點上繼續運行這個指定的時間長度。
當某種條件為真時,節點控制器會自動給節點添加一個污點。當前內置的污點包括:
- node.kubernetes.io/not-ready:節點未准備好。這相當於節點狀態 Ready 的值為 "False"。
- node.kubernetes.io/unreachable:節點控制器訪問不到節點。這相當於節點狀態 Ready 的值為 "Unknown"。
- node.kubernetes.io/memory-pressure:節點存在內存壓力。
- node.kubernetes.io/disk-pressure:節點存在磁盤壓力。
- node.kubernetes.io/pid-pressure: 節點的 PID 壓力。
- node.kubernetes.io/network-unavailable:節點網絡不可用。
- node.kubernetes.io/unschedulable: 節點不可調度。
- node.cloudprovider.kubernetes.io/uninitialized:如果 kubelet 啟動時指定了一個 "外部" 雲平台驅動,它將給當前節點添加一個污點將其標志為不可用。在 cloud-controller-manager 的一個控制器初始化這個節點后,kubelet 將刪除這個污點。
在節點被驅逐時,節點控制器或者 kubelet 會添加帶有 NoExecute 效應的相關污點。如果異常狀態恢復正常,kubelet 或節點控制器能夠移除相關的污點。
說明: 為了保證由於節點問題引起的 Pod 驅逐速率限制行為正常,系統實際上會以限定速率的方式添加污點。在像主控節點與工作節點間通信中斷等場景下,這樣做可以避免 Pod 被大量驅逐。
使用這個功能特性,結合 tolerationSeconds,Pod 就可以指定當節點出現一個或全部上述問題時還將在這個節點上運行多長的時間。
比如,一個使用了很多本地狀態的應用程序在網絡斷開時,仍然希望停留在當前節點上運行一段較長的時間,願意等待網絡恢復以避免被驅逐。在這種情況下,Pod 的容忍度可能是下面這樣的:
tolerations:
- key: "node.kubernetes.io/unreachable"
operator: "Exists"
effect: "NoExecute"
tolerationSeconds: 6000
說明:
Kubernetes 會自動給 Pod 添加一個 key 為 node.kubernetes.io/not-ready 的容忍度並配置 tolerationSeconds=300,除非用戶提供的 Pod 配置中已經已存在了 key 為 node.kubernetes.io/not-ready 的容忍度。同樣,Kubernetes 會給 Pod 添加一個 key 為 node.kubernetes.io/unreachable 的容忍度並配置 tolerationSeconds=300,除非用戶提供的 Pod 配置中已經已存在了 key 為 node.kubernetes.io/unreachable 的容忍度。
這種自動添加的容忍度意味着在其中一種問題被檢測到時 Pod 默認能夠繼續停留在當前節點運行 5 分鍾。
DaemonSet 中的 Pod 被創建時,針對以下污點自動添加的 NoExecute 的容忍度將不會指定 tolerationSeconds:
- node.kubernetes.io/unreachable
- node.kubernetes.io/not-ready
這保證了出現上述問題時 DaemonSet 中的 Pod 永遠不會被驅逐。
基於節點狀態添加污點
控制平面使用節點控制器自動創建與節點狀況對應的帶有 NoSchedule 效應的污點。
調度器在進行調度時檢查污點,而不是檢查節點狀況。這確保節點狀況不會直接影響調度。例如,如果 DiskPressure 節點狀況處於活躍狀態,則控制平面添加 node.kubernetes.io/disk-pressure 污點並且不會調度新的 pod 到受影響的節點。如果 MemoryPressure 節點狀況處於活躍狀態,則控制平面添加 node.kubernetes.io/memory-pressure 污點。
對於新創建的 Pod,可以通過添加相應的 Pod 容忍度來忽略節點狀況。控制平面還在具有除 BestEffort 之外的 QoS 類的 pod 上 添加 node.kubernetes.io/memory-pressure 容忍度。這是因為 Kubernetes 將 Guaranteed 或 Burstable QoS 類中的 Pod(甚至沒有設置內存請求的 Pod)視為能夠應對內存壓力,而新創建的 BestEffort Pod 不會被調度到受影響的節點上。
DaemonSet 控制器自動為所有守護進程添加如下 NoSchedule 容忍度以防 DaemonSet 崩潰:
- node.kubernetes.io/memory-pressure
- node.kubernetes.io/disk-pressure
- node.kubernetes.io/pid-pressure (1.14 或更高版本)
- node.kubernetes.io/unschedulable (1.10 或更高版本)
- node.kubernetes.io/network-unavailable (只適合主機網絡配置)
添加上述容忍度確保了向后兼容,您也可以選擇自由向 DaemonSet 添加容忍度。
5 - Pod 優先級和搶占
FEATURE STATE: Kubernetes v1.14 [stable]
Pod 可以有優先級。優先級表示一個 Pod 相對於其他 Pod 的重要性。如果一個 Pod 無法被調度,調度程序會嘗試搶占(驅逐)較低優先級的 Pod,以使懸決 Pod 可以被調度。
警告:
在一個並非所有用戶都是可信的集群中,惡意用戶可能以最高優先級創建 Pod,導致其他 Pod 被驅逐或者無法被調度。管理員可以使用 ResourceQuota 來阻止用戶創建高優先級的 Pod。
如何使用優先級和搶占
要使用優先級和搶占:
-
新增一個或多個 PriorityClass。
-
創建 Pod,並將其 priorityClassName 設置為新增的 PriorityClass。當然你不需要直接創建 Pod;通常,你將會添加 priorityClassName 到集合對象(如 Deployment) 的 Pod 模板中。
繼續閱讀以獲取有關這些步驟的更多信息。
說明:
Kubernetes 已經提供了 2 個 PriorityClass:system-cluster-critical 和 system-node-critical。這些是常見的類,用於確保始終優先調度關鍵組件。
PriorityClass
PriorityClass 是一個無名稱空間對象,它定義了從優先級類名稱到優先級整數值的映射。名稱在 PriorityClass 對象元數據的 name 字段中指定。值在必填的 value 字段中指定。值越大,優先級越高。PriorityClass 對象的名稱必須是有效的 DNS 子域名,並且它不能以 system- 為前綴。
PriorityClass 對象可以設置任何小於或等於 10 億的 32 位整數值。較大的數字是為通常不應被搶占或驅逐的關鍵的系統 Pod 所保留的。集群管理員應該為這類映射分別創建獨立的 PriorityClass 對象。
PriorityClass 還有兩個可選字段:globalDefault 和 description。globalDefault 字段表示這個 PriorityClass 的值應該用於沒有 priorityClassName 的 Pod。系統中只能存在一個 globalDefault 設置為 true 的 PriorityClass。如果不存在設置了 globalDefault 的 PriorityClass,則沒有 priorityClassName 的 Pod 的優先級為零。
description 字段是一個任意字符串。它用來告訴集群用戶何時應該使用此 PriorityClass。
關於 PodPriority 和現有集群的注意事項
-
如果你升級一個已經存在的但尚未使用此特性的集群,該集群中已經存在的 Pod 的優先級等效於零。
-
添加一個將 globalDefault 設置為 true 的 PriorityClass 不會改變現有 Pod 的優先級。此類 PriorityClass 的值僅用於添加 PriorityClass 后創建的 Pod。
-
如果你刪除了某個 PriorityClass 對象,則使用被刪除的 PriorityClass 名稱的現有 Pod 保持不變,但是你不能再創建使用已刪除的 PriorityClass 名稱的 Pod。
PriorityClass 示例
apiVersion: scheduling.k8s.io/v1
kind: PriorityClass
metadata:
name: high-priority
value: 1000000
globalDefault: false
description: "此優先級類應僅用於 XYZ 服務 Pod。"
非搶占式 PriorityClass
FEATURE STATE: Kubernetes v1.19 [beta]
配置了 PreemptionPolicy: Never 的 Pod 將被放置在調度隊列中較低優先級 Pod 之前,但它們不能搶占其他 Pod。等待調度的非搶占式 Pod 將留在調度隊列中,直到有足夠的可用資源,它才可以被調度。非搶占式 Pod,像其他 Pod 一樣,受調度程序回退的影響。這意味着如果調度程序嘗試這些 Pod 並且無法調度它們,它們將以更低的頻率被重試,從而允許其他優先級較低的 Pod 排在它們之前。
非搶占式 Pod 仍可能被其他高優先級 Pod 搶占。
PreemptionPolicy 默認為 PreemptLowerPriority,這將允許該 PriorityClass 的 Pod 搶占較低優先級的 Pod(現有默認行為也是如此)。如果 PreemptionPolicy 設置為 Never,則該 PriorityClass 中的 Pod 將是非搶占式的。
數據科學工作負載是一個示例用例。用戶可以提交他們希望優先於其他工作負載的作業,但不希望因為搶占運行中的 Pod 而導致現有工作被丟棄。設置為 PreemptionPolicy: Never 的高優先級作業將在其他排隊的 Pod 之前被調度,只要足夠的集群資源“自然地”變得可用。
非搶占式 PriorityClass 示例
apiVersion: scheduling.k8s.io/v1
kind: PriorityClass
metadata:
name: high-priority-nonpreempting
value: 1000000
preemptionPolicy: Never
globalDefault: false
description: "This priority class will not cause other pods to be preempted."
Pod 優先級
在你擁有一個或多個 PriorityClass 對象之后,你可以創建在其規約中指定這些 PriorityClass 名稱之一的 Pod。優先級准入控制器使用 priorityClassName 字段並填充優先級的整數值。如果未找到所指定的優先級類,則拒絕 Pod。
以下 YAML 是 Pod 配置的示例,它使用在前面的示例中創建的 PriorityClass。優先級准入控制器檢查 Pod 規約並將其優先級解析為 1000000。
apiVersion: v1
kind: Pod
metadata:
name: nginx
labels:
env: test
spec:
containers:
- name: nginx
image: nginx
imagePullPolicy: IfNotPresent
priorityClassName: high-priority
Pod 優先級對調度順序的影響
當啟用 Pod 優先級時,調度程序會按優先級對懸決 Pod 進行排序,並且每個懸決的 Pod 會被放置在調度隊列中其他優先級較低的懸決 Pod 之前。因此,如果滿足調度要求,較高優先級的 Pod 可能會比具有較低優先級的 Pod 更早調度。如果無法調度此類 Pod,調度程序將繼續並嘗試調度其他較低優先級的 Pod。
搶占
Pod 被創建后會進入隊列等待調度。調度器從隊列中挑選一個 Pod 並嘗試將它調度到某個節點上。如果沒有找到滿足 Pod 的所指定的所有要求的節點,則觸發對懸決 Pod 的搶占邏輯。讓我們將懸決 Pod 稱為 P。搶占邏輯試圖找到一個節點,在該節點中刪除一個或多個優先級低於 P 的 Pod,則可以將 P 調度到該節點上。如果找到這樣的節點,一個或多個優先級較低的 Pod 會被從節點中驅逐。被驅逐的 Pod 消失后,P 可以被調度到該節點上。
用戶暴露的信息
當 Pod P 搶占節點 N 上的一個或多個 Pod 時,Pod P 狀態的 nominatedNodeName 字段被設置為節點 N 的名稱。該字段幫助調度程序跟蹤為 Pod P 保留的資源,並為用戶提供有關其集群中搶占的信息。
請注意,Pod P 不一定會調度到“被提名的節點(Nominated Node)”。在 Pod 因搶占而犧牲時,它們將獲得體面終止期。如果調度程序正在等待犧牲者 Pod 終止時另一個節點變得可用,則調度程序將使用另一個節點來調度 Pod P。因此,Pod 規約中的 nominatedNodeName 和 nodeName 並不總是相同。此外,如果調度程序搶占節點 N 上的 Pod,但隨后比 Pod P 更高優先級的 Pod 到達,則調度程序可能會將節點 N 分配給新的更高優先級的 Pod。在這種情況下,調度程序會清除 Pod P 的 nominatedNodeName。通過這樣做,調度程序使 Pod P 有資格搶占另一個節點上的 Pod。
搶占的限制
被搶占犧牲者的體面終止
當 Pod 被搶占時,犧牲者會得到他們的體面終止期。它們可以在體面終止期內完成工作並退出。如果它們不這樣做就會被殺死。這個體面終止期在調度程序搶占 Pod 的時間點和待處理的 Pod (P) 可以在節點 (N) 上調度的時間點之間划分出了一個時間跨度。同時,調度器會繼續調度其他待處理的 Pod。當犧牲者退出或被終止時,調度程序會嘗試在待處理隊列中調度 Pod。因此,調度器搶占犧牲者的時間點與 Pod P 被調度的時間點之間通常存在時間間隔。為了最小化這個差距,可以將低優先級 Pod 的體面終止時間設置為零或一個小數字。
支持 PodDisruptionBudget,但不保證
PodDisruptionBudget (PDB) 允許多副本應用程序的所有者限制因自願性質的干擾而同時終止的 Pod 數量。Kubernetes 在搶占 Pod 時支持 PDB,但對 PDB 的支持是基於盡力而為原則的。調度器會嘗試尋找不會因被搶占而違反 PDB 的犧牲者,但如果沒有找到這樣的犧牲者,搶占仍然會發生,並且即使違反了 PDB 約束也會刪除優先級較低的 Pod。
與低優先級 Pod 之間的 Pod 間親和性
只有當這個問題的答案是肯定的時,才考慮在一個節點上執行搶占操作:“如果從此節點上刪除優先級低於懸決 Pod 的所有 Pod,懸決 Pod 是否可以在該節點上調度?”
說明: 搶占並不一定會刪除所有較低優先級的 Pod。如果懸決 Pod 可以通過刪除少於所有較低優先級的 Pod 來調度,那么只有一部分較低優先級的 Pod 會被刪除。即便如此,上述問題的答案必須是肯定的。如果答案是否定的,則不考慮在該節點上執行搶占。
如果懸決 Pod 與節點上的一個或多個較低優先級 Pod 具有 Pod 間親和性,則在沒有這些較低優先級 Pod 的情況下,無法滿足 Pod 間親和性規則。在這種情況下,調度程序不會搶占節點上的任何 Pod。相反,它尋找另一個節點。調度程序可能會找到合適的節點,也可能不會。無法保證懸決 Pod 可以被調度。
我們針對此問題推薦的解決方案是僅針對同等或更高優先級的 Pod 設置 Pod 間親和性。
跨節點搶占
假設正在考慮在一個節點 N 上執行搶占,以便可以在 N 上調度待處理的 Pod P。只有當另一個節點上的 Pod 被搶占時,P 才可能在 N 上變得可行。下面是一個例子:
- 正在考慮將 Pod P 調度到節點 N 上。
- Pod Q 正在與節點 N 位於同一區域的另一個節點上運行。
- Pod P 與 Pod Q 具有 Zone 維度的反親和(topologyKey:topology.kubernetes.io/zone)。
- Pod P 與 Zone 中的其他 Pod 之間沒有其他反親和性設置。
- 為了在節點 N 上調度 Pod P,可以搶占 Pod Q,但調度器不會進行跨節點搶占。因此,Pod P 將被視為在節點 N 上不可調度。
如果將 Pod Q 從所在節點中移除,則不會違反 Pod 間反親和性約束,並且 Pod P 可能會被調度到節點 N 上。
如果有足夠的需求,並且如果我們找到性能合理的算法,我們可能會考慮在未來版本中添加跨節點搶占。
故障排除
Pod 優先級和搶占可能會產生不必要的副作用。以下是一些潛在問題的示例以及處理這些問題的方法。
Pod 被不必要地搶占
搶占在資源壓力較大時從集群中刪除現有 Pod,為更高優先級的懸決 Pod 騰出空間。如果你錯誤地為某些 Pod 設置了高優先級,這些無意的高優先級 Pod 可能會導致集群中出現搶占行為。Pod 優先級是通過設置 Pod 規約中的 priorityClassName 字段來指定的。優先級的整數值然后被解析並填充到 podSpec 的 priority 字段。
為了解決這個問題,你可以將這些 Pod 的 priorityClassName 更改為使用較低優先級的類,或者將該字段留空。默認情況下,空的 priorityClassName 解析為零。
當 Pod 被搶占時,集群會為被搶占的 Pod 記錄事件。只有當集群沒有足夠的資源用於 Pod 時,才會發生搶占。在這種情況下,只有當懸決 Pod(搶占者)的優先級高於受害 Pod 時才會發生搶占。當沒有懸決 Pod,或者懸決 Pod 的優先級等於或低於犧牲者時,不得發生搶占。如果在這種情況下發生搶占,請提出問題。
有 Pod 被搶占,但搶占者並沒有被調度
當 Pod 被搶占時,它們會收到請求的體面終止期,默認為 30 秒。如果受害 Pod 在此期限內沒有終止,它們將被強制終止。一旦所有犧牲者都離開,就可以調度搶占者 Pod。
在搶占者 Pod 等待犧牲者離開的同時,可能某個適合同一個節點的更高優先級的 Pod 被創建。在這種情況下,調度器將調度優先級更高的 Pod 而不是搶占者。
這是預期的行為:具有較高優先級的 Pod 應該取代具有較低優先級的 Pod。
優先級較高的 Pod 在優先級較低的 Pod 之前被搶占
調度程序嘗試查找可以運行懸決 Pod 的節點。如果沒有找到這樣的節點,調度程序會嘗試從任意節點中刪除優先級較低的 Pod,以便為懸決 Pod 騰出空間。如果具有低優先級 Pod 的節點無法運行懸決 Pod,調度器可能會選擇另一個具有更高優先級 Pod 的節點(與其他節點上的 Pod 相比)進行搶占。犧牲者的優先級必須仍然低於搶占者 Pod。
當有多個節點可供執行搶占操作時,調度器會嘗試選擇具有一組優先級最低的 Pod 的節點。但是,如果此類 Pod 具有 PodDisruptionBudget,當它們被搶占時,則會違反 PodDisruptionBudget,那么調度程序可能會選擇另一個具有更高優先級 Pod 的節點。
當存在多個節點搶占且上述場景均不適用時,調度器會選擇優先級最低的節點。
Pod 優先級和服務質量之間的相互作用
Pod 優先級和 QoS 類是兩個正交特征,交互很少,並且對基於 QoS 類設置 Pod 的優先級沒有默認限制。調度器的搶占邏輯在選擇搶占目標時不考慮 QoS。搶占會考慮 Pod 優先級並嘗試選擇一組優先級最低的目標。僅當移除優先級最低的 Pod 不足以讓調度程序調度搶占式 Pod,或者最低優先級的 Pod 受 PodDisruptionBudget 保護時,才會考慮優先級較高的 Pod。
kubelet 使用優先級來確定節點壓力驅逐 Pod 的順序。你可以使用 QoS 類來估計 Pod 最有可能被驅逐的順序。kubelet 根據以下因素對 Pod 進行驅逐排名:
- 對緊俏資源的使用是否超過請求值
- Pod 優先級
- 相對於請求的資源使用量
當某 Pod 的資源用量未超過其請求時,kubelet 節點壓力驅逐不會驅逐該 Pod。如果優先級較低的 Pod 沒有超過其請求,則不會被驅逐。另一個優先級高於其請求的 Pod 可能會被驅逐。
6 - 節點壓力驅逐
節點壓力驅逐是 kubelet 主動終止 Pod 以回收節點上資源的過程。
kubelet 監控集群節點的 CPU、內存、磁盤空間和文件系統的 inode 等資源。當這些資源中的一個或者多個達到特定的消耗水平,kubelet 可以主動地使節點上一個或者多個 Pod 失效,以回收資源防止飢餓。
在節點壓力驅逐期間,kubelet 將所選 Pod 的 PodPhase 設置為 Failed。這將終止 Pod。
節點壓力驅逐不同於 API 發起的驅逐。
kubelet 並不理會你配置的 PodDisruptionBudget 或者是 Pod 的 terminationGracePeriodSeconds。如果你使用了軟驅逐條件,kubelet 會考慮你所配置的 eviction-max-pod-grace-period。如果你使用了硬驅逐條件,它使用 0s 寬限期來終止 Pod。
如果 Pod 是由替換失敗 Pod 的工作負載資源(例如 StatefulSet 或者 Deployment)管理,則控制平面或 kube-controller-manager 會創建新的 Pod 來代替被驅逐的 Pod。
說明:
kubelet 在終止最終用戶 Pod 之前會嘗試回收節點級資源。例如,它會在磁盤資源不足時刪除未使用的容器鏡像。
kubelet 使用各種參數來做出驅逐決定,如下所示:
- 驅逐信號
- 驅逐條件
- 監控間隔
驅逐信號
驅逐信號是特定資源在特定時間點的當前狀態。kubelet 使用驅逐信號,通過將信號與驅逐條件進行比較來做出驅逐決定,驅逐條件是節點上應該可用資源的最小量。
kubelet 使用以下驅逐信號:
驅逐信號
|
描述
|
memory.available
|
memory.available := node.status.capacity[memory] - node.stats.memory.workingSet
|
nodefs.available
|
nodefs.available := node.stats.fs.available
|
nodefs.inodesFree
|
nodefs.inodesFree := node.stats.fs.inodesFree
|
imagefs.available
|
imagefs.available := node.stats.runtime.imagefs.available
|
imagefs.inodesFree
|
imagefs.inodesFree := node.stats.runtime.imagefs.inodesFree
|
pid.available
|
pid.available := node.stats.rlimit.maxpid - node.stats.rlimit.curproc
|
在上表中,描述列顯示了 kubelet 如何獲取信號的值。每個信號支持百分比值或者是字面值。kubelet 計算相對於與信號有關的總量的百分比值。
memory.available 的值來自 cgroupfs,而不是像 free -m 這樣的工具。這很重要,因為 free -m 在容器中不起作用,如果用戶使用節點可分配資源這一功能特性,資源不足的判定是基於 CGroup 層次結構中的用戶 Pod 所處的局部及 CGroup 根節點作出的。這個腳本重現了 kubelet 為計算 memory.available 而執行的相同步驟。kubelet 在其計算中排除了 inactive_file(即非活動 LRU 列表上基於文件來虛擬的內存的字節數),因為它假定在壓力下內存是可回收的。
kubelet 支持以下文件系統分區:
- nodefs:節點的主要文件系統,用於本地磁盤卷、emptyDir、日志存儲等。例如,nodefs 包含 /var/lib/kubelet/。
- imagefs:可選文件系統,供容器運行時存儲容器鏡像和容器可寫層。
kubelet 會自動發現這些文件系統並忽略其他文件系統。kubelet 不支持其他配置。
說明:
一些 kubelet 垃圾收集功能已被棄用,以支持驅逐。有關已棄用功能的列表,請參閱 kubelet 垃圾收集棄用。
驅逐條件
你可以為 kubelet 指定自定義驅逐條件,以便在作出驅逐決定時使用。
驅逐條件的形式為 [eviction-signal][operator][quantity],其中:
- eviction-signal 是要使用的驅逐信號。
- operator 是你想要的關系運算符,比如 <(小於)。
- quantity 是驅逐條件數量,例如 1Gi。quantity 的值必須與 Kubernetes 使用的數量表示相匹配。你可以使用文字值或百分比(%)。
例如,如果一個節點的總內存為 10Gi 並且你希望在可用內存低於 1Gi 時觸發驅逐,則可以將驅逐條件定義為 memory.available<10% 或 memory.available< 1G。你不能同時使用二者。
你可以配置軟和硬驅逐條件。
軟驅逐條件
軟驅逐條件將驅逐條件與管理員所必須指定的寬限期配對。在超過寬限期之前,kubelet 不會驅逐 Pod。如果沒有指定的寬限期,kubelet 會在啟動時返回錯誤。
你可以既指定軟驅逐條件寬限期,又指定 Pod 終止寬限期的上限,給 kubelet 在驅逐期間使用。如果你指定了寬限期的上限並且 Pod 滿足軟驅逐閾條件,則 kubelet 將使用兩個寬限期中的較小者。如果你沒有指定寬限期上限,kubelet 會立即殺死被驅逐的 Pod,不允許其體面終止。
你可以使用以下標志來配置軟驅逐條件:
- eviction-soft:一組驅逐條件,如 memory.available<1.5Gi,如果驅逐條件持續時長超過指定的寬限期,可以觸發 Pod 驅逐。
- eviction-soft-grace-period:一組驅逐寬限期,如 memory.available=1m30s,定義軟驅逐條件在觸發 Pod 驅逐之前必須保持多長時間。
- eviction-max-pod-grace-period:在滿足軟驅逐條件而終止 Pod 時使用的最大允許寬限期(以秒為單位)。
硬驅逐條件
硬驅逐條件沒有寬限期。當達到硬驅逐條件時,kubelet 會立即殺死 pod,而不會正常終止以回收緊缺的資源。
你可以使用 eviction-hard 標志來配置一組硬驅逐條件,例如 memory.available<1Gi。
kubelet 具有以下默認硬驅逐條件:
- memory.available<100Mi
- nodefs.available<10%
- imagefs.available<15%
- nodefs.inodesFree<5%(Linux 節點)
驅逐監測間隔
kubelet 根據其配置的 housekeeping-interval(默認為 10s)評估驅逐條件。
節點條件
kubelet 報告節點狀況以反映節點處於壓力之下,因為滿足硬或軟驅逐條件,與配置的寬限期無關。
kubelet 根據下表將驅逐信號映射為節點狀況:
節點條件
|
驅逐信號
|
描述
|
MemoryPressure
|
memory.available
|
節點上的可用內存已滿足驅逐條件
|
DiskPressure
|
nodefs.available、nodefs.inodesFree、imagefs.available 或 imagefs.inodesFree
|
節點的根文件系統或映像文件系統上的可用磁盤空間和 inode 已滿足驅逐條件
|
PIDPressure
|
pid.available
|
(Linux) 節點上的可用進程標識符已低於驅逐條件
|
kubelet 根據配置的 --node-status-update-frequency 更新節點條件,默認為 10s。
節點條件振盪
在某些情況下,節點在軟驅逐條件上下振盪,而沒有保持定義的寬限期。這會導致報告的節點條件在 true 和 false 之間不斷切換,從而導致錯誤的驅逐決策。
為了防止振盪,你可以使用 eviction-pressure-transition-period 標志,該標志控制 kubelet 在將節點條件轉換為不同狀態之前必須等待的時間。過渡期的默認值為 5m。
回收節點級資源
kubelet 在驅逐最終用戶 Pod 之前會先嘗試回收節點級資源。
當報告 DiskPressure 節點狀況時,kubelet 會根據節點上的文件系統回收節點級資源。
有 imagefs
如果節點有一個專用的 imagefs 文件系統供容器運行時使用,kubelet 會執行以下操作:
- 如果 nodefs 文件系統滿足驅逐條件,kubelet 垃圾收集死亡 Pod 和容器。
- 如果 imagefs 文件系統滿足驅逐條件,kubelet 將刪除所有未使用的鏡像。
沒有 imagefs
如果節點只有一個滿足驅逐條件的 nodefs 文件系統,kubelet 按以下順序釋放磁盤空間:
- 對死亡的 Pod 和容器進行垃圾收集
- 刪除未使用的鏡像
kubelet 驅逐時 Pod 的選擇
如果 kubelet 回收節點級資源的嘗試沒有使驅逐信號低於條件,則 kubelet 開始驅逐最終用戶 Pod。
kubelet 使用以下參數來確定 Pod 驅逐順序:
- Pod 的資源使用是否超過其請求
- Pod 優先級
- Pod 相對於請求的資源使用情況
因此,kubelet 按以下順序排列和驅逐 Pod:
- 首先考慮資源使用量超過其請求的 BestEffort 或 Burstable Pod。這些 Pod 會根據它們的優先級以及它們的資源使用級別超過其請求的程度被逐出。
- 資源使用量少於請求量的 Guaranteed Pod 和 Burstable Pod 根據其優先級被最后驅逐。
說明:
kubelet 不使用 Pod 的 QoS 類來確定驅逐順序。在回收內存等資源時,你可以使用 QoS 類來估計最可能的 Pod 驅逐順序。QoS 不適用於臨時存儲(EphemeralStorage)請求,因此如果節點在 DiskPressure 下,則上述場景將不適用。
僅當 Guaranteed Pod 中所有容器都被指定了請求和限制並且二者相等時,才保證 Pod 不被驅逐。這些 Pod 永遠不會因為另一個 Pod 的資源消耗而被驅逐。如果系統守護進程(例如 kubelet、docker 和 journald)消耗的資源比通過 system-reserved 或 kube-reserved 分配保留的資源多,並且該節點只有 Guaranteed 或 Burstable Pod 使用的資源少於其上剩余的請求,那么 kubelet 必須選擇驅逐這些 Pod 中的一個以保持節點穩定性並減少資源匱乏對其他 Pod 的影響。在這種情況下,它會選擇首先驅逐最低優先級的 Pod。
當 kubelet 因 inode 或 PID 不足而驅逐 pod 時,它使用優先級來確定驅逐順序,因為 inode 和 PID 沒有請求。
kubelet 根據節點是否具有專用的 imagefs 文件系統對 Pod 進行不同的排序:
有 imagefs
如果 nodefs 觸發驅逐,kubelet 會根據 nodefs 使用情況(本地卷 + 所有容器的日志)對 Pod 進行排序。
如果 imagefs 觸發驅逐,kubelet 會根據所有容器的可寫層使用情況對 Pod 進行排序。
沒有 imagefs
如果 nodefs 觸發驅逐,kubelet 會根據磁盤總用量(本地卷 + 日志和所有容器的可寫層)對 Pod 進行排序。
最小驅逐回收
在某些情況下,驅逐 Pod 只會回收少量的緊俏資源。這可能導致 kubelet 反復達到配置的驅逐條件並觸發多次驅逐。
你可以使用 --eviction-minimum-reclaim 標志或 kubelet 配置文件為每個資源配置最小回收量。當 kubelet 注意到某個資源耗盡時,它會繼續回收該資源,直到回收到你所指定的數量為止。
例如,以下配置設置最小回收量:
apiVersion: kubelet.config.k8s.io/v1beta1
kind: KubeletConfiguration
evictionHard:
memory.available: "500Mi"
nodefs.available: "1Gi"
imagefs.available: "100Gi"
evictionMinimumReclaim:
memory.available: "0Mi"
nodefs.available: "500Mi"
imagefs.available: "2Gi"
在這個例子中,如果 nodefs.available 信號滿足驅逐條件,kubelet 會回收資源,直到信號達到 1Gi 的條件,然后繼續回收至少 500Mi 直到信號達到 1.5Gi。
類似地,kubelet 會回收 imagefs 資源,直到 imagefs.available 信號達到 102Gi。
對於所有資源,默認的 eviction-minimum-reclaim 為 0。
節點內存不足行為
如果節點在 kubelet 能夠回收內存之前遇到內存不足(OOM)事件,則節點依賴 oom_killer 來響應。
kubelet 根據 Pod 的服務質量(QoS)為每個容器設置一個 oom_score_adj 值。
服務質量
|
oom_score_adj
|
Guaranteed
|
-997
|
BestEffort
|
1000
|
Burstable
|
min(max(2, 1000 - (1000 * memoryRequestBytes) / machineMemoryCapacityBytes), 999)
|
說明:
kubelet 還將具有 system-node-critical 優先級 的 Pod 中的容器 oom_score_adj 值設為 -997。
如果 kubelet 在節點遇到 OOM 之前無法回收內存,則 oom_killer 根據它在節點上使用的內存百分比計算 oom_score,然后加上 oom_score_adj 得到每個容器有效的 oom_score。然后它會殺死得分最高的容器。
這意味着低 QoS Pod 中相對於其調度請求消耗內存較多的容器,將首先被殺死。
與 Pod 驅逐不同,如果容器被 OOM 殺死,kubelet 可以根據其 RestartPolicy 重新啟動它。
最佳實踐
以下部分描述了驅逐配置的最佳實踐。
可調度的資源和驅逐策略
當你為 kubelet 配置驅逐策略時,你應該確保調度程序不會在 Pod 觸發驅逐時對其進行調度,因為這類 Pod 會立即引起內存壓力。
考慮以下場景:
- 節點內存容量:10Gi
- 操作員希望為系統守護進程(內核、kubelet 等)保留 10% 的內存容量
- 操作員希望驅逐內存利用率為 95% 的Pod,以減少系統 OOM 的概率。
為此,kubelet 啟動設置如下:
--eviction-hard=memory.available<500Mi
--system-reserved=memory=1.5Gi
在此配置中,--system-reserved 標志為系統預留了 1.5Gi 的內存,即總內存的 10% + 驅逐條件量。
如果 Pod 使用的內存超過其請求值或者系統使用的內存超過 1Gi,則節點可以達到驅逐條件,這使得 memory.available 信號低於 500Mi 並觸發條件。
DaemonSet
Pod 優先級是做出驅逐決定的主要因素。如果你不希望 kubelet 驅逐屬於 DaemonSet 的 Pod,請在 Pod 規約中為這些 Pod 提供足夠高的 priorityClass。你還可以使用優先級較低的 priorityClass 或默認配置,僅在有足夠資源時才運行 DaemonSet Pod。
已知問題
以下部分描述了與資源不足處理相關的已知問題。
kubelet 可能不會立即觀察到內存壓力
默認情況下,kubelet 輪詢 cAdvisor 以定期收集內存使用情況統計信息。如果該輪詢時間窗口內內存使用量迅速增加,kubelet 可能無法足夠快地觀察到 MemoryPressure,但是 OOMKiller 仍將被調用。
你可以使用 --kernel-memcg-notification 標志在 kubelet 上啟用 memcg 通知 API,以便在超過條件時立即收到通知。
如果你不是追求極端利用率,而是要采取合理的過量使用措施,則解決此問題的可行方法是使用 --kube-reserved 和 --system-reserved 標志為系統分配內存。
active_file 內存未被視為可用內存
在 Linux 上,內核跟蹤活動 LRU 列表上的基於文件所虛擬的內存字節數作為 active_file 統計信息。kubelet 將 active_file 內存區域視為不可回收。對於大量使用塊設備形式的本地存儲(包括臨時本地存儲)的工作負載,文件和塊數據的內核級緩存意味着許多最近訪問的緩存頁面可能被計為 active_file。如果這些內核塊緩沖區中在活動 LRU 列表上有足夠多,kubelet 很容易將其視為資源用量過量並為節點設置內存壓力污點,從而觸發 Pod 驅逐。
你可以通過為可能執行 I/O 密集型活動的容器設置相同的內存限制和內存請求來應對該行為。你將需要估計或測量該容器的最佳內存限制值。
7 - API 發起的驅逐
API 發起的驅逐是一個先調用 Eviction API 創建驅逐對象,再由該對象體面地中止 Pod 的過程。
你可以通過 kube-apiserver 的客戶端,比如 kubectl drain 這樣的命令,直接調用 Eviction API 發起驅逐。此操作創建一個 Eviction 對象,該對象再驅動 API 服務器終止選定的 Pod。
API 發起的驅逐將遵從你的 PodDisruptionBudgets 和 terminationGracePeriodSeconds 配置。
8 - 擴展資源的資源裝箱
FEATURE STATE: Kubernetes 1.16 [alpha]
使用 RequestedToCapacityRatioResourceAllocation 優先級函數,可以將 kube-scheduler 配置為支持包含擴展資源在內的資源裝箱操作。優先級函數可用於根據自定義需求微調 kube-scheduler 。
使用 RequestedToCapacityRatioResourceAllocation 啟用裝箱
Kubernetes 允許用戶指定資源以及每類資源的權重,以便根據請求數量與可用容量之比率為節點評分。這就使得用戶可以通過使用適當的參數來對擴展資源執行裝箱操作,從而提高了大型集群中稀缺資源的利用率。RequestedToCapacityRatioResourceAllocation 優先級函數的行為可以通過名為 RequestedToCapacityRatioArgs 的配置選項進行控制。該標志由兩個參數 shape 和 resources 組成。shape 允許用戶根據 utilization 和 score 值將函數調整為最少請求(least requested)或最多請求(most requested)計算。resources 包含由 name 和 weight 組成,name 指定評分時要考慮的資源,weight 指定每種資源的權重。
以下是一個配置示例,該配置將 requestedToCapacityRatioArguments 設置為對擴展資源 intel.com/foo 和 intel.com/bar 的裝箱行為
apiVersion: kubescheduler.config.k8s.io/v1beta1
kind: KubeSchedulerConfiguration
profiles:
# ...
pluginConfig:
- name: RequestedToCapacityRatio
args:
shape:
- utilization: 0
score: 10
- utilization: 100
score: 0
resources:
- name: intel.com/foo
weight: 3
- name: intel.com/bar
weight: 5
使用 kube-scheduler 標志 --config=/path/to/config/file 引用 KubeSchedulerConfiguration 文件將配置傳遞給調度器。
默認情況下此功能處於被禁用狀態
調整 RequestedToCapacityRatioResourceAllocation 優先級函數
shape 用於指定 RequestedToCapacityRatioPriority 函數的行為。
{"utilization": 0, "score": 0},
{"utilization": 100, "score": 10}
上面的參數在 utilization 為 0% 時給節點評分為 0,在 utilization 為 100% 時給節點評分為 10,因此啟用了裝箱行為。要啟用最少請求(least requested)模式,必須按如下方式反轉得分值。
{"utilization": 0, "score": 10},
{"utilization": 100, "score": 0}
resources 是一個可選參數,默認情況下設置為:
"resources": [
{"name": "CPU", "weight": 1},
{"name": "Memory", "weight": 1}
]
它可以用來添加擴展資源,如下所示:
"resources": [
{"name": "intel.com/foo", "weight": 5},
{"name": "CPU", "weight": 3},
{"name": "Memory", "weight": 1}
]
weight 參數是可選的,如果未指定,則設置為 1。同時,weight 不能設置為負值。
RequestedToCapacityRatioResourceAllocation 優先級函數如何對節點評分
本節適用於希望了解此功能的內部細節的人員。以下是如何針對給定的一組值來計算節點得分的示例。
請求的資源
intel.com/foo: 2
Memory: 256MB
CPU: 2
資源權重
intel.com/foo: 5
Memory: 1
CPU: 3
FunctionShapePoint {{0, 0}, {100, 10}}
節點 Node 1 配置
可用:
intel.com/foo : 4
Memory : 1 GB
CPU: 8
已用:
intel.com/foo: 1
Memory: 256MB
CPU: 1
節點得分:
intel.com/foo = resourceScoringFunction((2+1),4)
= (100 - ((4-3)*100/4)
= (100 - 25)
= 75
= rawScoringFunction(75)
= 7
Memory = resourceScoringFunction((256+256),1024)
= (100 -((1024-512)*100/1024))
= 50
= rawScoringFunction(50)
= 5
CPU = resourceScoringFunction((2+1),8)
= (100 -((8-3)*100/8))
= 37.5
= rawScoringFunction(37.5)
= 3
NodeScore = (7 * 5) + (5 * 1) + (3 * 3) / (5 + 1 + 3)
= 5
節點 Node 2 配置
可用:
intel.com/foo: 8
Memory: 1GB
CPU: 8
已用:
intel.com/foo: 2
Memory: 512MB
CPU: 6
節點得分:
intel.com/foo = resourceScoringFunction((2+2),8)
= (100 - ((8-4)*100/8)
= (100 - 50)
= 50
= rawScoringFunction(50)
= 5
Memory = resourceScoringFunction((256+512),1024)
= (100 -((1024-768)*100/1024))
= 75
= rawScoringFunction(75)
= 7
CPU = resourceScoringFunction((2+6),8)
= (100 -((8-8)*100/8))
= 100
= rawScoringFunction(100)
= 10
NodeScore = (5 * 5) + (7 * 1) + (10 * 3) / (5 + 1 + 3)
= 7
9 - 調度框架
FEATURE STATE: Kubernetes 1.19 [stable]
調度框架是面向 Kubernetes 調度器的一種插件架構,它為現有的調度器添加了一組新的“插件” API。插件會被編譯到調度器之中。這些 API 允許大多數調度功能以插件的形式實現,同時使調度“核心”保持簡單且可維護。請參考調度框架的設計提案獲取框架設計的更多技術信息。
框架工作流程
調度框架定義了一些擴展點。調度器插件注冊后在一個或多個擴展點處被調用。這些插件中的一些可以改變調度決策,而另一些僅用於提供信息。
每次調度一個 Pod 的嘗試都分為兩個階段,即調度周期和綁定周期。
調度周期和綁定周期
調度周期為 Pod 選擇一個節點,綁定周期將該決策應用於集群。調度周期和綁定周期一起被稱為“調度上下文”。
調度周期是串行運行的,而綁定周期可能是同時運行的。
如果確定 Pod 不可調度或者存在內部錯誤,則可以終止調度周期或綁定周期。Pod 將返回隊列並重試。
擴展點
下圖顯示了一個 Pod 的調度上下文以及調度框架公開的擴展點。在此圖片中,“過濾器”等同於“斷言”,“評分”相當於“優先級函數”。
一個插件可以在多個擴展點處注冊,以執行更復雜或有狀態的任務。
調度框架擴展點
隊列排序
隊列排序插件用於對調度隊列中的 Pod 進行排序。隊列排序插件本質上提供 less(Pod1, Pod2) 函數。一次只能啟動一個隊列插件。
前置過濾
前置過濾插件用於預處理 Pod 的相關信息,或者檢查集群或 Pod 必須滿足的某些條件。如果 PreFilter 插件返回錯誤,則調度周期將終止。
過濾
過濾插件用於過濾出不能運行該 Pod 的節點。對於每個節點,調度器將按照其配置順序調用這些過濾插件。如果任何過濾插件將節點標記為不可行,則不會為該節點調用剩下的過濾插件。節點可以被同時進行評估。
后置過濾
這些插件在篩選階段后調用,但僅在該 Pod 沒有可行的節點時調用。插件按其配置的順序調用。如果任何后過濾器插件標記節點為“可調度”,則其余的插件不會調用。典型的后篩選實現是搶占,試圖通過搶占其他 Pod 的資源使該 Pod 可以調度。
前置評分
前置評分插件用於執行 “前置評分” 工作,即生成一個可共享狀態供評分插件使用。如果 PreScore 插件返回錯誤,則調度周期將終止。
評分
評分插件用於對通過過濾階段的節點進行排名。調度器將為每個節點調用每個評分插件。將有一個定義明確的整數范圍,代表最小和最大分數。在標准化評分階段之后,調度器將根據配置的插件權重合並所有插件的節點分數。
標准化評分
標准化評分插件用於在調度器計算節點的排名之前修改分數。在此擴展點注冊的插件將使用同一插件的評分結果被調用。每個插件在每個調度周期調用一次。
例如,假設一個 BlinkingLightScorer 插件基於具有的閃爍指示燈數量來對節點進行排名。
func ScoreNode(_ *v1.pod, n *v1.Node) (int, error) {
return getBlinkingLightCount(n)
}
然而,最大的閃爍燈個數值可能比 NodeScoreMax 小。要解決這個問題,BlinkingLightScorer 插件還應該注冊該擴展點。
func NormalizeScores(scores map[string]int) {
highest := 0
for _, score := range scores {
highest = max(highest, score)
}
for node, score := range scores {
scores[node] = score*NodeScoreMax/highest
}
}
如果任何 NormalizeScore 插件返回錯誤,則調度階段將終止。
說明: 希望執行“預保留”工作的插件應該使用 NormalizeScore 擴展點。
Reserve
Reserve 是一個信息性的擴展點。管理運行時狀態的插件(也成為“有狀態插件”)應該使用此擴展點,以便調度器在節點給指定 Pod 預留了資源時能夠通知該插件。這是在調度器真正將 Pod 綁定到節點之前發生的,並且它存在是為了防止在調度器等待綁定成功時發生競爭情況。
這個是調度周期的最后一步。一旦 Pod 處於保留狀態,它將在綁定周期結束時觸發不保留插件(失敗時)或綁定后插件(成功時)。
Permit
Permit 插件在每個 Pod 調度周期的最后調用,用於防止或延遲 Pod 的綁定。一個允許插件可以做以下三件事之一:
- 批准
一旦所有 Permit 插件批准 Pod 后,該 Pod 將被發送以進行綁定。 - 拒絕
如果任何 Permit 插件拒絕 Pod,則該 Pod 將被返回到調度隊列。這將觸發Unreserve 插件。 - 等待(帶有超時)
如果一個 Permit 插件返回 “等待” 結果,則 Pod 將保持在一個內部的 “等待中” 的 Pod 列表,同時該 Pod 的綁定周期啟動時即直接阻塞直到得到批准。如果超時發生,等待變成拒絕,並且 Pod 將返回調度隊列,從而觸發 Unreserve 插件。
說明: 盡管任何插件可以訪問 “等待中” 狀態的 Pod 列表並批准它們 (查看 FrameworkHandle)。我們期望只有允許插件可以批准處於 “等待中” 狀態的預留 Pod 的綁定。一旦 Pod 被批准了,它將發送到預綁定階段。
預綁定
預綁定插件用於執行 Pod 綁定前所需的任何工作。例如,一個預綁定插件可能需要提供網絡卷並且在允許 Pod 運行在該節點之前將其掛載到目標節點上。
如果任何 PreBind 插件返回錯誤,則 Pod 將被拒絕並且退回到調度隊列中。
Bind
Bind 插件用於將 Pod 綁定到節點上。直到所有的 PreBind 插件都完成,Bind 插件才會被調用。各綁定插件按照配置順序被調用。綁定插件可以選擇是否處理指定的 Pod。如果綁定插件選擇處理 Pod,剩余的綁定插件將被跳過。
綁定后
這是個信息性的擴展點。綁定后插件在 Pod 成功綁定后被調用。這是綁定周期的結尾,可用於清理相關的資源。
Unreserve
這是個信息性的擴展點。如果 Pod 被保留,然后在后面的階段中被拒絕,則 Unreserve 插件將被通知。Unreserve 插件應該清楚保留 Pod 的相關狀態。
使用此擴展點的插件通常也使用 Reserve。
插件 API
插件 API 分為兩個步驟。首先,插件必須完成注冊並配置,然后才能使用擴展點接口。擴展點接口具有以下形式。
type Plugin interface {
Name() string
}
type QueueSortPlugin interface {
Plugin
Less(*v1.pod, *v1.pod) bool
}
type PreFilterPlugin interface {
Plugin
PreFilter(context.Context, *framework.CycleState, *v1.pod) error
}
// ...
插件配置
你可以在調度器配置中啟用或禁用插件。如果你在使用 Kubernetes v1.18 或更高版本,大部分調度插件都在使用中且默認啟用。
除了默認的插件,你還可以實現自己的調度插件並且將它們與默認插件一起配置。你可以訪問scheduler-plugins 了解更多信息。
如果你正在使用 Kubernetes v1.18 或更高版本,你可以將一組插件設置為一個調度器配置文件,然后定義不同的配置文件來滿足各類工作負載。了解更多關於多配置文件。
10 - 調度器性能調優
FEATURE STATE: Kubernetes 1.14 [beta]
作為 kubernetes 集群的默認調度器,kube-scheduler 主要負責將 Pod 調度到集群的 Node 上。
在一個集群中,滿足一個 Pod 調度請求的所有 Node 稱之為可調度 Node。調度器先在集群中找到一個 Pod 的可調度 Node,然后根據一系列函數對這些可調度 Node 打分,之后選出其中得分最高的 Node 來運行 Pod。最后,調度器將這個調度決定告知 kube-apiserver,這個過程叫做綁定(Binding)。
這篇文章將會介紹一些在大規模 Kubernetes 集群下調度器性能優化的方式。
在大規模集群中,你可以調節調度器的表現來平衡調度的延遲(新 Pod 快速就位)和精度(調度器很少做出糟糕的放置決策)。
你可以通過設置 kube-scheduler 的 percentageOfNodesToScore 來配置這個調優設置。這個 KubeSchedulerConfiguration 設置決定了調度集群中節點的閾值。
設置閾值
percentageOfNodesToScore 選項接受從 0 到 100 之間的整數值。0 值比較特殊,表示 kube-scheduler 應該使用其編譯后的默認值。如果你設置 percentageOfNodesToScore 的值超過了 100,kube-scheduler 的表現等價於設置值為 100。
要修改這個值,先編輯 kube-scheduler 的配置文件然后重啟調度器。大多數情況下,這個配置文件是 /etc/kubernetes/config/kube-scheduler.yaml。
修改完成后,你可以執行
kubectl get pods -n kube-system | grep kube-scheduler
來檢查該 kube-scheduler 組件是否健康。
節點打分閾值
要提升調度性能,kube-scheduler 可以在找到足夠的可調度節點之后停止查找。在大規模集群中,比起考慮每個節點的簡單方法相比可以節省時間。
你可以使用整個集群節點總數的百分比作為閾值來指定需要多少節點就足夠。kube-scheduler 會將它轉換為節點數的整數值。在調度期間,如果 kube-scheduler 已確認的可調度節點數足以超過了配置的百分比數量,kube-scheduler 將停止繼續查找可調度節點並繼續進行打分階段。
調度器如何遍歷節點詳細介紹了這個過程。
默認閾值
如果你不指定閾值,Kubernetes 使用線性公式計算出一個比例,在 100-節點集群下取 50%,在 5000-節點的集群下取 10%。這個自動設置的參數的最低值是 5%。
這意味着,調度器至少會對集群中 5% 的節點進行打分,除非用戶將該參數設置的低於 5。
如果你想讓調度器對集群內所有節點進行打分,則將 percentageOfNodesToScore 設置為 100。
示例
下面就是一個將 percentageOfNodesToScore 參數設置為 50% 的例子。
apiVersion: kubescheduler.config.k8s.io/v1alpha1
kind: KubeSchedulerConfiguration
algorithmSource:
provider: DefaultProvider
...
percentageOfNodesToScore: 50
調節 percentageOfNodesToScore 參數
percentageOfNodesToScore 的值必須在 1 到 100 之間,而且其默認值是通過集群的規模計算得來的。另外,還有一個 50 個 Node 的最小值是硬編碼在程序中。
值得注意的是,該參數設置后可能會導致只有集群中少數節點被選為可調度節點,很多節點都沒有進入到打分階段。這樣就會造成一種后果,一個本來可以在打分階段得分很高的節點甚至都不能進入打分階段。
由於這個原因,這個參數不應該被設置成一個很低的值。通常的做法是不會將這個參數的值設置的低於 10。很低的參數值一般在調度器的吞吐量很高且對節點的打分不重要的情況下才使用。換句話說,只有當你更傾向於在可調度節點中任意選擇一個節點來運行這個 Pod 時,才使用很低的參數設置。
調度器做調度選擇的時候如何覆蓋所有的 Node
如果你想要理解這一個特性的內部細節,那么請仔細閱讀這一章節。
在將 Pod 調度到節點上時,為了讓集群中所有節點都有公平的機會去運行這些 Pod,調度器將會以輪詢的方式覆蓋全部的 Node。你可以將 Node 列表想象成一個數組。調度器從數組的頭部開始篩選可調度節點,依次向后直到可調度節點的數量達到 percentageOfNodesToScore 參數的要求。在對下一個 Pod 進行調度的時候,前一個 Pod 調度篩選停止的 Node 列表的位置,將會來作為這次調度篩選 Node 開始的位置。
如果集群中的 Node 在多個區域,那么調度器將從不同的區域中輪詢 Node,來確保不同區域的 Node 接受可調度性檢查。如下例,考慮兩個區域中的六個節點:
Zone 1: Node 1, Node 2, Node 3, Node 4
Zone 2: Node 5, Node 6
調度器將會按照如下的順序去評估 Node 的可調度性:
Node 1, Node 5, Node 2, Node 6, Node 3, Node 4
在評估完所有 Node 后,將會返回到 Node 1,從頭開始。