Pod、Service、Volume 和 Namespace 是 Kubernetes 集群中四大基本對象,它們能夠表示系統中部署的應用、工作負載、網絡和磁盤資源,共同定義了集群的狀態。Kubernetes 中很多其他的資源其實只對這些基本的對象進行了組合。
Pod 是 Kubernetes 集群中能夠被創建和管理的最小部署單元,想要徹底和完整的了解 Kubernetes 的實現原理,我們必須要清楚 Pod 的實現原理以及最佳實踐。
在這里,我們將分兩個部分對 Pod 進行解析,第一部分主要會從概念入手介紹 Pod 中必須了解的特性,而第二部分會介紹 Pod 從創建到刪除的整個生命周期內的重要事件在源碼層面是如何實現的。
概述
作為 Kubernetes 集群中的基本單元,Pod 就是最小並且最簡單的 Kubernetes 對象,這個簡單的對象其實就能夠獨立啟動一個后端進程並在集群的內部為調用方提供服務。在上一篇文章 從 Kubernetes 中的對象談起 中,我們曾經介紹過簡單的 Kubernetes Pod 是如何使用 YAML 進行描述的:
apiVersion: v1 kind: Pod metadata: name: busybox labels: app: busybox spec: containers: - image: busybox command: - sleep - "3600" imagePullPolicy: IfNotPresent name: busybox restartPolicy: Always
這個 YAML 文件描述了一個 Pod 啟動時運行的容器和命令以及它的重啟策略,在當前 Pod 出現錯誤或者執行結束后是否應該被 Kubernetes 的控制器拉起來,除了這些比較顯眼的配置之外,元數據 metadata
的配置也非常重要,name
是當前對象在 Kuberentes 集群中的唯一標識符,而標簽 labels
可以幫助我們快速選擇對象。
在同一個 Pod 中,有幾個概念特別值得關注,首先就是容器,在 Pod 中其實可以同時運行一個或者多個容器,這些容器能夠共享網絡、存儲以及 CPU、內存等資源。在這一小節中我們將關注 Pod 中的容器、卷和網絡三大概念。
容器
每一個 Kubernetes 的 Pod 其實都具有兩種不同的容器,兩種不同容器的職責其實十分清晰,一種是 InitContainer
,這種容器會在 Pod 啟動時運行,主要用於初始化一些配置,另一種是 Pod 在 Running 狀態時內部存活的 Container
,它們的主要作用是對外提供服務或者作為工作節點處理異步任務等等。
通過對不同容器類型的命名我們也可以看出,InitContainer
會比 Container
優先啟動,在 kubeGenericRuntimeManager.SyncPod
方法中會先后啟動兩種容器。
func (m *kubeGenericRuntimeManager) SyncPod(pod *v1.Pod, _ v1.PodStatus, podStatus *kubecontainer.PodStatus, pullSecrets []v1.Secret, backOff *flowcontrol.Backoff) (result kubecontainer.PodSyncResult) { // Step 1: Compute sandbox and container changes. // Step 2: Kill the pod if the sandbox has changed. // Step 3: kill any running containers in this pod which are not to keep. // Step 4: Create a sandbox for the pod if necessary. // ... // Step 5: start the init container. if container := podContainerChanges.NextInitContainerToStart; container != nil { msg, _ := m.startContainer(podSandboxID, podSandboxConfig, container, pod, podStatus, pullSecrets, podIP, kubecontainer.ContainerTypeInit) } // Step 6: start containers in podContainerChanges.ContainersToStart. for _, idx := range podContainerChanges.ContainersToStart { container := &pod.Spec.Containers[idx] msg, _ := m.startContainer(podSandboxID, podSandboxConfig, container, pod, podStatus, pullSecrets, podIP, kubecontainer.ContainerTypeRegular) } return }
通過分析私有方法 startContainer
的實現我們得出:容器的類型最終只會影響在 Debug 時創建的標簽,所以對於 Kubernetes 來說兩種容器的啟動和執行也就只有順序先后的不同。
卷
每一個 Pod 中的容器是可以通過 卷(Volume) 的方式共享文件目錄的,這些 Volume 能夠存儲持久化的數據;在當前 Pod 出現故障或者滾動更新時,對應 Volume 中的數據並不會被清除,而是會在 Pod 重啟后重新掛載到期望的文件目錄中:
kubelet.go 文件中的私有方法 syncPod
會調用 WaitForAttachAndMount
方法為等待當前 Pod 啟動需要的掛載文件:
func (vm *volumeManager) WaitForAttachAndMount(pod *v1.Pod) error { expectedVolumes := getExpectedVolumes(pod) uniquePodName := util.GetUniquePodName(pod) vm.desiredStateOfWorldPopulator.ReprocessPod(uniquePodName) wait.PollImmediate( podAttachAndMountRetryInterval, podAttachAndMountTimeout, vm.verifyVolumesMountedFunc(uniquePodName, expectedVolumes)) return nil }
我們會在 后面的章節 詳細地介紹 Kubernetes 中卷的創建、掛載是如何進行的,在這里我們需要知道的是卷的掛載是 Pod 啟動之前必須要完成的工作:
func (kl *Kubelet) syncPod(o syncPodOptions) error { // ... if !kl.podIsTerminated(pod) { kl.volumeManager.WaitForAttachAndMount(pod) } pullSecrets := kl.getPullSecretsForPod(pod) result := kl.containerRuntime.SyncPod(pod, apiPodStatus, podStatus, pullSecrets, kl.backOff) kl.reasonCache.Update(pod.UID, result) return nil }
在當前 Pod 的卷創建完成之后,就會調用上一節中提到的 SyncPod
公有方法繼續進行同步 Pod 信息和創建、啟動容器的工作。
網絡
同一個 Pod 中的多個容器會被共同分配到同一個 Host 上並且共享網絡棧,也就是說這些 Pod 能夠通過 localhost 互相訪問到彼此的端口和服務,如果使用了相同的端口也會發生沖突,同一個 Pod 上的所有容器會連接到同一個網絡設備上,這個網絡設備就是由 Pod Sandbox 中的沙箱容器在 RunPodSandbox
方法中啟動時創建的:
func (ds *dockerService) RunPodSandbox(ctx context.Context, r *runtimeapi.RunPodSandboxRequest) (*runtimeapi.RunPodSandboxResponse, error) { config := r.GetConfig() // Step 1: Pull the image for the sandbox. image := defaultSandboxImage // Step 2: Create the sandbox container. createConfig, _ := ds.makeSandboxDockerConfig(config, image) createResp, _ := ds.client.CreateContainer(*createConfig) resp := &runtimeapi.RunPodSandboxResponse{PodSandboxId: createResp.ID} ds.setNetworkReady(createResp.ID, false) // Step 3: Create Sandbox Checkpoint. ds.checkpointManager.CreateCheckpoint(createResp.ID, constructPodSandboxCheckpoint(config)) // Step 4: Start the sandbox container. ds.client.StartContainer(createResp.ID) // Step 5: Setup networking for the sandbox. cID := kubecontainer.BuildContainerID(runtimeName, createResp.ID) networkOptions := make(map[string]string) ds.network.SetUpPod(config.GetMetadata().Namespace, config.GetMetadata().Name, cID, config.Annotations, networkOptions) return resp, nil }
沙箱容器其實就是 pause
容器,上述方法引用的 defaultSandboxImage
其實就是官方提供的 k8s.gcr.io/pause:3.1
鏡像,這里會創建沙箱鏡像和檢查點並啟動容器。
每一個節點上都會由 Kubernetes 的網絡插件 Kubenet 創建一個基本的 cbr0
網橋並為每一個 Pod 創建 veth
虛擬網絡設備,同一個 Pod 中的所有容器就會通過這個網絡設備共享網絡,也就是能夠通過 localhost 互相訪問彼此暴露的端口和服務。
小結
Kubernetes 中的每一個 Pod 都包含多個容器,這些容器在通過 Kubernetes 創建之后就能共享網絡和存儲,這其實是 Pod 非常重要的特性,我們能通過這個特性構建比較復雜的服務拓撲和依賴關系。
生命周期
想要深入理解 Pod 的實現原理,最好最快的辦法就是從 Pod 的生命周期入手,通過理解 Pod 創建、重啟和刪除的原理我們最終就能夠系統地掌握 Pod 的生命周期與核心原理。
當 Pod 被創建之后,就會進入健康檢查狀態,當 Kubernetes 確定當前 Pod 已經能夠接受外部的請求時,才會將流量打到新的 Pod 上並繼續對外提供服務,在這期間如果發生了錯誤就可能會觸發重啟機制,在 Pod 被刪除之前都會觸發一個 PreStop
的鈎子,其中的方法之前完成之后 Pod 才會被刪除,接下來我們就會按照這里的順序依次介紹 Pod 『從生到死』的過程。
創建
Pod 的創建都是通過 SyncPod
來實現的,創建的過程大體上可以分為六個步驟:
- 計算 Pod 中沙盒和容器的變更;
- 強制停止 Pod 對應的沙盒;
- 強制停止所有不應該運行的容器;
- 為 Pod 創建新的沙盒;
- 創建 Pod 規格中指定的初始化容器;
- 依次創建 Pod 規格中指定的常規容器;
我們可以看到 Pod 的創建過程其實是比較簡單的,首先計算 Pod 規格和沙箱的變更,然后停止可能影響這一次創建或者更新的容器,最后依次創建沙盒、初始化容器和常規容器。
func (m *kubeGenericRuntimeManager) SyncPod(pod *v1.Pod, _ v1.PodStatus, podStatus *kubecontainer.PodStatus, pullSecrets []v1.Secret, backOff *flowcontrol.Backoff) (result kubecontainer.PodSyncResult) { podContainerChanges := m.computePodActions(pod, podStatus) if podContainerChanges.CreateSandbox { ref, _ := ref.GetReference(legacyscheme.Scheme, pod) } if podContainerChanges.KillPod { if podContainerChanges.CreateSandbox { m.purgeInitContainers(pod, podStatus) } } else { for containerID, containerInfo := range podContainerChanges.ContainersToKill { m.killContainer(pod, containerID, containerInfo.name, containerInfo.message, nil) } } } podSandboxID := podContainerChanges.SandboxID if podContainerChanges.CreateSandbox { podSandboxID, _, _ = m.createPodSandbox(pod, podContainerChanges.Attempt) } podSandboxConfig, _ := m.generatePodSandboxConfig(pod, podContainerChanges.Attempt) if container := podContainerChanges.NextInitContainerToStart; container != nil { msg, _ := m.startContainer(podSandboxID, podSandboxConfig, container, pod, podStatus, pullSecrets, podIP, kubecontainer.ContainerTypeInit) } for _, idx := range podContainerChanges.ContainersToStart { container := &pod.Spec.Containers[idx] msg, _ := m.startContainer(podSandboxID, podSandboxConfig, container, pod, podStatus, pullSecrets, podIP, kubecontainer.ContainerTypeRegular) } return }
簡化后的 SyncPod
方法的脈絡非常清晰,可以很好地理解整個創建 Pod 的工作流程;而初始化容器和常規容器被調用 startContainer
來啟動:
func (m *kubeGenericRuntimeManager) startContainer(podSandboxID string, podSandboxConfig *runtimeapi.PodSandboxConfig, container *v1.Container, pod *v1.Pod, podStatus *kubecontainer.PodStatus, pullSecrets []v1.Secret, podIP string, containerType kubecontainer.ContainerType) (string, error) { imageRef, _, _ := m.imagePuller.EnsureImageExists(pod, container, pullSecrets) // ... containerID, _ := m.runtimeService.CreateContainer(podSandboxID, containerConfig, podSandboxConfig) m.internalLifecycle.PreStartContainer(pod, container, containerID) m.runtimeService.StartContainer(containerID) if container.Lifecycle != nil && container.Lifecycle.PostStart != nil { kubeContainerID := kubecontainer.ContainerID{ Type: m.runtimeName, ID: containerID, } msg, _ := m.runner.Run(kubeContainerID, pod, container, container.Lifecycle.PostStart) } return "", nil }
在啟動每一個容器的過程中也都按照相同的步驟進行操作:
- 通過鏡像拉取器獲得當前容器中使用鏡像的引用;
- 調用遠程的
runtimeService
創建容器; - 調用內部的生命周期方法
PreStartContainer
為當前的容器設置分配的 CPU 等資源; - 調用遠程的
runtimeService
開始運行鏡像; - 如果當前的容器包含
PostStart
鈎子就會執行該回調;
每次 SyncPod
被調用時不一定是創建新的 Pod 對象,它還會承擔更新、刪除和同步 Pod 規格的職能,根據輸入的新規格執行相應的操作。
健康檢查
如果我們遵循 Pod 的最佳實踐,其實應該盡可能地為每一個 Pod 添加 livenessProbe
和 readinessProbe
的健康檢查,這兩者能夠為 Kubernetes 提供額外的存活信息,如果我們配置了合適的健康檢查方法和規則,那么就不會出現服務未啟動就被打入流量或者長時間未響應依然沒有重啟等問題。
在 Pod 被創建或者被移除時,會被加入到當前節點上的 ProbeManager
中,ProbeManager
會負責這些 Pod 的健康檢查:
func (kl *Kubelet) HandlePodAdditions(pods []*v1.Pod) { start := kl.clock.Now() for _, pod := range pods { kl.podManager.AddPod(pod) kl.dispatchWork(pod, kubetypes.SyncPodCreate, mirrorPod, start) kl.probeManager.AddPod(pod) } } func (kl *Kubelet) HandlePodRemoves(pods []*v1.Pod) { start := kl.clock.Now() for _, pod := range pods { kl.podManager.DeletePod(pod) kl.deletePod(pod) kl.probeManager.RemovePod(pod) } }
簡化后的 HandlePodAdditions
和 HandlePodRemoves
方法非常直白,我們可以直接來看 ProbeManager
如何處理不同節點的健康檢查。
每一個新的 Pod 都會被調用 ProbeManager
的AddPod
函數,這個方法會初始化一個新的 Goroutine 並在其中運行對當前 Pod 進行健康檢查:
func (m *manager) AddPod(pod *v1.Pod) { key := probeKey{podUID: pod.UID} for _, c := range pod.Spec.Containers { key.containerName = c.Name if c.ReadinessProbe != nil { key.probeType = readiness w := newWorker(m, readiness, pod, c) m.workers[key] = w go w.run() } if c.LivenessProbe != nil { key.probeType = liveness w := newWorker(m, liveness, pod, c) m.workers[key] = w go w.run() } } }
在執行健康檢查的過程中,Worker 只是負責根據當前 Pod 的狀態定期觸發一次 Probe
,它會根據 Pod 的配置分別選擇調用 Exec
、HTTPGet
或 TCPSocket
三種不同的 Probe
方式:
func (pb *prober) runProbe(probeType probeType, p *v1.Probe, pod *v1.Pod, status v1.PodStatus, container v1.Container, containerID kubecontainer.ContainerID) (probe.Result, string, error) { timeout := time.Duration(p.TimeoutSeconds) * time.Second if p.Exec != nil { command := kubecontainer.ExpandContainerCommandOnlyStatic(p.Exec.Command, container.Env) return pb.exec.Probe(pb.newExecInContainer(container, containerID, command, timeout)) } if p.HTTPGet != nil { scheme := strings.ToLower(string(p.HTTPGet.Scheme)) host := p.HTTPGet.Host port, _ := extractPort(p.HTTPGet.Port, container) path := p.HTTPGet.Path url := formatURL(scheme, host, port, path) headers := buildHeader(p.HTTPGet.HTTPHeaders) if probeType == liveness { return pb.livenessHttp.Probe(url, headers, timeout) } else { // readiness return pb.readinessHttp.Probe(url, headers, timeout) } } if p.TCPSocket != nil { port, _ := extractPort(p.TCPSocket.Port, container) host := p.TCPSocket.Host return pb.tcp.Probe(host, port, timeout) } return probe.Unknown, "", fmt.Errorf("Missing probe handler for %s:%s", format.Pod(pod), container.Name) }
Kubernetes 在 Pod 啟動后的 InitialDelaySeconds
時間內會等待 Pod 的啟動和初始化,在這之后會開始健康檢查,默認的健康檢查重試次數是三次,如果健康檢查正常運行返回了一個確定的結果,那么 Worker 就是記錄這次的結果,在連續失敗 FailureThreshold
次或者成功 SuccessThreshold
次,那么就會改變當前 Pod 的狀態,這也是為了避免由於服務不穩定帶來的抖動。
刪除
當 Kubelet 在 HandlePodRemoves
方法中接收到來自客戶端的刪除請求時,就會通過一個名為 deletePod
的私有方法中的 Channel 將這一事件傳遞給 PodKiller 進行處理:
func (kl *Kubelet) deletePod(pod *v1.Pod) error { kl.podWorkers.ForgetWorker(pod.UID) runningPods, _ := kl.runtimeCache.GetPods() runningPod := kubecontainer.Pods(runningPods).FindPod("", pod.UID) podPair := kubecontainer.PodPair{APIPod: pod, RunningPod: &runningPod} kl.podKillingCh <- &podPair return nil }
Kubelet 除了將事件通知給 PodKiller 之外,還需要將當前 Pod 對應的 Worker 從持有的 podWorkers
中刪除;PodKiller 其實就是 Kubelet 持有的一個 Goroutine,它會在后台持續運行並監聽來自 podKillingCh
的事件:
經過一系列的方法調用之后,最終調用容器運行時的 killContainersWithSyncResult
方法,這個方法會同步地殺掉當前 Pod 中全部的容器:
func (m *kubeGenericRuntimeManager) killContainersWithSyncResult(pod *v1.Pod, runningPod kubecontainer.Pod, gracePeriodOverride *int64) (syncResults []*kubecontainer.SyncResult) { containerResults := make(chan *kubecontainer.SyncResult, len(runningPod.Containers)) for _, container := range runningPod.Containers { go func(container *kubecontainer.Container) { killContainerResult := kubecontainer.NewSyncResult(kubecontainer.KillContainer, container.Name) m.killContainer(pod, container.ID, container.Name, "Need to kill Pod", gracePeriodOverride) containerResults <- killContainerResult }(container) } close(containerResults) for containerResult := range containerResults { syncResults = append(syncResults, containerResult) } return }
對於每一個容器來說,它們在被停止之前都會先調用 PreStop
的鈎子方法,讓容器中的應用程序能夠有時間完成一些未處理的操作,隨后調用遠程的服務停止運行的容器:
func (m *kubeGenericRuntimeManager) killContainer(pod *v1.Pod, containerID kubecontainer.ContainerID, containerName string, reason string, gracePeriodOverride *int64) error { containerSpec := kubecontainer.GetContainerSpec(pod, containerName); gracePeriod := int64(minimumGracePeriodInSeconds) switch { case pod.DeletionGracePeriodSeconds != nil: gracePeriod = *pod.DeletionGracePeriodSeconds case pod.Spec.TerminationGracePeriodSeconds != nil: gracePeriod = *pod.Spec.TerminationGracePeriodSeconds } m.executePreStopHook(pod, containerID, containerSpec, gracePeriod m.internalLifecycle.PreStopContainer(containerID.ID) m.runtimeService.StopContainer(containerID.ID, gracePeriod) m.containerRefManager.ClearRef(containerID) return err }
從這個簡化版本的 killContainer
方法中,我們可以大致看出停止運行容器的大致邏輯,先從 Pod 的規格中計算出當前停止所需要的時間,然后運行鈎子方法和內部的生命周期方法,最后將容器停止並清除引用。
總結
在這篇文章中,我們已經介紹了 Pod 中的幾個重要概念 — 容器、卷和網絡以及從創建到刪除整個過程是如何實現的。
Kubernetes 中 Pod 的運行和管理總是與 kubelet 以及它的組件密不可分,后面的文章中也會介紹 kubelet 究竟是什么,它在整個 Kubernetes 中扮演什么樣的角色。
更多參考:https://draveness.me/kubernetes-pod
容器編排
- 談 Kubernetes 的架構設計與實現原理
- 從 Kubernetes 中的對象談起
- 詳解 Kubernetes Pod 的實現原理
- 詳解 Kubernetes Service 的實現原理
- 詳解 Kubernetes Volume 的實現原理
- 詳解 Kubernetes ReplicaSet 的實現原理
- 詳解 Kubernetes Deployment 的實現原理
- 詳解 Kubernetes StatefulSet 的實現原理
- 詳解 Kubernetes DaemonSet 的實現原理
- 詳解 Kubernetes Job 和 CronJob 的實現原理
- 詳解 Kubernetes 垃圾收集器 的實現原理