[K8s]Kubernetes-工作負載(上)


工作負載

理解 Pods,Kubernetes 中可部署的最小計算對象,以及輔助它運行它們的高層抽象對象。

工作負載是在 Kubernetes 上運行的應用程序。

無論你的負載是單一組件還是由多個一同工作的組件構成,在 Kubernetes 中你可以在一組 Pods 中運行它。在 Kubernetes 中,Pod 代表的是集群上處於運行狀態的一組容器。

Kubernetes Pods 有確定的生命周期。例如,當某 Pod 在你的集群中運行時,Pod 運行所在的 節點出現致命錯誤時,所有該節點上的 Pods 都會失敗。Kubernetes 將這類失敗視為最終狀態:即使該節點后來恢復正常運行,你也需要創建新的 Pod 來恢復應用。

不過,為了讓用戶的日子略微好過一些,你並不需要直接管理每個 Pod。相反,你可以使用負載資源來替你管理一組 Pods。這些資源配置控制器來確保合適類型的、處於運行狀態的 Pod 個數是正確的,與你所指定的狀態相一致。

Kubernetes 提供若干種內置的工作負載資源:

  • Deployment 和 ReplicaSet(替換原來的資源 ReplicationController)。Deployment 很適合用來管理你的集群上的無狀態應用,Deployment 中的所有 Pod 都是相互等價的,並且在需要的時候被換掉。
  • StatefulSet 讓你能夠運行一個或者多個以某種方式跟蹤應用狀態的 Pods。例如,如果你的負載會將數據作持久存儲,你可以運行一個 StatefulSet,將每個 Pod 與某個 PersistentVolume 對應起來。你在 StatefulSet 中各個 Pod 內運行的代碼可以將數據復制到同一 StatefulSet 中的其它 Pod 中以提高整體的服務可靠性。
  • DaemonSet 定義提供節點本地支撐設施的 Pods。這些 Pods 可能對於你的集群的運維是非常重要的,例如作為網絡鏈接的輔助工具或者作為網絡插件的一部分等等。每次你向集群中添加一個新節點時,如果該節點與某 DaemonSet 的規約匹配,則控制面會為該 DaemonSet 調度一個 Pod 到該新節點上運行。
  • Job 和 CronJob。定義一些一直運行到結束並停止的任務。Job 用來表達的是一次性的任務,而 CronJob 會根據其時間規划反復運行。

在龐大的 Kubernetes 生態系統中,你還可以找到一些提供額外操作的第三方工作負載資源。通過使用定制資源定義(CRD),你可以添加第三方工作負載資源,以完成原本不是 Kubernetes 核心功能的工作。例如,如果你希望運行一組 Pods,但要求所有 Pods 都可用時才執行操作(比如針對某種高吞吐量的分布式任務),你可以實現一個能夠滿足這一需求的擴展,並將其安裝到集群中運行。

1 - Pods

Pod 是可以在 Kubernetes 中創建和管理的、最小的可部署的計算單元。

Pod(就像在鯨魚莢或者豌豆莢中)是一組(一個或多個)容器;這些容器共享存儲、網絡、以及怎樣運行這些容器的聲明。Pod 中的內容總是並置(colocated)的並且一同調度,在共享的上下文中運行。Pod 所建模的是特定於應用的“邏輯主機”,其中包含一個或多個應用容器,這些容器是相對緊密的耦合在一起的。在非雲環境中,在相同的物理機或虛擬機上運行的應用類似於在同一邏輯主機上運行的雲應用。

除了應用容器,Pod 還可以包含在 Pod 啟動期間運行的 Init 容器。你也可以在集群中支持臨時性容器的情況下,為調試的目的注入臨時性容器。

什么是 Pod?

說明:除了 Docker 之外,Kubernetes 支持很多其他容器運行時,Docker 是最有名的運行時,使用 Docker 的術語來描述 Pod 會很有幫助。

Pod 的共享上下文包括一組 Linux 名字空間、控制組(cgroup)和可能一些其他的隔離方面,即用來隔離 Docker 容器的技術。在 Pod 的上下文中,每個獨立的應用可能會進一步實施隔離。

就 Docker 概念的術語而言,Pod 類似於共享名字空間和文件系統卷的一組 Docker 容器。

使用 Pod

通常你不需要直接創建 Pod,甚至單實例 Pod。相反,你會使用諸如 Deployment 或 Job 這類工作負載資源來創建 Pod。如果 Pod 需要跟蹤狀態,可以考慮 StatefulSet 資源。

Kubernetes 集群中的 Pod 主要有兩種用法:

  • 運行單個容器的 Pod。"每個 Pod 一個容器"模型是最常見的 Kubernetes 用例;在這種情況下,可以將 Pod 看作單個容器的包裝器,並且 Kubernetes 直接管理 Pod,而不是容器。

  • 運行多個協同工作的容器的 Pod。Pod 可能封裝由多個緊密耦合且需要共享資源的共處容器組成的應用程序。這些位於同一位置的容器可能形成單個內聚的服務單元 —— 一個容器將文件從共享卷提供給公眾,而另一個單獨的“邊車”(sidecar)容器則刷新或更新這些文件。Pod 將這些容器和存儲資源打包為一個可管理的實體。

說明:將多個並置、同管的容器組織到一個 Pod 中是一種相對高級的使用場景。只有在一些場景中,容器之間緊密關聯時你才應該使用這種模式。

每個 Pod 都旨在運行給定應用程序的單個實例。如果希望橫向擴展應用程序(例如,運行多個實例 以提供更多的資源),則應該使用多個 Pod,每個實例使用一個 Pod。在 Kubernetes 中,這通常被稱為 副本(Replication)。通常使用一種工作負載資源及其控制器來創建和管理一組 Pod 副本。

Pod 怎樣管理多個容器

Pod 被設計成支持形成內聚服務單元的多個協作過程(形式為容器)。Pod 中的容器被自動安排到集群中的同一物理機或虛擬機上,並可以一起進行調度。容器之間可以共享資源和依賴、彼此通信、協調何時以及何種方式終止自身。

例如,你可能有一個容器,為共享卷中的文件提供 Web 服務器支持,以及一個單獨的 “sidecar(掛斗)”容器負責從遠端更新這些文件,如下圖所示:

image

有些 Pod 具有 Init 容器和應用容器。Init 容器會在啟動應用容器之前運行並完成。

Pod 天生地為其成員容器提供了兩種共享資源:網絡和存儲。

使用 Pod

你很少在 Kubernetes 中直接創建一個個的 Pod,甚至是單實例(Singleton)的 Pod。這是因為 Pod 被設計成了相對臨時性的、用后即拋的一次性實體。當 Pod 由你或者間接地由控制器創建時,它被調度在集群中的節點上運行。Pod 會保持在該節點上運行,直到 Pod 結束執行、Pod 對象被刪除、Pod 因資源不足而被驅逐或者節點失效為止。

說明:重啟 Pod 中的容器不應與重啟 Pod 混淆。Pod 不是進程,而是容器運行的環境。在被刪除之前,Pod 會一直存在。

當你為 Pod 對象創建清單時,要確保所指定的 Pod 名稱是合法的 DNS 子域名。

Pod 和控制器

你可以使用工作負載資源來創建和管理多個 Pod。資源的控制器能夠處理副本的管理、上線,並在 Pod 失效時提供自愈能力。例如,如果一個節點失敗,控制器注意到該節點上的 Pod 已經停止工作,就可以創建替換性的 Pod。調度器會將替身 Pod 調度到一個健康的節點執行。

下面是一些管理一個或者多個 Pod 的工作負載資源的示例:

  • Deployment
  • StatefulSet
  • DaemonSet

Pod 模版

負載資源的控制器通常使用 Pod 模板(Pod Template)來替你創建 Pod 並管理它們。

Pod 模板是包含在工作負載對象中的規范,用來創建 Pod。這類負載資源包括Deployment、 Job 和 DaemonSets等。

工作負載的控制器會使用負載對象中的 PodTemplate 來生成實際的 Pod。PodTemplate 是你用來運行應用時指定的負載資源的目標狀態的一部分。

下面的示例是一個簡單的 Job 的清單,其中的 template 指示啟動一個容器。該 Pod 中的容器會打印一條消息之后暫停。

apiVersion: batch/v1
kind: Job
metadata:
  name: hello
spec:
  template:
    # 這里是 Pod 模版
    spec:
      containers:
      - name: hello
        image: busybox
        command: ['sh', '-c', 'echo "Hello, Kubernetes!" && sleep 3600']
      restartPolicy: OnFailure
    # 以上為 Pod 模版

修改 Pod 模版或者切換到新的 Pod 模版都不會對已經存在的 Pod 起作用。Pod 不會直接收到模版的更新。相反,新的 Pod 會被創建出來,與更改后的 Pod 模版匹配。

