Kubernetes Pod 設計與實現 - Pause 容器

Pause 容器

Pod 是 Kubernetes 中應用運行的最小單位 (同時也是頂級資源),也稱爲容器組,單個 Pod 可以看作是一個邏輯獨立的主機,擁有自己的 IP、主機名稱、進程等。

每個節點上面可以運行多個 Pod, 每個 Pod 中可以運行多個容器,那麼單個 Pod 中的容器是如何共享同一個 IPC、Network 等資源的呢?這就需要使用到本文的主角: Pause 容器。

Pause 容器是一個基礎容器,顧名思義 (該容器進程會一直處於 “暫停” 狀態),主要作用是將相關容器集中到一起,並通過讓內部容器加入其命名空間,最終形成邏輯上的容器組。其次,在啓用共享 PID 命名空間的情況下,它作爲每個 Pod 中第一個啓動的進程 (init 進程) 接收並處理殭屍進程 (殭屍進程最壞情況下會耗盡內存)。

當 Pod 中的容器重啓之後,需要位於和重啓之前相同的命名空間中,因爲容器的生命週期和 Pod 進行了綁定,所以 Pause 容器可以使這個過程變得非常快捷方便。也就是說,同一個 Pod 中的所有容器看到的 Namespace 視圖是一樣的。

Pause 容器和 Pod 的生命週期是一樣的,Pause 容器從 Pod 創建調度後開始運行 (Pod 中第一個啓動的容器),一直到 Pod 被刪除後退出,如果在這期間 Pod 被驅逐或關閉,Kubernetes 會重新創建 Pod 並自動運行 Pause 容器

示例

上面的圖中展示了一個典型的 Pod 結構,其中包含了 3 個容器:

  1. Pause 容器,負責構建 Pod 的 Namespace 基礎

  2. nginx 容器,加入到 Pause 容器的 Namespace 中,共享資源

  3. ghost 容器,加入到 Pause 容器的 Namespace 中,共享資源


源碼實現

Pause 官方容器使用 C 語言編寫,Github 地址爲: https://github.com/kubernetes/kubernetes/tree/master/build/pause, 源代碼只有短短十幾行,本文以 Linux 實現爲例學習 Pause 容器的實現。

linux 目錄下面只有兩個文件, pause.c, orphan.c, 其中 orphan.c 文件中的代碼只是模擬了一個殭屍進程用於測試,這裏不做分析。

pause.c

Pause 容器的核心實現代碼:

// 引入各種頭文件

// 信號註冊處理函數 (SIGINT, SIGTERM)
// 這種屬於正常退出情況,所以退出碼爲 0
static void sigdown(int signo) {
  psignal(signo, "Shutting down, got signal");
  exit(0);
}

// 信號註冊處理函數 (SIGCHLD)
// 由操作系統發送給父進程,通知子進程的狀態變化
//   子進程終止:
//   子進程暫停或恢復運行:
static void sigreap(int signo) {
  // 等待指定的子進程退出

  // 函數原型:
  //   pid_t waitpid(pid_t pid, int *status, int options);

  // 參數說明:
  //   -1: 任意子進程
  //   NULL: 子進程的退出狀態無需存儲
  //   WNOHANG: 非阻塞,沒有找到子進程時直接返回 0
  while (waitpid(-1, NULL, WNOHANG) > 0)
    ;
  // 如果子進程的數量大於 0, 無限循環
}

