詳解 Kubernetes Pod 的實現原理

Pod、Service、Volume 和 Namespace 是 Kubernetes 集羣中四大基本對象,它們可以表示系統中部署的應用、工做負載、網絡和磁盤資源,共同定義了集羣的狀態。Kubernetes 中不少其餘的資源其實只對這些基本的對象進行了組合。docker

kubernetes-basic-objects

Pod 是 Kubernetes 集羣中可以被建立和管理的最小部署單元,想要完全和完整的瞭解 Kubernetes 的實現原理,咱們必需要清楚 Pod 的實現原理以及最佳實踐。後端

在這裏,咱們將分兩個部分對 Pod 進行解析,第一部分主要會從概念入手介紹 Pod 中必須瞭解的特性,而第二部分會介紹 Pod 從建立到刪除的整個生命週期內的重要事件在源碼層面是如何實現的。api

概述

做爲 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 中的容器、卷和網絡三大概念。app

容器

每個 Kubernetes 的 Pod 其實都具備兩種不一樣的容器,兩種不一樣容器的職責其實十分清晰,一種是 InitContainer,這種容器會在 Pod 啓動時運行,主要用於初始化一些配置,另外一種是 Pod 在 Running 狀態時內部存活的 Container,它們的主要做用是對外提供服務或者做爲工做節點處理異步任務等等。異步

kubernetes-pod-init-and-regular-containers

經過對不一樣容器類型的命名咱們也能夠看出,InitContainer 會比 Container 優先啓動,在 kubeGenericRuntimeManager.SyncPod 方法中會前後啓動兩種容器。tcp

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 來講兩種容器的啓動和執行也就只有順序前後的不一樣。ide

每個 Pod 中的容器是能夠經過 卷(Volume) 的方式共享文件目錄的,這些 Volume 可以存儲持久化的數據;在當前 Pod 出現故障或者滾動更新時,對應 Volume 中的數據並不會被清除,而是會在 Pod 重啓後從新掛載到指望的文件目錄中:函數

kubernetes-containers-share-volumes

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-pod-network

每個節點上都會由 Kubernetes 的網絡插件 Kubenet 建立一個基本的 cbr0 網橋併爲每個 Pod 建立 veth虛擬網絡設備,同一個 Pod 中的全部容器就會經過這個網絡設備共享網絡,也就是可以經過 localhost 互相訪問彼此暴露的端口和服務。

小結

Kubernetes 中的每個 Pod 都包含多個容器,這些容器在經過 Kubernetes 建立以後就能共享網絡和存儲,這實際上是 Pod 很是重要的特性,咱們能經過這個特性構建比較複雜的服務拓撲和依賴關係。

生命週期

想要深刻理解 Pod 的實現原理,最好最快的辦法就是從 Pod 的生命週期入手,經過理解 Pod 建立、重啓和刪除的原理咱們最終就可以系統地掌握 Pod 的生命週期與核心原理。

kubernetes-pod-lifecycle

當 Pod 被建立以後,就會進入健康檢查狀態,當 Kubernetes 肯定當前 Pod 已經可以接受外部的請求時,纔會將流量打到新的 Pod 上並繼續對外提供服務,在這期間若是發生了錯誤就可能會觸發重啓機制,在 Pod 被刪除以前都會觸發一個 PreStop 的鉤子,其中的方法以前完成以後 Pod 纔會被刪除,接下來咱們就會按照這裏的順序依次介紹 Pod 『從生到死』的過程。

建立

Pod 的建立都是經過 SyncPod 來實現的,建立的過程大致上能夠分爲六個步驟:

  1. 計算 Pod 中沙盒和容器的變動;
  2. 強制中止 Pod 對應的沙盒;
  3. 強制中止全部不該該運行的容器;
  4. 爲 Pod 建立新的沙盒;
  5. 建立 Pod 規格中指定的初始化容器;
  6. 依次建立 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 } 

在啓動每個容器的過程當中也都按照相同的步驟進行操做:

  1. 經過鏡像拉取器得到當前容器中使用鏡像的引用;
  2. 調用遠程的 runtimeService 建立容器;
  3. 調用內部的生命週期方法 PreStartContainer 爲當前的容器設置分配的 CPU 等資源;
  4. 調用遠程的 runtimeService 開始運行鏡像;
  5. 若是當前的容器包含 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 如何處理不一樣節點的健康檢查。

kubernetes-probe-manager

每個新的 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 的配置分別選擇調用 ExecHTTPGet 或 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 的事件:

kubernetes-pod-killer

通過一系列的方法調用以後,最終調用容器運行時的 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

容器編排

相關文章
相關標籤/搜索