例如,Deployment 控制器針對每個 Deployment 對象確保運行中的 Pod 與當前的 Pod 模版匹配。如果模版被更新,則 Deployment 必須刪除現有的 Pod,基於更新后的模版創建新的 Pod。每個工作負載資源都實現了自己的規則,用來處理對 Pod 模版的更新。

在節點上,kubelet並不直接監測或管理與 Pod 模版相關的細節或模版的更新,這些細節都被抽象出來。這種抽象和關注點分離簡化了整個系統的語義,並且使得用戶可以在不改變現有代碼的前提下就能擴展集群的行為。

Pod 更新與替換

正如前面章節所述,當某工作負載的 Pod 模板被改變時,控制器會基於更新的模板創建新的 Pod 對象而不是對現有 Pod 執行更新或者修補操作。

Kubernetes 並不禁止你直接管理 Pod。對運行中的 Pod 的某些字段執行就地更新操作還是可能的。不過,類似 patch 和 replace 這類更新操作有一些限制:

  • Pod 的絕大多數元數據都是不可變的。例如,你不可以改變其 namespace、name、uid 或者 creationTimestamp 字段;generation 字段是比較特別的,如果更新該字段,只能增加字段取值而不能減少。

  • 如果 metadata.deletionTimestamp 已經被設置,則不可以向 metadata.finalizers 列表中添加新的條目。

  • Pod 更新不可以改變除 spec.containers[*].image、spec.initContainers[*].image、 spec.activeDeadlineSeconds 或 spec.tolerations 之外的字段。對於 spec.tolerations,你只被允許添加新的條目到其中。

  • 在更新spec.activeDeadlineSeconds 字段時,以下兩種更新操作是被允許的:

    1. 如果該字段尚未設置,可以將其設置為一個正數;
    2. 如果該字段已經設置為一個正數,可以將其設置為一個更小的、非負的整數。

資源共享和通信

Pod 使它的成員容器間能夠進行數據共享和通信。

Pod 中的存儲

一個 Pod 可以設置一組共享的存儲卷。Pod 中的所有容器都可以訪問該共享卷,從而允許這些容器共享數據。卷還允許 Pod 中的持久數據保留下來,即使其中的容器需要重新啟動。有關 Kubernetes 如何在 Pod 中實現共享存儲並將其提供給 Pod 的更多信息。

Pod 聯網

每個 Pod 都在每個地址族中獲得一個唯一的 IP 地址。Pod 中的每個容器共享網絡名字空間,包括 IP 地址和網絡端口。Pod 內的容器可以使用 localhost 互相通信。當 Pod 中的容器與 Pod 之外的實體通信時,它們必須協調如何使用共享的網絡資源(例如端口)。

在同一個 Pod 內,所有容器共享一個 IP 地址和端口空間,並且可以通過 localhost 發現對方。 他們也能通過如 SystemV 信號量或 POSIX 共享內存這類標准的進程間通信方式互相通信。不同 Pod 中的容器的 IP 地址互不相同,沒有特殊配置就不能使用 IPC 進行通信。如果某容器希望與運行於其他 Pod 中的容器通信,可以通過 IP 聯網的方式實現。

Pod 中的容器所看到的系統主機名與為 Pod 配置的 name 屬性值相同。網絡部分提供了更多有關此內容的信息。

容器的特權模式

在 Linux 中,Pod 中的任何容器都可以使用容器規約中的安全性上下文中的 privileged(Linux)參數啟用特權模式。這對於想要使用操作系統管理權能(Capabilities,如操縱網絡堆棧和訪問設備)的容器很有用。

如果你的集群啟用了 WindowsHostProcessContainers 特性,你可以使用 Pod 規約中安全上下文的 windowsOptions.hostProcess 參數來創建 Windows HostProcess Pod。這些 Pod 中的所有容器都必須以 Windows HostProcess 容器方式運行。HostProcess Pod 可以直接運行在主機上,它也能像 Linux 特權容器一樣,用於執行管理任務。

說明:你的容器運行時必須支持特權容器的概念才能使用這一配置。

靜態 Pod

靜態 Pod(Static Pod) 直接由特定節點上的 kubelet 守護進程管理,不需要API服務器看到它們。盡管大多數 Pod 都是通過控制面(例如,Deployment)來管理的,對於靜態 Pod 而言,kubelet 直接監控每個 Pod,並在其失效時重啟之。

靜態 Pod 通常綁定到某個節點上的 kubelet。其主要用途是運行自托管的控制面。在自托管場景中,使用 kubelet 來管理各個獨立的控制面組件。

kubelet 自動嘗試為每個靜態 Pod 在 Kubernetes API 服務器上創建一個鏡像 Pod。這意味着在節點上運行的 Pod 在 API 服務器上是可見的,但不可以通過 API 服務器來控制。

說明:
靜態 Pod 的 spec 不能引用其他的 API 對象(例如:ServiceAccount、ConfigMap、Secret等)。

容器探針

Probe 是由 kubelet 對容器執行的定期診斷。要執行診斷,kubelet 可以執行三種動作:

  • ExecAction(借助容器運行時執行)
  • TCPSocketAction(由 kubelet 直接檢測)
  • HTTPGetAction(由 kubelet 直接檢測)

1.1 - Pod 的生命周期

本頁面講述 Pod 的生命周期。Pod 遵循一個預定義的生命周期,起始於 Pending 階段,如果至少其中有一個主要容器正常啟動,則進入 Running,之后取決於 Pod 中是否有容器以失敗狀態結束而進入 Succeeded 或者 Failed 階段。

在 Pod 運行期間,kubelet 能夠重啟容器以處理一些失效場景。在 Pod 內部,Kubernetes 跟蹤不同容器的狀態並確定使 Pod 重新變得健康所需要采取的動作。

在 Kubernetes API 中,Pod 包含規約部分和實際狀態部分。Pod 對象的狀態包含了一組 Pod 狀況(Conditions)。如果應用需要的話,你也可以向其中注入自定義的就緒性信息。

Pod 在其生命周期中只會被調度一次。一旦 Pod 被調度(分派)到某個節點,Pod 會一直在該節點運行,直到 Pod 停止或者被終止。

Pod 生命期

和一個個獨立的應用容器一樣,Pod 也被認為是相對臨時性(而不是長期存在)的實體。Pod 會被創建、賦予一個唯一的 ID(UID),並被調度到節點,並在終止(根據重啟策略)或刪除之前一直運行在該節點。

如果一個節點死掉了,調度到該節點的 Pod 也被計划在給定超時期限結束后刪除。

Pod 自身不具有自愈能力。如果 Pod 被調度到某節點而該節點之后失效,Pod 會被刪除;類似地,Pod 無法在因節點資源耗盡或者節點維護而被驅逐期間繼續存活。Kubernetes 使用一種高級抽象來管理這些相對而言可隨時丟棄的 Pod 實例,稱作控制器。

任何給定的 Pod(由 UID 定義)從不會被“重新調度(rescheduled)”到不同的節點;相反,這一 Pod 可以被一個新的、幾乎完全相同的 Pod 替換掉。如果需要,新 Pod 的名字可以不變,但是其 UID 會不同。

如果某物聲稱其生命期與某 Pod 相同,例如存儲卷,這就意味着該對象在此 Pod (UID 亦相同)存在期間也一直存在。如果 Pod 因為任何原因被刪除,甚至某完全相同的替代 Pod 被創建時,這個相關的對象(例如這里的卷)也會被刪除並重建。

image

Pod 結構圖例

一個包含多個容器的 Pod 中包含一個用來拉取文件的程序和一個 Web 服務器,均使用持久卷作為容器間共享的存儲。

Pod 階段

Pod 的 status 字段是一個 PodStatus 對象,其中包含一個 phase 字段。

Pod 的階段(Phase)是 Pod 在其生命周期中所處位置的簡單宏觀概述。該階段並不是對容器或 Pod 狀態的綜合匯總,也不是為了成為完整的狀態機。

Pod 階段的數量和含義是嚴格定義的。除了本文檔中列舉的內容外,不應該再假定 Pod 有其他的 phase 值。

下面是 phase 可能的值:

取值
描述
Pending(懸決)
Pod 已被 Kubernetes 系統接受,但有一個或者多個容器尚未創建亦未運行。此階段包括等待 Pod 被調度的時間和通過網絡下載鏡像的時間,
Running(運行中)
Pod 已經綁定到了某個節點,Pod 中所有的容器都已被創建。至少有一個容器仍在運行,或者正處於啟動或重啟狀態。
Succeeded(成功)
Pod 中的所有容器都已成功終止,並且不會再重啟。
Failed(失敗)
Pod 中的所有容器都已終止,並且至少有一個容器是因為失敗終止。也就是說,容器以非 0 狀態退出或者被系統終止。
Unknown(未知)
因為某些原因無法取得 Pod 的狀態。這種情況通常是因為與 Pod 所在主機通信失敗。