int main(int argc, char **argv) {
  // 打印版本號 ...

  if (getpid() != 1)
    // 如果 Pause 進程 ID 不等於 1, 輸出警告信息

    // 因爲進程 ID 等於 1 是 init 進程,負責接收處理殭屍進程
    // 所以如果 Pause 進程 ID 不等於 1, 可能產生殭屍進程的堆積
    fprintf(stderr, "Warning: pause should be the first process\n");

  // 註冊 SIGINT 信號 (Ctrl + C)
  if (sigaction(SIGINT, &(struct sigaction){.sa_handler = sigdown}, NULL) < 0)
    return 1;
  // 註冊 SIGTERM 信號 (例如執行 kill, systemctl stop 等命令)
  if (sigaction(SIGTERM, &(struct sigaction){.sa_handler = sigdown}, NULL) < 0)
    return 2;
  // 註冊 SIGCHLD 信號
  if (sigaction(SIGCHLD, &(struct sigaction){.sa_handler = sigreap,
                                             .sa_flags = SA_NOCLDSTOP},
                NULL) < 0)
    return 3;

  // 無限循環
  for (;;)
    // pause 函數可以使當前進程陷入睡眠狀態,避免浪費 CPU 資源
    // 直到捕獲到一個信號後被喚醒
    pause();

  // 爲什麼退出碼是 42 ? 因爲作者比較任性 ?
  // 感興趣的讀者可以看看回答
  // https://stackoverflow.com/questions/16236182/what-is-the-origin-of-magic-number-42-indispensable-in-coding
  return 42;
}

通過上面的源代碼可以看到,Pause 容器的本質就是一個獨立的進程,該進程作爲容器內第一個進程啓動之後變身爲父進程,後續啓動的進程都會成爲該進程的子進程, 父進程通過信號量的變化來執行對應的操作。Pause 容器唯一的作用是 保證即使 Pod 中沒有任何容器運行也不會被刪除,因爲這時候還有 Pause 容器在運行。


Kubernetes Pod 啓動流程

對 Pause 容器 (進程) 有了基礎認識之後,我們可以看看 Kubernetes 中的 Pod 是如何通過 Pause 進程來管理子容器 (進程) 的, Pod 中的容器可以大致分類爲:

  1. 臨時容器 (一般用於 debug)

  2. init 容器

  3. 業務容器

Pod 創建的主幹代碼位於 kubeGenericRuntimeManager 對象的 SyncPod 方法中,下面結合具體的代碼做一個簡單的註解。

// https://github.com/kubernetes/kubernetes/blob/f8a4e343a106a73145464e8de8a919d13b59d25a/pkg/kubelet/kuberuntime/kuberuntime_manager.go#L1053

// SyncPod 方法通過執行下列步驟使 Pod 運行並達到預期的狀態
//
//  1. 計算 Pause 容器和其他容器的狀態變化
//  2. 清理 Pause 容器
//  3. 清理還在運行中,但是狀態不合理的容器 (也就是早應該退出的容器)
//  4. 創建 Pause 容器
//  5. 創建臨時容器
//  6. 創建 init 容器
//  7. 調整運行容器的狀態
//  8. 創建具體的業務容器
func (m *kubeGenericRuntimeManager) SyncPod(ctx context.Context, ...) (result kubecontainer.PodSyncResult) {
 // Step 1: 計算 Pause 容器和其他容器的狀態變化
 podContainerChanges := m.computePodActions(ctx, pod, podStatus)

 ...

 // Step 2: 如果 Pause 容器發生了變化,結束 Pod
 if podContainerChanges.KillPod {
  ...

 } else {
  // Step 3: 結束 Pod 內所有不需要運行的容器
  for containerID, containerInfo := range podContainerChanges.ContainersToKill {
     ...
  }
 }

 // 默認使用參數 Pod 狀態對象的 IP 地址作爲 Pod 的 IP
 var podIPs []string
 if podStatus != nil {
  podIPs = podStatus.IPs
 }

 // Step 4: 創建 Pause 容器
 podSandboxID := podContainerChanges.SandboxID
 if podContainerChanges.CreateSandbox {
  podSandboxID, msg, err = m.createPodSandbox(ctx, pod, podContainerChanges.Attempt)
  ...
 }

 ...

 // 針對各種類型容器的通用啓動函數
 // 參數說明:
 //   typeName: 容器類型 (例如臨時容器,init 容器等)
 //   metricLabel: 容器的標籤,主要用於 metric 採集
 start := func(ctx context.Context, typeName, metricLabel string, spec *startSpec) error {
  ...

  // 啓動容器
  // 真正啓動容器的方法
  if msg, err := m.startContainer(ctx, ...); err != nil {
   ...
  }

  return nil
 }

 // Step 5: 創建臨時容器
 for _, idx := range podContainerChanges.EphemeralContainersToStart {
  start(ctx, "ephemeral container", ...)
 }

 if !utilfeature.DefaultFeatureGate.Enabled(features.SidecarContainers) {
  // Step 6: 創建單個 init 容器
  if container := podContainerChanges.NextInitContainerToStart; container != nil {
   if err := start(ctx, "init container", ...); err != nil {
    ...
   }
  }
 } else {
  // Step 6: 創建多個 init 容器
  for _, idx := range podContainerChanges.InitContainersToStart {
   container := &pod.Spec.InitContainers[idx]
   if err := start(ctx, "init container", ...); err != nil {
      ...
   }
  }
 }

 // Step 7: 調整運行容器的狀態
 if isInPlacePodVerticalScalingAllowed(pod) {
    ...
 }

 // Step 8: 創建具體的業務容器
 for _, idx := range podContainerChanges.ContainersToStart {
  start(ctx, "container", ...)
 }

 return
}

