1. 程式人生 > 其它 >[K8s]Kubernetes-工作負載(上)

[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(掛斗)”容器負責從遠端更新這些檔案,如下圖所示:

有些 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 被建立時,這個相關的物件(例如這裡的卷)也會被刪除並重建。

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

然後從邏輯上看叢集如下:

你可以複用在大多數叢集上自動建立和填充的常用標籤,而不是手動新增標籤。

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 中:

如果希望新來的 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" 上:

或者

你可以調整 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 上:

可以使用 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 節點叢集:

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

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

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

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

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

而且你知道 "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-aavailable pod-bavailable pod-cavailable
pod-xavailable

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

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

node-1draining node-2 node-3
pod-aterminating pod-bavailable pod-cavailable
pod-xterminating

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-1draining node-2 node-3
pod-aterminating pod-bavailable pod-cavailable
pod-xterminating pod-dstarting pod-y

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

node-1drained node-2 node-3
pod-bavailable pod-cavailable
pod-dstarting pod-y

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

叢集狀態如下所示:

node-1drained node-2 node-3
pod-bavailable pod-cavailable
pod-davailable 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-1drained node-2 node-3 no node
pod-bavailable pod-cavailable pod-epending
pod-davailable 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 命令進行故障排查。

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