如果某節點死掉或者與集群中其他節點失聯,Kubernetes 會實施一種策略,將失去的節點上運行的所有 Pod 的 phase 設置為 Failed。

容器狀態

Kubernetes 會跟蹤 Pod 中每個容器的狀態,就像它跟蹤 Pod 總體上的階段一樣。你可以使用容器生命周期回調來在容器生命周期中的特定時間點觸發事件。

一旦調度器將 Pod 分派給某個節點,kubelet 就通過容器運行時開始為 Pod 創建容器。容器的狀態有三種:Waiting(等待)、Running(運行中)和 Terminated(已終止)。

要檢查 Pod 中容器的狀態,你可以使用 kubectl describe pod <pod 名稱>。其輸出中包含 Pod 中每個容器的狀態。

每種狀態都有特定的含義:

Waiting (等待) 

如果容器並不處在 Running 或 Terminated 狀態之一,它就處在 Waiting 狀態。處於 Waiting 狀態的容器仍在運行它完成啟動所需要的操作:例如,從某個容器鏡像倉庫拉取容器鏡像,或者向容器應用 Secret 數據等等。當你使用 kubectl 來查詢包含 Waiting 狀態的容器的 Pod 時,你也會看到一個 Reason 字段,其中給出了容器處於等待狀態的原因。

Running(運行中) 

Running 狀態表明容器正在執行狀態並且沒有問題發生。如果配置了 postStart 回調,那么該回調已經執行且已完成。如果你使用 kubectl 來查詢包含 Running 狀態的容器的 Pod 時,你也會看到關於容器進入 Running 狀態的信息。

Terminated(已終止) 

處於 Terminated 狀態的容器已經開始執行並且或者正常結束或者因為某些原因失敗。如果你使用 kubectl 來查詢包含 Terminated 狀態的容器的 Pod 時,你會看到容器進入此狀態的原因、退出代碼以及容器執行期間的起止時間。

如果容器配置了 preStop 回調,則該回調會在容器進入 Terminated 狀態之前執行。

容器重啟策略

Pod 的 spec 中包含一個 restartPolicy 字段,其可能取值包括 Always、OnFailure 和 Never。默認值是 Always。

restartPolicy 適用於 Pod 中的所有容器。restartPolicy 僅針對同一節點上 kubelet 的容器重啟動作。當 Pod 中的容器退出時,kubelet 會按指數回退方式計算重啟的延遲(10s、20s、40s、...),其最長延遲為 5 分鍾。一旦某容器執行了 10 分鍾並且沒有出現問題,kubelet 對該容器的重啟回退計時器執行重置操作。

Pod 狀況

Pod 有一個 PodStatus 對象,其中包含一個 PodConditions 數組。Pod 可能通過也可能未通過其中的一些狀況測試。

  • PodScheduled:Pod 已經被調度到某節點;
  • ContainersReady:Pod 中所有容器都已就緒;
  • Initialized:所有的 Init 容器都已成功啟動;
  • Ready:Pod 可以為請求提供服務,並且應該被添加到對應服務的負載均衡池中。
字段名稱
描述
type
Pod 狀況的名稱
status
表明該狀況是否適用,可能的取值有 "True", "False" 或 "Unknown"
lastProbeTime
上次探測 Pod 狀況時的時間戳
lastTransitionTime
Pod 上次從一種狀態轉換到另一種狀態時的時間戳
reason
機器可讀的、駝峰編碼(UpperCamelCase)的文字,表述上次狀況變化的原因
message
人類可讀的消息,給出上次狀態轉換的詳細信息

Pod 就緒態

FEATURE STATE: Kubernetes v1.14 [stable]

你的應用可以向 PodStatus 中注入額外的反饋或者信號:Pod Readiness(Pod 就緒態)。要使用這一特性,可以設置 Pod 規約中的 readinessGates 列表,為 kubelet 提供一組額外的狀況供其評估 Pod 就緒態時使用。

就緒態門控基於 Pod 的 status.conditions 字段的當前值來做決定。如果 Kubernetes 無法在 status.conditions 字段中找到某狀況,則該狀況的狀態值默認為 "False"。

這里是一個例子:

kind: Pod
...
spec:
  readinessGates:
    - conditionType: "www.example.com/feature-1"
status:
  conditions:
    - type: Ready                              # 內置的 Pod 狀況
      status: "False"
      lastProbeTime: null
      lastTransitionTime: 2018-01-01T00:00:00Z
    - type: "www.example.com/feature-1"        # 額外的 Pod 狀況
      status: "False"
      lastProbeTime: null
      lastTransitionTime: 2018-01-01T00:00:00Z
  containerStatuses:
    - containerID: docker://abcd...
      ready: true
...

你所添加的 Pod 狀況名稱必須滿足 Kubernetes 標簽鍵名格式。

Pod 就緒態的狀態

命令 kubectl patch 不支持修改對象的狀態。如果需要設置 Pod 的 status.conditions,應用或者 Operators 需要使用 PATCH 操作。你可以使用 Kubernetes 客戶端庫之一來編寫代碼,針對 Pod 就緒態設置定制的 Pod 狀況。

對於使用定制狀況的 Pod 而言,只有當下面的陳述都適用時,該 Pod 才會被評估為就緒:

  • Pod 中所有容器都已就緒;
  • readinessGates 中的所有狀況都為 True 值。

當 Pod 的容器都已就緒,但至少一個定制狀況沒有取值或者取值為 False,kubelet 將 Pod 的狀況設置為 ContainersReady。

容器探針

Probe 是由 kubelet 對容器執行的定期診斷。要執行診斷,kubelet 調用由容器實現的 Handler (處理程序)。有三種類型的處理程序:

  • ExecAction: 在容器內執行指定命令。如果命令退出時返回碼為 0 則認為診斷成功。

  • TCPSocketAction: 對容器的 IP 地址上的指定端口執行 TCP 檢查。如果端口打開,則診斷被認為是成功的。

  • HTTPGetAction: 對容器的 IP 地址上指定端口和路徑執行 HTTP Get 請求。如果響應的狀態碼大於等於 200 且小於 400,則診斷被認為是成功的。

每次探測都將獲得以下三種結果之一:

  • Success(成功):容器通過了診斷。
  • Failure(失敗):容器未通過診斷。
  • Unknown(未知):診斷失敗,因此不會采取任何行動。

針對運行中的容器,kubelet 可以選擇是否執行以下三種探針,以及如何針對探測結果作出反應:

  • livenessProbe:指示容器是否正在運行。如果存活態探測失敗,則 kubelet 會殺死容器,並且容器將根據其重啟策略決定未來。如果容器不提供存活探針,則默認狀態為 Success。

  • readinessProbe:指示容器是否准備好為請求提供服務。如果就緒態探測失敗,端點控制器將從與 Pod 匹配的所有服務的端點列表中刪除該 Pod 的 IP 地址。初始延遲之前的就緒態的狀態值默認為 Failure。 如果容器不提供就緒態探針,則默認狀態為 Success。

  • startupProbe: 指示容器中的應用是否已經啟動。如果提供了啟動探針,則所有其他探針都會被禁用,直到此探針成功為止。如果啟動探測失敗,kubelet 將殺死容器,而容器依其重啟策略進行重啟。如果容器沒有提供啟動探測,則默認狀態為 Success。

何時該使用存活態探針?

FEATURE STATE: Kubernetes v1.0 [stable]

如果容器中的進程能夠在遇到問題或不健康的情況下自行崩潰,則不一定需要存活態探針; kubelet 將根據 Pod 的restartPolicy 自動執行修復操作。

如果你希望容器在探測失敗時被殺死並重新啟動,那么請指定一個存活態探針,並指定restartPolicy 為 "Always" 或 "OnFailure"。

何時該使用就緒態探針?

FEATURE STATE: Kubernetes v1.0 [stable]

如果要僅在探測成功時才開始向 Pod 發送請求流量,請指定就緒態探針。在這種情況下,就緒態探針可能與存活態探針相同,但是規約中的就緒態探針的存在意味着 Pod 將在啟動階段不接收任何數據,並且只有在探針探測成功后才開始接收數據。

如果你希望容器能夠自行進入維護狀態,也可以指定一個就緒態探針,檢查某個特定於就緒態的因此不同於存活態探測的端點。

如果你的應用程序對后端服務有嚴格的依賴性,你可以同時實現存活態和就緒態探針。當應用程序本身是健康的,存活態探針檢測通過后,就緒態探針會額外檢查每個所需的后端服務是否可用。這可以幫助你避免將流量導向只能返回錯誤信息的 Pod。