上面的 SyncPod 方法主要完成了 Pod 的創建和 Pod 內各種容器的創建,從方法內部的調用可以看到,真正啓動容器的操作實現在 startContainer 方法中:

// startContainer 方法啓動容器並返回執行結果
// 主要分爲如下幾個步驟:
//   1. 拉取鏡像
//   2. 創建容器
//   3. 啓動容器
//   4. 執行生命週期事件鉤子函數 (容器啓動後函數)
func (m *kubeGenericRuntimeManager) startContainer(ctx context.Context, ...) (string, error) {
 container := spec.container

 // Step 1: 拉取鏡像
 imageRef, msg, err := m.imagePuller.EnsureImageExists(ctx, ...)

 ...

 // Step 2: 創建容器
 // 新容器的重啓次數爲 0
 // 舊容器的重啓次數遞增
 restartCount := 0
 containerStatus := podStatus.FindContainerStatusByName(container.Name)
 if containerStatus != nil {
  restartCount = containerStatus.RestartCount + 1
 } else {
    ...
 }

 ...

 // 爲容器設置資源限制

 ...

 // 生成容器配置
 containerConfig, cleanupAction, err := m.generateContainerConfig(ctx, ...)

 ...

 // Step 3: 啓動容器
 err = m.runtimeService.StartContainer(ctx, containerID)

 ...

 // 將容器日誌文件通過軟鏈接 連接到遺留的歷史容器日誌位置
 if _, err := m.osInterface.Stat(containerLog); !os.IsNotExist(err) {
     ...
 }

 // Step 4: 執行生命週期事件鉤子函數 (容器啓動後函數)
 if container.Lifecycle != nil && container.Lifecycle.PostStart != nil {
  msg, handlerErr := m.runner.Run(ctx, ...)
  if handlerErr != nil {
   // 如果鉤子函數執行失敗,殺死容器
   if err := m.killContainer(ctx, ...); err != nil {
       ...
   }
   return msg, ErrPostStartHook
  }
 }

 return "", nil
}

到這裏,容器的創建和啓動代碼就分析完了。

Reference

擴展閱讀

鏈接

[1]

pause(2) - Linux man page: https://linux.die.net/man/2/pause

[2]

waitpid(2) - Linux man page: https://linux.die.net/man/2/waitpid

[3]

Docker 網絡原理概覽: https://dbwu.tech/posts/docker_network/

[4]

Kubernetes 應用最佳實踐 - init 容器和鉤子函數: https://dbwu.tech/posts/k8s/best_practice/init_container/

[5]

The Almighty Pause Container: https://www.ianlewis.org/en/almighty-pause-container

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/uAKTP7MwUO5ZcqhZDKG2Ug