如果你的容器需要在啟動期間加載大型數據、配置文件或執行遷移,你可以使用啟動探針。然而,如果你想區分已經失敗的應用和仍在處理其啟動數據的應用,你可能更傾向於使用就緒探針。

說明:
請注意,如果你只是想在 Pod 被刪除時能夠排空請求,則不一定需要使用就緒態探針;在刪除 Pod 時,Pod 會自動將自身置於未就緒狀態,無論就緒態探針是否存在。等待 Pod 中的容器停止期間,Pod 會一直處於未就緒狀態。

何時該使用啟動探針?

FEATURE STATE: Kubernetes v1.18 [beta]

對於所包含的容器需要較長時間才能啟動就緒的 Pod 而言,啟動探針是有用的。你不再需要配置一個較長的存活態探測時間間隔,只需要設置另一個獨立的配置選定,對啟動期間的容器執行探測,從而允許使用遠遠超出存活態時間間隔所允許的時長。

如果你的容器啟動時間通常超出 initialDelaySeconds + failureThreshold × periodSeconds 總值,你應該設置一個啟動探測,對存活態探針所使用的同一端點執行檢查。periodSeconds 的默認值是 10 秒。你應該將其 failureThreshold 設置得足夠高,以便容器有充足的時間完成啟動,並且避免更改存活態探針所使用的默認值。這一設置有助於減少死鎖狀況的發生。

Pod 的終止

由於 Pod 所代表的是在集群中節點上運行的進程,當不再需要這些進程時允許其體面地終止是很重要的。一般不應武斷地使用 KILL 信號終止它們,導致這些進程沒有機會完成清理操作。

設計的目標是令你能夠請求刪除進程,並且知道進程何時被終止,同時也能夠確保刪除操作終將完成。當你請求刪除某個 Pod 時,集群會記錄並跟蹤 Pod 的體面終止周期,而不是直接強制地殺死 Pod。在存在強制關閉設施的前提下,kubelet 會嘗試體面地終止 Pod。

通常情況下,容器運行時會發送一個 TERM 信號到每個容器中的主進程。很多容器運行時都能夠注意到容器鏡像中 STOPSIGNAL 的值,並發送該信號而不是 TERM。一旦超出了體面終止限期,容器運行時會向所有剩余進程發送 KILL 信號,之后 Pod 就會被從 API 服務器上移除。如果 kubelet 或者容器運行時的管理服務在等待進程終止期間被重啟,集群會從頭開始重試,賦予 Pod 完整的體面終止限期。

下面是一個例子:

  1. 你使用 kubectl 工具手動刪除某個特定的 Pod,而該 Pod 的體面終止限期是默認值(30 秒)。

  2. API 服務器中的 Pod 對象被更新,記錄涵蓋體面終止限期在內 Pod 的最終死期,超出所計算時間點則認為 Pod 已死(dead)。如果你使用 kubectl describe 來查驗你正在刪除的 Pod,該 Pod 會顯示為 "Terminating" (正在終止)。在 Pod 運行所在的節點上:kubelet 一旦看到 Pod 被標記為正在終止(已經設置了體面終止限期),kubelet 即開始本地的 Pod 關閉過程。

    1. 如果 Pod 中的容器之一定義了 preStop 回調,kubelet 開始在容器內運行該回調邏輯。如果超出體面終止限期時,preStop 回調邏輯仍在運行,kubelet 會請求給予該 Pod 的寬限期一次性增加 2 秒鍾。

    說明:如果 preStop 回調所需要的時間長於默認的體面終止限期,你必須修改 terminationGracePeriodSeconds 屬性值來使其正常工作。

    1. kubelet 接下來觸發容器運行時發送 TERM 信號給每個容器中的進程 1。

    說明:Pod 中的容器會在不同時刻收到 TERM 信號,接收順序也是不確定的。如果關閉的順序很重要,可以考慮使用 preStop 回調邏輯來協調。

  3. 與此同時,kubelet 啟動體面關閉邏輯,控制面會將 Pod 從對應的端點列表(以及端點切片列表,如果啟用了的話)中移除,過濾條件是 Pod 被對應的服務以某選擇算符選定。ReplicaSets和其他工作負載資源不再將關閉進程中的 Pod 視為合法的、能夠提供服務的副本。關閉動作很慢的 Pod 也無法繼續處理請求數據,因為負載均衡器(例如服務代理)已經在終止寬限期開始的時候將其從端點列表中移除。

  4. 超出終止寬限期限時,kubelet 會觸發強制關閉過程。容器運行時會向 Pod 中所有容器內仍在運行的進程發送 SIGKILL 信號。kubelet 也會清理隱藏的 pause 容器,如果容器運行時使用了這種容器的話。

  5. kubelet 觸發強制從 API 服務器上刪除 Pod 對象的邏輯,並將體面終止限期設置為 0 (這意味着馬上刪除)。

  6. API 服務器刪除 Pod 的 API 對象,從任何客戶端都無法再看到該對象。

強制終止 Pod

注意: 對於某些工作負載及其 Pod 而言,強制刪除很可能會帶來某種破壞。

默認情況下,所有的刪除操作都會附有 30 秒鍾的寬限期限。kubectl delete 命令支持 --grace-period=<seconds> 選項,允許你重載默認值,設定自己希望的期限值。

將寬限期限強制設置為 0 意味着立即從 API 服務器刪除 Pod。如果 Pod 仍然運行於某節點上,強制刪除操作會觸發 kubelet 立即執行清理操作。

說明: 你必須在設置 --grace-period=0 的同時額外設置 --force 參數才能發起強制刪除請求。

執行強制刪除操作時,API 服務器不再等待來自 kubelet 的、關於 Pod 已經在原來運行的節點上終止執行的確認消息。API 服務器直接刪除 Pod 對象,這樣新的與之同名的 Pod 即可以被創建。 在節點側,被設置為立即終止的 Pod 仍然會在被強行殺死之前獲得一點點的寬限時間。

失效 Pod 的垃圾收集

對於已失敗的 Pod 而言,對應的 API 對象仍然會保留在集群的 API 服務器上,直到用戶或者控制器進程顯式地將其刪除。

控制面組件會在 Pod 個數超出所配置的閾值(根據 kube-controller-manager 的 terminated-pod-gc-threshold 設置)時刪除已終止的 Pod(階段值為 Succeeded 或 Failed)。這一行為會避免隨着時間演進不斷創建和終止 Pod 而引起的資源泄露問題。

1.2 - Init 容器

本頁提供了 Init 容器的概覽。Init 容器是一種特殊容器,在 Pod 內的應用容器啟動之前運行。Init 容器可以包括一些應用鏡像中不存在的實用工具和安裝腳本。

你可以在 Pod 的規約中與用來描述應用容器的 containers 數組平行的位置指定 Init 容器。

理解 Init 容器

每個 Pod 中可以包含多個容器,應用運行在這些容器里面,同時 Pod 也可以有一個或多個先於應用容器啟動的 Init 容器。

Init 容器與普通的容器非常像,除了如下兩點:

  • 它們總是運行到完成。
  • 每個都必須在下一個啟動之前成功完成。

如果 Pod 的 Init 容器失敗,kubelet 會不斷地重啟該 Init 容器直到該容器成功為止。然而,如果 Pod 對應的 restartPolicy 值為 "Never",並且 Pod 的 Init 容器失敗,則 Kubernetes 會將整個 Pod 狀態設置為失敗。

為 Pod 設置 Init 容器需要在 Pod 規約中添加 initContainers 字段,該字段以 Container 類型對象數組的形式組織,和應用的 containers 數組同級相鄰。

Init 容器的狀態在 status.initContainerStatuses 字段中以容器狀態數組的格式返回(類似 status.containerStatuses 字段)。

與普通容器的不同之處

Init 容器支持應用容器的全部字段和特性,包括資源限制、數據卷和安全設置。然而,Init 容器對資源請求和限制的處理稍有不同,在下面資源節有說明。

同時 Init 容器不支持 lifecycle、livenessProbe、readinessProbe 和 startupProbe,因為它們必須在 Pod 就緒之前運行完成。

如果為一個 Pod 指定了多個 Init 容器,這些容器會按順序逐個運行。每個 Init 容器必須運行成功,下一個才能夠運行。當所有的 Init 容器運行完成時,Kubernetes 才會為 Pod 初始化應用容器並像平常一樣運行。

使用 Init 容器

因為 Init 容器具有與應用容器分離的單獨鏡像,其啟動相關代碼具有如下優勢:

  • Init 容器可以包含一些安裝過程中應用容器中不存在的實用工具或個性化代碼。例如,沒有必要僅為了在安裝過程中使用類似 sed、awk、python 或 dig 這樣的工具而去 FROM 一個鏡像來生成一個新的鏡像。

  • Init 容器可以安全地運行這些工具,避免這些工具導致應用鏡像的安全性降低。

  • 應用鏡像的創建者和部署者可以各自獨立工作,而沒有必要聯合構建一個單獨的應用鏡像。

  • Init 容器能以不同於 Pod 內應用容器的文件系統視圖運行。因此,Init 容器可以訪問應用容器不能訪問的 Secret 的權限。

  • 由於 Init 容器必須在應用容器啟動之前運行完成,因此 Init 容器提供了一種機制來阻塞或延遲應用容器的啟動,直到滿足了一組先決條件。一旦前置條件滿足,Pod 內的所有的應用容器會並行啟動。

示例

下面是一些如何使用 Init 容器的想法:

  • 等待一個 Service 完成創建,通過類似如下 shell 命令:

for i in {1..100}; do sleep 1; if dig myservice; then exit 0; fi; exit 1

  • 注冊這個 Pod 到遠程服務器,通過在命令中調用 API,類似如下:

curl -X POST http://$MANAGEMENT_SERVICE_HOST:$MANAGEMENT_SERVICE_PORT/register -d 'instance=$(<POD_NAME>)&ip=$(<POD_IP>)'

  • 在啟動應用容器之前等一段時間,使用類似命令:

sleep 60

  • 克隆 Git 倉庫到卷中。

  • 將配置值放到配置文件中,運行模板工具為主應用容器動態地生成配置文件。例如,在配置文件中存放 POD_IP 值,並使用 Jinja 生成主應用配置文件。

使用 Init 容器的情況

下面的例子定義了一個具有 2 個 Init 容器的簡單 Pod。第一個等待 myservice 啟動,第二個等待 mydb 啟動。一旦這兩個 Init容器 都啟動完成,Pod 將啟動 spec 節中的應用容器。

apiVersion: v1
kind: Pod
metadata:
  name: myapp-pod
  labels:
    app: myapp
spec:
  containers:
  - name: myapp-container
    image: busybox:1.28
    command: ['sh', '-c', 'echo The app is running! && sleep 3600']
  initContainers:
  - name: init-myservice
    image: busybox:1.28
    command: ['sh', '-c', "until nslookup myservice.$(cat /var/run/secrets/kubernetes.io/serviceaccount/namespace).svc.cluster.local; do echo waiting for myservice; sleep 2; done"]
  - name: init-mydb
    image: busybox:1.28
    command: ['sh', '-c', "until nslookup mydb.$(cat /var/run/secrets/kubernetes.io/serviceaccount/namespace).svc.cluster.local; do echo waiting for mydb; sleep 2; done"]

你通過運行下面的命令啟動 Pod:

kubectl apply -f myapp.yaml

輸出類似於:

pod/myapp-pod created

使用下面的命令檢查其狀態:

kubectl get -f myapp.yaml

輸出類似於:

NAME        READY     STATUS     RESTARTS   AGE
myapp-pod   0/1       Init:0/2   0          6m

或者查看更多詳細信息:

kubectl describe -f myapp.yaml

輸出類似於:

Name:          myapp-pod
Namespace:     default
[...]
Labels:        app=myapp
Status:        Pending
[...]
Init Containers:
  init-myservice:
[...]
    State:         Running
[...]
  init-mydb:
[...]
    State:         Waiting
      Reason:      PodInitializing
    Ready:         False
[...]
Containers:
  myapp-container:
[...]
    State:         Waiting
      Reason:      PodInitializing
    Ready:         False
[...]
Events:
  FirstSeen    LastSeen    Count    From                      SubObjectPath                           Type          Reason        Message
  ---------    --------    -----    ----                      -------------                           --------      ------        -------
  16s          16s         1        {default-scheduler }                                              Normal        Scheduled     Successfully assigned myapp-pod to 172.17.4.201
  16s          16s         1        {kubelet 172.17.4.201}    spec.initContainers{init-myservice}     Normal        Pulling       pulling image "busybox"
  13s          13s         1        {kubelet 172.17.4.201}    spec.initContainers{init-myservice}     Normal        Pulled        Successfully pulled image "busybox"
  13s          13s         1        {kubelet 172.17.4.201}    spec.initContainers{init-myservice}     Normal        Created       Created container with docker id 5ced34a04634; Security:[seccomp=unconfined]
  13s          13s         1        {kubelet 172.17.4.201}    spec.initContainers{init-myservice}     Normal        Started       Started container with docker id 5ced34a04634

如需查看 Pod 內 Init 容器的日志,請執行:

kubectl logs myapp-pod -c init-myservice # 查看第一個 Init 容器
kubectl logs myapp-pod -c init-mydb      # 查看第二個 Init 容器

在這一刻,Init 容器將會等待至發現名稱為 mydb 和 myservice 的 Service。

如下為創建這些 Service 的配置文件:

---
apiVersion: v1
kind: Service
metadata:
  name: myservice
spec:
  ports:
  - protocol: TCP
    port: 80
    targetPort: 9376
---
apiVersion: v1
kind: Service
metadata:
  name: mydb
spec:
  ports:
  - protocol: TCP
    port: 80
    targetPort: 9377

創建 mydb 和 myservice 服務的命令:

kubectl create -f services.yaml

輸出類似於:

service "myservice" created
service "mydb" created

這樣你將能看到這些 Init 容器執行完畢,隨后 my-app 的 Pod 進入 Running 狀態:

kubectl get -f myapp.yaml

輸出類似於:

NAME        READY     STATUS    RESTARTS   AGE
myapp-pod   1/1       Running   0          9m

這個簡單例子應該能為你創建自己的 Init 容器提供一些啟發。

具體行為

在 Pod 啟動過程中,每個 Init 容器會在網絡和數據卷初始化之后按順序啟動。kubelet 運行依據 Init 容器在 Pod 規約中的出現順序依次運行之。

每個 Init 容器成功退出后才會啟動下一個 Init 容器。如果某容器因為容器運行時的原因無法啟動,或以錯誤狀態退出,kubelet 會根據 Pod 的 restartPolicy 策略進行重試。然而,如果 Pod 的 restartPolicy 設置為 "Always",Init 容器失敗時會使用 restartPolicy 的 "OnFailure" 策略。

在所有的 Init 容器沒有成功之前,Pod 將不會變成 Ready 狀態。Init 容器的端口將不會在 Service 中進行聚集。正在初始化中的 Pod 處於 Pending 狀態,但會將狀況 Initializing 設置為 false。

如果 Pod 重啟,所有 Init 容器必須重新執行。

對 Init 容器規約的修改僅限於容器的 image 字段。更改 Init 容器的 image 字段,等同於重啟該 Pod。

因為 Init 容器可能會被重啟、重試或者重新執行,所以 Init 容器的代碼應該是冪等的。特別地,基於 emptyDirs 寫文件的代碼,應該對輸出文件可能已經存在做好准備。

Init 容器具有應用容器的所有字段。然而 Kubernetes 禁止使用 readinessProbe,因為 Init 容器不能定義不同於完成態(Completion)的就緒態(Readiness)。Kubernetes 會在校驗時強制執行此檢查。

在 Pod 上使用 activeDeadlineSeconds 和在容器上使用 livenessProbe 可以避免 Init 容器一直重復失敗。activeDeadlineSeconds 時間包含了 Init 容器啟動的時間。然而,如果用戶將他們的應用程序以 Job 方式部署,建議使用 activeDeadlineSeconds, 因為 activeDeadlineSeconds 在 Init 容器結束后仍有效果。如果你設置了 activeDeadlineSeconds,已經在正常運行的 Pod 會被殺死。

在 Pod 中的每個應用容器和 Init 容器的名稱必須唯一;與任何其它容器共享同一個名稱,會在校驗時拋出錯誤。

資源

在給定的 Init 容器執行順序下,資源使用適用於如下規則:

  • 所有 Init 容器上定義的任何特定資源的 limit 或 request 的最大值,作為 Pod 有效初始 request/limit。如果任何資源沒有指定資源限制,這被視為最高限制。
  • Pod 對資源的 有效 limit/request 是如下兩者的較大者:
    • 所有應用容器對某個資源的 limit/request 之和
    • 對某個資源的有效初始 limit/request
  • 基於有效 limit/request 完成調度,這意味着 Init 容器能夠為初始化過程預留資源,這些資源在 Pod 生命周期過程中並沒有被使用。
  • Pod 的 有效 QoS 層,與 Init 容器和應用容器的一樣。

配額和限制適用於有效 Pod 的請求和限制值。Pod 級別的 cgroups 是基於有效 Pod 的請求和限制值,和調度器相同。

Pod 重啟的原因

Pod 重啟會導致 Init 容器重新執行,主要有如下幾個原因:

  • Pod 的基礎設施容器 (譯者注:如 pause 容器) 被重啟。這種情況不多見,必須由具備 root 權限訪問節點的人員來完成。

  • 當 restartPolicy 設置為 "Always",Pod 中所有容器會終止而強制重啟。由於垃圾收集機制的原因,Init 容器的完成記錄將會丟失。

當 Init 容器的鏡像發生改變或者 Init 容器的完成記錄因為垃圾收集等原因被丟失時,Pod 不會被重啟。這一行為適用於 Kubernetes v1.20 及更新版本。如果你在使用較早版本的 Kubernetes,可查閱你所使用的版本對應的文檔。

1.3 - Pod 拓撲分布約束

FEATURE STATE: Kubernetes v1.19 [stable]

你可以使用拓撲分布約束(Topology Spread Constraints)來控制 Pods 在集群內故障域之間的分布,例如區域(Region)、可用區(Zone)、節點和其他用戶自定義拓撲域。這樣做有助於實現高可用並提升資源利用率。

說明: 在 v1.18 之前的 Kubernetes 版本中,如果要使用 Pod 拓撲擴展約束,你必須在 API 服務器和調度器中啟用 EvenPodsSpread 特性門控。

先決條件

節點標簽

拓撲分布約束依賴於節點標簽來標識每個節點所在的拓撲域。例如,某節點可能具有標簽:node=node1,zone=us-east-1a,region=us-east-1

假設你擁有具有以下標簽的一個 4 節點集群:

NAME    STATUS   ROLES    AGE     VERSION   LABELS
node1   Ready    <none>   4m26s   v1.16.0   node=node1,zone=zoneA
node2   Ready    <none>   3m58s   v1.16.0   node=node2,zone=zoneA
node3   Ready    <none>   3m17s   v1.16.0   node=node3,zone=zoneB
node4   Ready    <none>   2m43s   v1.16.0   node=node4,zone=zoneB

然后從邏輯上看集群如下:

image

你可以復用在大多數集群上自動創建和填充的常用標簽,而不是手動添加標簽。

Pod 的分布約束

API

pod.spec.topologySpreadConstraints 字段定義如下所示:

apiVersion: v1
kind: Pod
metadata:
  name: mypod
spec:
  topologySpreadConstraints:
    - maxSkew: <integer>
      topologyKey: <string>
      whenUnsatisfiable: <string>
      labelSelector: <object>

你可以定義一個或多個 topologySpreadConstraint 來指示 kube-scheduler 如何根據與現有的 Pod 的關聯關系將每個傳入的 Pod 部署到集群中。字段包括:

  • maxSkew 描述 Pod 分布不均的程度。這是給定拓撲類型中任意兩個拓撲域中匹配的 pod 之間的最大允許差值。它必須大於零。取決於 whenUnsatisfiable 的取值,其語義會有不同。
    • 當 whenUnsatisfiable 等於 "DoNotSchedule" 時,maxSkew 是目標拓撲域中匹配的 Pod 數與全局最小值之間可存在的差異。
    • 當 whenUnsatisfiable 等於 "ScheduleAnyway" 時,調度器會更為偏向能夠降低偏差值的拓撲域。
  • topologyKey 是節點標簽的鍵。如果兩個節點使用此鍵標記並且具有相同的標簽值,則調度器會將這兩個節點視為處於同一拓撲域中。調度器試圖在每個拓撲域中放置數量均衡的 Pod。
  • whenUnsatisfiable 指示如果 Pod 不滿足分布約束時如何處理:
    • DoNotSchedule(默認)告訴調度器不要調度。
    • ScheduleAnyway 告訴調度器仍然繼續調度,只是根據如何能將偏差最小化來對節點進行排序。
  • labelSelector 用於查找匹配的 pod。匹配此標簽的 Pod 將被統計,以確定相應拓撲域中 Pod 的數量。有關詳細信息,請參考標簽選擇算符。

你可以執行 kubectl explain Pod.spec.topologySpreadConstraints 命令以了解關於 topologySpreadConstraints 的更多信息。

例子:單個 TopologySpreadConstraint

假設你擁有一個 4 節點集群,其中標記為 foo:bar 的 3 個 Pod 分別位於 node1、node2 和 node3 中:

image

如果希望新來的 Pod 均勻分布在現有的可用區域,則可以按如下設置其規約:

kind: Pod
apiVersion: v1
metadata:
  name: mypod
  labels:
    foo: bar
spec:
  topologySpreadConstraints:
  - maxSkew: 1
    topologyKey: zone
    whenUnsatisfiable: DoNotSchedule
    labelSelector:
      matchLabels:
        foo: bar
  containers:
  - name: pause
    image: k8s.gcr.io/pause:3.1

topologyKey: zone 意味着均勻分布將只應用於存在標簽鍵值對為 "zone:<any value>" 的節點。whenUnsatisfiable: DoNotSchedule 告訴調度器如果新的 Pod 不滿足約束,則讓它保持懸決狀態。

如果調度器將新的 Pod 放入 "zoneA",Pods 分布將變為 [3, 1],因此實際的偏差為 2(3 - 1)。這違反了 maxSkew: 1 的約定。此示例中,新 Pod 只能放置在 "zoneB" 上:

image

或者

image

你可以調整 Pod 規約以滿足各種要求:

  • 將 maxSkew 更改為更大的值,比如 "2",這樣新的 Pod 也可以放在 "zoneA" 上。
  • 將 topologyKey 更改為 "node",以便將 Pod 均勻分布在節點上而不是區域中。在上面的例子中,如果 maxSkew 保持為 "1",那么傳入的 Pod 只能放在 "node4" 上。
  • 將 whenUnsatisfiable: DoNotSchedule 更改為 whenUnsatisfiable: ScheduleAnyway, 以確保新的 Pod 始終可以被調度(假設滿足其他的調度 API)。但是,最好將其放置在匹配 Pod 數量較少的拓撲域中。(請注意,這一優先判定會與其他內部調度優先級(如資源使用率等)排序准則一起進行標准化。)

例子:多個 TopologySpreadConstraints

下面的例子建立在前面例子的基礎上。假設你擁有一個 4 節點集群,其中 3 個標記為 foo:bar 的 Pod 分別位於 node1、node2 和 node3 上:

image

可以使用 2 個 TopologySpreadConstraint 來控制 Pod 在區域和節點兩個維度上的分布:

kind: Pod
apiVersion: v1
metadata:
  name: mypod
  labels:
    foo: bar
spec:
  topologySpreadConstraints:
  - maxSkew: 1
    topologyKey: zone
    whenUnsatisfiable: DoNotSchedule
    labelSelector:
      matchLabels:
        foo: bar
  - maxSkew: 1
    topologyKey: node
    whenUnsatisfiable: DoNotSchedule
    labelSelector:
      matchLabels:
        foo: bar
  containers:
  - name: pause
    image: k8s.gcr.io/pause:3.1

在這種情況下,為了匹配第一個約束,新的 Pod 只能放置在 "zoneB" 中;而在第二個約束中,新的 Pod 只能放置在 "node4" 上。最后兩個約束的結果加在一起,唯一可行的選擇是放置在 "node4" 上。

多個約束之間可能存在沖突。假設有一個跨越 2 個區域的 3 節點集群:

image

如果對集群應用 "two-constraints.yaml",會發現 "mypod" 處於 Pending 狀態。這是因為:為了滿足第一個約束,"mypod" 只能放在 "zoneB" 中,而第二個約束要求 "mypod" 只能放在 "node2" 上。Pod 調度無法滿足兩種約束。

為了克服這種情況,你可以增加 maxSkew 或修改其中一個約束,讓其使用 whenUnsatisfiable: ScheduleAnyway。

節點親和性與節點選擇器的相互作用 

如果 Pod 定義了 spec.nodeSelector 或 spec.affinity.nodeAffinity,調度器將從傾斜計算中跳過不匹配的節點。

假設你有一個跨越 zoneA 到 zoneC 的 5 節點集群:

image

而且你知道 "zoneC" 必須被排除在外。在這種情況下,可以按如下方式編寫 yaml,
以便將 "mypod" 放置在 "zoneB" 上,而不是 "zoneC" 上。同樣,spec.nodeSelector
也要一樣處理。

kind: Pod
apiVersion: v1
metadata:
  name: mypod
  labels:
    foo: bar
spec:
  topologySpreadConstraints:
  - maxSkew: 1
    topologyKey: zone
    whenUnsatisfiable: DoNotSchedule
    labelSelector:
      matchLabels:
        foo: bar
  affinity:
    nodeAffinity:
      requiredDuringSchedulingIgnoredDuringExecution:
        nodeSelectorTerms:
        - matchExpressions:
          - key: zone
            operator: NotIn
            values:
            - zoneC
  containers:
  - name: pause
    image: k8s.gcr.io/pause:3.1

調度器不會預先知道集群擁有的所有區域和其他拓撲域。拓撲域由集群中存在的節點確定。在自動伸縮的集群中,如果一個節點池(或節點組)的節點數量為零,而用戶正期望其擴容時,可能會導致調度出現問題。因為在這種情況下,調度器不會考慮這些拓撲域信息,因為它們是空的,沒有節點。

其他值得注意的語義

這里有一些值得注意的隱式約定:

  • 只有與新的 Pod 具有相同命名空間的 Pod 才能作為匹配候選者。

  • 調度器會忽略沒有 topologySpreadConstraints[*].topologyKey的節點。這意味着:

    • 位於這些節點上的 Pod 不影響 maxSkew 的計算。在上面的例子中,假設 "node1" 沒有標簽 "zone",那么 2 個 Pod 將被忽略,因此傳入的 Pod 將被調度到 "zoneA" 中。

    • 新的 Pod 沒有機會被調度到這類節點上。在上面的例子中,假設一個帶有標簽 {zone-typo: zoneC} 的 "node5" 加入到集群,它將由於沒有標簽鍵 "zone" 而被忽略。

  • 注意,如果新 Pod 的 topologySpreadConstraints[*].labelSelector 與自身的標簽不匹配,將會發生什么。在上面的例子中,如果移除新 Pod 上的標簽,Pod 仍然可以調度到 "zoneB",因為約束仍然滿足。然而,在調度之后,集群的不平衡程度保持不變。zoneA 仍然有 2 個帶有 {foo:bar} 標簽的 Pod,zoneB 有 1 個帶有 {foo:bar} 標簽的 Pod。因此,如果這不是你所期望的,建議工作負載的 topologySpreadConstraints[*].labelSelector 與其自身的標簽匹配。

集群級別的默認約束

為集群設置默認的拓撲分布約束也是可能的。默認拓撲分布約束在且僅在以下條件滿足時才會應用到 Pod 上:

  • Pod 沒有在其 .spec.topologySpreadConstraints 設置任何約束;
  • Pod 隸屬於某個服務、副本控制器、ReplicaSet 或 StatefulSet。

你可以在調度方案(Scheduling Profile)中將默認約束作為 PodTopologySpread 插件參數的一部分來設置。約束的設置采用如前所述的 API,只是 labelSelector 必須為空。選擇算符是根據 Pod 所屬的服務、副本控制器、ReplicaSet 或 StatefulSet 來設置的。

配置的示例可能看起來像下面這個樣子:

apiVersion: kubescheduler.config.k8s.io/v1beta1
kind: KubeSchedulerConfiguration

profiles:
  - pluginConfig:
      - name: PodTopologySpread
        args:
          defaultConstraints:
            - maxSkew: 1
              topologyKey: topology.kubernetes.io/zone
              whenUnsatisfiable: ScheduleAnyway
          defaultingType: List

說明:
默認調度約束所生成的評分可能與 SelectorSpread 插件. 所生成的評分有沖突。 建議你在為 PodTopologySpread 設置默認約束是禁用調度方案中的該插件。

內部默認約束

FEATURE STATE: Kubernetes v1.20 [beta]

當你使用了默認啟用的 DefaultPodTopologySpread 特性門控時,原來的 SelectorSpread 插件會被禁用。kube-scheduler 會使用下面的默認拓撲約束作為 PodTopologySpread 插件的配置:

defaultConstraints:
  - maxSkew: 3
    topologyKey: "kubernetes.io/hostname"
    whenUnsatisfiable: ScheduleAnyway
  - maxSkew: 5
    topologyKey: "topology.kubernetes.io/zone"
    whenUnsatisfiable: ScheduleAnyway

此外,原來用於提供等同行為的 SelectorSpread 插件也會被禁用。

說明:
如果你的節點不會同時設置 kubernetes.io/hostname 和 topology.kubernetes.io/zone 標簽,你應該定義自己的約束而不是使用 Kubernetes 的默認約束。

插件 PodTopologySpread 不會為未設置分布約束中所給拓撲鍵的節點評分。

如果你不想為集群使用默認的 Pod 分布約束,你可以通過設置 defaultingType 參數為 List 和 將 PodTopologySpread 插件配置中的 defaultConstraints 參數置空來禁用默認 Pod 分布約束。

apiVersion: kubescheduler.config.k8s.io/v1beta1
kind: KubeSchedulerConfiguration

profiles:
  - pluginConfig:
      - name: PodTopologySpread
        args:
          defaultConstraints: []
          defaultingType: List

與 PodAffinity/PodAntiAffinity 相比較

在 Kubernetes 中,與“親和性”相關的指令控制 Pod 的調度方式(更密集或更分散)。

  • 對於 PodAffinity,你可以嘗試將任意數量的 Pod 集中到符合條件的拓撲域中。
  • 對於 PodAntiAffinity,只能將一個 Pod 調度到某個拓撲域中。

要實現更細粒度的控制,你可以設置拓撲分布約束來將 Pod 分布到不同的拓撲域下,從而實現高可用性或節省成本。這也有助於工作負載的滾動更新和平穩地擴展副本規模。

已知局限性

  • Deployment 縮容操作可能導致 Pod 分布不平衡。
  • 具有污點的節點上的 Pods 也會被統計。參考 Issue 80921。

1.4 - 干擾(Disruptions)

本指南針對的是希望構建高可用性應用程序的應用所有者,他們有必要了解可能發生在 Pod 上的干擾類型。

文檔同樣適用於想要執行自動化集群操作(例如升級和自動擴展集群)的集群管理員。

自願干擾和非自願干擾

Pod 不會消失,除非有人(用戶或控制器)將其銷毀,或者出現了不可避免的硬件或軟件系統錯誤。

我們把這些不可避免的情況稱為應用的非自願干擾(Involuntary Disruptions)。例如:

  • 節點下層物理機的硬件故障
  • 集群管理員錯誤地刪除虛擬機(實例)
  • 雲提供商或虛擬機管理程序中的故障導致的虛擬機消失
  • 內核錯誤
  • 節點由於集群網絡隔離從集群中消失
  • 由於節點資源不足導致 pod 被驅逐。

除了資源不足的情況,大多數用戶應該都熟悉這些情況;它們不是特定於 Kubernetes 的。

我們稱其他情況為自願干擾(Voluntary Disruptions)。包括由應用程序所有者發起的操作和由集群管理員發起的操作。典型的應用程序所有者的操作包括:

  • 刪除 Deployment 或其他管理 Pod 的控制器
  • 更新了 Deployment 的 Pod 模板導致 Pod 重啟
  • 直接刪除 Pod(例如,因為誤操作)

集群管理員操作包括:

  • 排空(drain)節點進行修復或升級。
  • 從集群中排空節點以縮小集群(了解集群自動擴縮)。
  • 從節點中移除一個 Pod,以允許其他 Pod 使用該節點。

這些操作可能由集群管理員直接執行,也可能由集群管理員所使用的自動化工具執行,或者由集群托管提供商自動執行。

咨詢集群管理員或聯系雲提供商,或者查詢發布文檔,以確定是否為集群啟用了任何資源干擾源。 如果沒有啟用,可以不用創建 Pod Disruption Budgets(Pod 干擾預算)

注意: 並非所有的自願干擾都會受到 Pod 干擾預算的限制。例如,刪除 Deployment 或 Pod 的刪除操作就會跳過 Pod 干擾預算檢查。

處理干擾

以下是減輕非自願干擾的一些方法:

  • 確保 Pod 在請求中給出所需資源。
  • 如果需要更高的可用性,請復制應用程序。(了解有關運行多副本的無狀態和有狀態應用程序的信息。)
  • 為了在運行復制應用程序時獲得更高的可用性,請跨機架(使用反親和性或跨區域(如果使用多區域集群)擴展應用程序。

自願干擾的頻率各不相同。在一個基本的 Kubernetes 集群中,沒有自願干擾(只有用戶觸發的干擾)。然而,集群管理員或托管提供商可能運行一些可能導致自願干擾的額外服務。例如,節點軟更新可能導致自願干擾。另外,集群(節點)自動縮放的某些實現可能導致碎片整理和緊縮節點的自願干擾。集群管理員或托管提供商應該已經記錄了各級別的自願干擾(如果有的話)。有些配置選項,例如在 pod spec 中使用 PriorityClasses 也會產生自願(和非自願)的干擾。

Kubernetes 提供特性來滿足在出現頻繁自願干擾的同時運行高可用的應用程序。我們稱這些特性為干擾預算(Disruption Budget)。

干擾預算

FEATURE STATE: Kubernetes v1.21 [stable]

即使你會經常引入自願性干擾,Kubernetes 也能夠支持你運行高度可用的應用。

應用程序所有者可以為每個應用程序創建 PodDisruptionBudget 對象(PDB)。PDB 將限制在同一時間因自願干擾導致的復制應用程序中宕機的 pod 數量。例如,基於票選機制的應用程序希望確保運行的副本數永遠不會低於仲裁所需的數量。Web 前端可能希望確保提供負載的副本數量永遠不會低於總數的某個百分比。

集群管理員和托管提供商應該使用遵循 PodDisruptionBudgets 的接口(通過調用Eviction API),而不是直接刪除 Pod 或 Deployment。

例如,kubectl drain 命令可以用來標記某個節點即將停止服務。運行 kubectl drain 命令時,工具會嘗試驅逐機器上的所有 Pod。kubectl 所提交的驅逐請求可能會暫時被拒絕,所以該工具會定時重試失敗的請求,直到所有的 Pod 都被終止,或者達到配置的超時時間。

PDB 指定應用程序可以容忍的副本數量(相當於應該有多少副本)。例如,具有 .spec.replicas: 5 的 Deployment 在任何時間都應該有 5 個 Pod。如果 PDB 允許其在某一時刻有 4 個副本,那么驅逐 API 將允許同一時刻僅有一個而不是兩個 Pod 自願干擾。

使用標簽選擇器來指定構成應用程序的一組 Pod,這與應用程序的控制器(Deployment,StatefulSet 等) 選擇 Pod 的邏輯一樣。

Pod 控制器的 .spec.replicas 計算“預期的” Pod 數量。根據 Pod 對象的 .metadata.ownerReferences 字段來發現控制器。

PDB 不能阻止非自願干擾的發生,但是確實會計入預算。

由於應用程序的滾動升級而被刪除或不可用的 Pod 確實會計入干擾預算,但是控制器(如 Deployment 和 StatefulSet)在進行滾動升級時不受 PDB 的限制。應用程序更新期間的故障處理方式是在對應的工作負載資源的 spec 中配置的。

當使用驅逐 API 驅逐 Pod 時,Pod 會被體面地終止,期間會參考 PodSpec 中的 terminationGracePeriodSeconds 配置值。

PDB 例子

假設集群有 3 個節點,node-1 到 node-3。集群上運行了一些應用。其中一個應用有 3 個副本,分別是 pod-a,pod-b 和 pod-c。另外,還有一個不帶 PDB 的無關 pod pod-x 也同樣顯示出來。最初,所有的 Pod 分布如下:

node-1
node-2
node-3
pod-a available
pod-b available
pod-c available
pod-x available
 
 

3 個 Pod 都是 deployment 的一部分,並且共同擁有同一個 PDB,要求 3 個 Pod 中至少有 2 個 Pod 始終處於可用狀態。

例如,假設集群管理員想要重啟系統,升級內核版本來修復內核中的權限。集群管理員首先使用 kubectl drain 命令嘗試排空 node-1 節點。命令嘗試驅逐 pod-a 和 pod-x。操作立即就成功了。兩個 Pod 同時進入 terminating 狀態。這時的集群處於下面的狀態:

node-1 draining
node-2
node-3
pod-a terminating
pod-b available
pod-c available
pod-x terminating
 
 

Deployment 控制器觀察到其中一個 Pod 正在終止,因此它創建了一個替代 Pod pod-d。由於 node-1 被封鎖(cordon),pod-d 落在另一個節點上。同樣其他控制器也創建了 pod-y 作為 pod-x 的替代品。

注意: 對於 StatefulSet 來說,pod-a(也稱為 pod-0)需要在替換 Pod 創建之前完全終止,替代它的也稱為 pod-0,但是具有不同的 UID。除此之外,此示例也適用於 StatefulSet。)

當前集群的狀態如下:

node-1 draining
node-2
node-3
pod-a terminating
pod-b available
pod-c available
pod-x terminating
pod-d starting
pod-y

在某一時刻,Pod 被終止,集群如下所示:

node-1 drained
node-2
node-3
 
pod-b available
pod-c available
 
pod-d starting
pod-y

此時,如果一個急躁的集群管理員試圖排空(drain)node-2 或 node-3,drain 命令將被阻塞, 因為對於 Deployment 來說只有 2 個可用的 Pod,並且它的 PDB 至少需要 2 個。經過一段時間,pod-d 變得可用。

集群狀態如下所示:

node-1 drained
node-2
node-3
 
pod-b available
pod-c available
 
pod-d available
pod-y

現在,集群管理員試圖排空(drain)node-2。drain 命令將嘗試按照某種順序驅逐兩個 Pod,假設先是 pod-b,然后是 pod-d。命令成功驅逐 pod-b,但是當它嘗試驅逐 pod-d時將被拒絕,因為對於 Deployment 來說只剩一個可用的 Pod 了。

Deployment 創建 pod-b 的替代 Pod pod-e。因為集群中沒有足夠的資源來調度 pod-e,drain 命令再次阻塞。集群最終將是下面這種狀態:

node-1 drained
node-2
node-3
no node
 
pod-b available
pod-c available
pod-e pending
 
pod-d available
pod-y
 

此時,集群管理員需要增加一個節點到集群中以繼續升級操作。

可以看到 Kubernetes 如何改變干擾發生的速率,根據:

  • 應用程序需要多少個副本
  • 優雅關閉應用實例需要多長時間
  • 啟動應用新實例需要多長時間
  • 控制器的類型
  • 集群的資源能力

分離集群所有者和應用所有者角色

通常,將集群管理者和應用所有者視為彼此了解有限的獨立角色是很有用的。這種責任分離在下面這些場景下是有意義的:

  • 當有許多應用程序團隊共用一個 Kubernetes 集群,並且有自然的專業角色
  • 當第三方工具或服務用於集群自動化管理

Pod 干擾預算通過在角色之間提供接口來支持這種分離。

如果你的組織中沒有這樣的責任分離,則可能不需要使用 Pod 干擾預算。

如何在集群上執行干擾性操作

如果你是集群管理員,並且需要對集群中的所有節點執行干擾操作,例如節點或系統軟件升級,則可以使用以下選項

  • 接受升級期間的停機時間。
  • 故障轉移到另一個完整的副本集群。
    • 沒有停機時間,但是對於重復的節點和人工協調成本可能是昂貴的。
  • 編寫可容忍干擾的應用程序和使用 PDB。
    • 不停機。
    • 最小的資源重復。
    • 允許更多的集群管理自動化。
    • 編寫可容忍干擾的應用程序是棘手的,但對於支持容忍自願干擾所做的工作,和支持自動擴縮和容忍非自願干擾所做工作相比,有大量的重疊

1.5 - 臨時容器

FEATURE STATE: Kubernetes v1.22 [alpha]

本頁面概述了臨時容器:一種特殊的容器,該容器在現有 Pod 中臨時運行,以便完成用戶發起的操作,例如故障排查。你會使用臨時容器來檢查服務,而不是用它來構建應用程序。

警告:
臨時容器處於 Alpha 階段,不適用於生產環境集群。根據 Kubernetes 棄用政策,此 Alpha 功能將來可能發生重大變化或被完全刪除。

了解臨時容器

Pod 是 Kubernetes 應用程序的基本構建塊。由於 Pod 是一次性且可替換的,因此一旦 Pod 創建,就無法將容器加入到 Pod 中。取而代之的是,通常使用 Deployment 以受控的方式來刪除並替換 Pod。

有時有必要檢查現有 Pod 的狀態。例如,對於難以復現的故障進行排查。在這些場景中,可以在現有 Pod 中運行臨時容器來檢查其狀態並運行任意命令。

什么是臨時容器?

臨時容器與其他容器的不同之處在於,它們缺少對資源或執行的保證,並且永遠不會自動重啟,因此不適用於構建應用程序。臨時容器使用與常規容器相同的 ContainerSpec 節來描述,但許多字段是不兼容和不允許的。

  • 臨時容器沒有端口配置,因此像 ports,livenessProbe,readinessProbe 這樣的字段是不允許的。
  • Pod 資源分配是不可變的,因此 resources 配置是不允許的。
  • 有關允許字段的完整列表,請參見 EphemeralContainer 參考文檔。

臨時容器是使用 API 中的一種特殊的 ephemeralcontainers 處理器進行創建的,而不是直接添加到 pod.spec 段,因此無法使用 kubectl edit 來添加一個臨時容器。

與常規容器一樣,將臨時容器添加到 Pod 后,將不能更改或刪除臨時容器。

臨時容器的用途

當由於容器崩潰或容器鏡像不包含調試工具而導致 kubectl exec 無用時,臨時容器對於交互式故障排查很有用。

尤其是,Distroless 鏡像允許用戶部署最小的容器鏡像,從而減少攻擊面並減少故障和漏洞的暴露。由於 distroless 鏡像不包含 Shell 或任何的調試工具,因此很難單獨使用 kubectl exec 命令進行故障排查。

使用臨時容器時,啟用進程名字空間共享很有幫助,可以查看其他容器中的進程。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM