kubectl 建立 Pod 背後到底發生了什麼?

原文連接:kubectl 建立 Pod 背後到底發生了什麼?node

想象一下,若是我想將 nginx 部署到 Kubernetes 集羣,我可能會在終端中輸入相似這樣的命令:nginx

$ kubectl run --image=nginx --replicas=3
複製代碼

而後回車。幾秒鐘後,你就會看到三個 nginx pod 分佈在全部的工做節點上。這一切就像變魔術同樣,但你並不知道這一切的背後究竟發生了什麼事情。git

Kubernetes 的神奇之處在於:它能夠經過用戶友好的 API 來處理跨基礎架構的 deployments,而背後的複雜性被隱藏在簡單的抽象中。但爲了充分理解它爲咱們提供的價值,咱們須要理解它的內部原理。github

本指南將引導您理解從 client 到 Kubelet 的請求的完整生命週期,必要時會經過源代碼來講明背後發生了什麼。web

這是一份能夠在線修改的文檔,若是你發現有什麼能夠改進或重寫的,歡迎提供幫助!算法

1. kubectl

驗證和生成器

當敲下回車鍵之後,kubectl 首先會執行一些客戶端驗證操做,以確保不合法的請求(例如,建立不支持的資源或使用格式錯誤的鏡像名稱)將會快速失敗,也不會發送給 kube-apiserver。經過減小沒必要要的負載來提升系統性能。shell

驗證經過以後, kubectl 開始將發送給 kube-apiserver 的 HTTP 請求進行封裝。kube-apiserver 與 etcd 進行通訊,全部嘗試訪問或更改 Kubernetes 系統狀態的請求都會經過 kube-apiserver 進行,kubectl 也不例外。kubectl 使用生成器(generators)來構造 HTTP 請求。生成器是一個用來處理序列化的抽象概念。json

經過 kubectl run 不只能夠運行 deployment,還能夠經過指定參數 --generator 來部署其餘多種資源類型。若是沒有指定 --generator 參數的值,kubectl 將會自動判斷資源的類型。api

例如,帶有參數 --restart-policy=Always 的資源將被部署爲 Deployment,而帶有參數 --restart-policy=Never 的資源將被部署爲 Pod。同時 kubectl 也會檢查是否須要觸發其餘操做,例如記錄命令(用來進行回滾或審計)。緩存

在 kubectl 判斷出要建立一個 Deployment 後,它將使用 DeploymentV1Beta1 生成器從咱們提供的參數中生成一個運行時對象

API 版本協商與 API 組

爲了更容易地消除字段或者從新組織資源結構,Kubernetes 支持多個 API 版本,每一個版本都在不一樣的 API 路徑下,例如 /api/v1 或者 /apis/extensions/v1beta1。不一樣的 API 版本代表不一樣的穩定性和支持級別,更詳細的描述能夠參考 Kubernetes API 概述

API 組旨在對相似資源進行分類,以便使得 Kubernetes API 更容易擴展。API 的組名在 REST 路徑或者序列化對象的 apiVersion 字段中指定。例如,Deployment 的 API 組名是 apps,最新的 API 版本是 v1beta2,這就是爲何你要在 Deployment manifests 頂部輸入 apiVersion: apps/v1beta2

kubectl 在生成運行時對象後,開始爲它找到適當的 API 組和 API 版本,而後組裝成一個版本化客戶端,該客戶端知道資源的各類 REST 語義。該階段被稱爲版本協商,kubectl 會掃描 remote API 上的 /apis 路徑來檢索全部可能的 API 組。因爲 kube-apiserver 在 /apis 路徑上公開了 OpenAPI 格式的規範文檔, 所以客戶端很容易找到合適的 API。

爲了提升性能,kubectl 將 OpenAPI 規範緩存到了 ~/.kube/cache 目錄。若是你想了解 API 發現的過程,請嘗試刪除該目錄並在運行 kubectl 命令時將 -v 參數的值設爲最大值,而後你將會看到全部試圖找到這些 API 版本的HTTP 請求。參考 kubectl 備忘單

最後一步纔是真正地發送 HTTP 請求。一旦請求發送以後得到成功的響應,kubectl 將會根據所需的輸出格式打印 success message。

客戶端身份認證

在發送 HTTP 請求以前還要進行客戶端認證,這是以前沒有提到的,如今能夠來看一下。

爲了可以成功發送請求,kubectl 須要先進行身份認證。用戶憑證保存在 kubeconfig 文件中,kubectl 經過如下順序來找到 kubeconfig 文件:

  • 若是提供了 --kubeconfig 參數, kubectl 就使用 --kubeconfig 參數提供的 kubeconfig 文件。
  • 若是沒有提供 --kubeconfig 參數,但設置了環境變量 $KUBECONFIG,則使用該環境變量提供的 kubeconfig 文件。
  • 若是 --kubeconfig 參數和環境變量 $KUBECONFIG 都沒有提供,kubectl 就使用默認的 kubeconfig 文件 $HOME/.kube/config

解析完 kubeconfig 文件後,kubectl 會肯定當前要使用的上下文、當前指向的羣集以及與當前用戶關聯的任何認證信息。若是用戶提供了額外的參數(例如 --username),則優先使用這些參數覆蓋 kubeconfig 中指定的值。一旦拿到這些信息以後, kubectl 就會把這些信息填充到將要發送的 HTTP 請求頭中:

  • x509 證書使用 tls.TLSConfig 發送(包括 CA 證書)。
  • bearer tokens 在 HTTP 請求頭 Authorization發送
  • 用戶名和密碼經過 HTTP 基本認證發送
  • OpenID 認證過程是由用戶事先手動處理的,產生一個像 bearer token 同樣被髮送的 token。

2. kube-apiserver

認證

如今咱們的請求已經發送成功了,接下來將會發生什麼?這時候就該 kube-apiserver 閃亮登場了!kube-apiserver 是客戶端和系統組件用來保存和檢索集羣狀態的主要接口。爲了執行相應的功能,kube-apiserver 須要可以驗證請求者是合法的,這個過程被稱爲認證。

那麼 apiserver 如何對請求進行認證呢?當 kube-apiserver 第一次啓動時,它會查看用戶提供的全部 CLI 參數,並組合成一個合適的令牌列表。

**舉個例子:**若是提供了 --client-ca-file 參數,則會將 x509 客戶端證書認證添加到令牌列表中;若是提供了 --token-auth-file 參數,則會將 breaer token 添加到令牌列表中。

每次收到請求時,apiserver 都會經過令牌鏈進行認證,直到某一個認證成功爲止

若是認證失敗,則請求失敗並返回相應的錯誤信息;若是驗證成功,則將請求中的 Authorization 請求頭刪除,並將用戶信息添加到其上下文中。這給後續的受權和准入控制器提供了訪問以前創建的用戶身份的能力。

受權

OK,如今請求已經發送,而且 kube-apiserver 已經成功驗證咱們是誰,終於解脫了!

然而事情並無結束,雖然咱們已經證實了咱們是合法的,但咱們有權執行此操做嗎?畢竟身份和權限不是一回事。爲了進行後續的操做,kube-apiserver 還要對用戶進行受權。

kube-apiserver 處理受權的方式與處理身份驗證的方式類似:經過 kube-apiserver 的啓動參數 --authorization_mode 參數設置。它將組合一系列受權者,這些受權者將針對每一個傳入的請求進行受權。若是全部受權者都拒絕該請求,則該請求會被禁止響應而且不會再繼續響應。若是某個受權者批准了該請求,則請求繼續。

kube-apiserver 目前支持如下幾種受權方法:

  • webhook: 它與集羣外的 HTTP(S) 服務交互。
  • ABAC: 它執行靜態文件中定義的策略。
  • RBAC: 它使用 rbac.authorization.k8s.io API Group實現受權決策,容許管理員經過 Kubernetes API 動態配置策略。
  • Node: 它確保 kubelet 只能訪問本身節點上的資源。

准入控制

突破了以前所說的認證和受權兩道關口以後,客戶端的調用請求就可以獲得 API Server 的真正響應了嗎?答案是:不能!

從 kube-apiserver 的角度來看,它已經驗證了咱們的身份而且賦予了相應的權限容許咱們繼續,但對於 Kubernetes 而言,其餘組件對於應不該該容許發生的事情仍是頗有意見的。因此這個請求還須要經過 Admission Controller 所控制的一個 准入控制鏈 的層層考驗,官方標準的 「關卡」 有近十個之多,並且還能自定義擴展!

雖然受權的重點是回答用戶是否有權限,但准入控制器會攔截請求以確保它符合集羣的更普遍的指望和規則。它們是資源對象保存到 etcd 以前的最後一個堡壘,封裝了一系列額外的檢查以確保操做不會產生意外或負面結果。不一樣於受權和認證只關心請求的用戶和操做,准入控制還處理請求的內容,而且僅對建立、更新、刪除或鏈接(如代理)等有效,而對讀操做無效。

准入控制器的工做方式與受權者和驗證者的工做方式相似,但有一點區別:與驗證鏈和受權鏈不一樣,若是某個准入控制器檢查不經過,則整個鏈會中斷,整個請求將當即被拒絕而且返回一個錯誤給終端用戶。

准入控制器設計的重點在於提升可擴展性,某個控制器都做爲一個插件存儲在 plugin/pkg/admission 目錄中,而且與某一個接口相匹配,最後被編譯到 kube-apiserver 二進制文件中。

大部分准入控制器都比較容易理解,接下來着重介紹 SecurityContextDenyResourceQuotaLimitRanger 這三個准入控制器。

  • SecurityContextDeny 該插件將禁止建立設置了 Security Context 的 Pod。
  • ResourceQuota 不只能限制某個 Namespace 中建立資源的數量,並且能限制某個 Namespace 中被 Pod 所請求的資源總量。該准入控制器和資源對象 ResourceQuota 一塊兒實現了資源配額管理。
  • LimitRanger 做用相似於上面的 ResourceQuota 控制器,針對 Namespace 資源的每一個個體(Pod 與 Container 等)的資源配額。該插件和資源對象 LimitRange 一塊兒實現資源配額管理。

3. etcd

到如今爲止,Kubernetes 已經對該客戶端的調用請求進行了全面完全地審查,而且已經驗證經過,運行它進入下一個環節。下一步 kube-apiserver 將對 HTTP 請求進行反序列化,而後利用獲得的結果構建運行時對象(有點像 kubectl 生成器的逆過程),並保存到 etcd 中。下面咱們將這個過程分解一下。

當收到請求時,kube-apiserver 是如何知道它該怎麼作的呢?事實上,在客戶端發送調用請求以前就已經產生了一系列很是複雜的流程。咱們就從 kube-apiserver 二進制文件首次運行開始分析吧:

  1. 當運行 kube-apiserver 二進制文件時,它會建立一個容許 apiserver 聚合的服務鏈。這是一種對 Kubernetes API 進行擴展的方式。
  2. 同時會建立一個 generic apiserver 做爲默認的 apiserver。
  3. 而後利用生成的 OpenAPI 規範來填充 apiserver 的配置。
  4. 而後 kube-apiserver 遍歷數據結構中指定的全部 API 組,並將每個 API 組做爲通用的存儲抽象保存到 etcd 中。當你訪問或變動資源狀態時,kube-apiserver 就會調用這些 API 組。
  5. 每一個 API 組都會遍歷它的全部組版本,而且將每一個 HTTP 路由映射到 REST 路徑中
  6. 當請求的 METHOD 是 POST 時,kube-apiserver 就會將請求轉交給 資源建立處理器

如今 kube-apiserver 已經知道了全部的路由及其對應的 REST 路徑,以便在請求匹配時知道調用哪些處理器和鍵值存儲。多麼機智的設計!如今假設客戶端的 HTTP 請求已經被 kube-apiserver 收到了:

  1. 若是處理鏈能夠將請求與已經註冊的路由進行匹配,就會將該請求交給註冊到該路由的專用處理器來處理;若是沒有任何一個路由能夠匹配該請求,就會將請求轉交給基於路徑的處理器(好比當調用 /apis 時);若是沒有任何一個基於路徑的處理器註冊到該路徑,請求就會被轉交給 not found 處理器,最後返回 404
  2. 幸運的是,咱們有一個名爲 createHandler 的註冊路由!它有什麼做用呢?首先它會解碼 HTTP 請求並進行基本的驗證,例如確保請求提供的 json 與 API 資源的版本相匹配。
  3. 接下來進入審計和准入控制階段。
  4. 而後資源將會經過 storage provider 保存到 etcd 中。默認狀況下保存到 etcd 中的鍵的格式爲 <namespace>/<name>,你也能夠自定義。
  5. 資源建立過程當中出現的任何錯誤都會被捕獲,最後 storage provider 會執行 get 調用來確認該資源是否被成功建立。若是須要額外的清理工做,就會調用後期建立的處理器和裝飾器。
  6. 最後構造 HTTP 響應並返回給客戶端。

原來 apiserver 作了這麼多的工做,之前居然沒有發現呢!到目前爲止,咱們建立的 Deployment 資源已經保存到了 etcd 中,但 apiserver 仍然看不到它。

4. 初始化

在一個資源對象被持久化到數據存儲以後,apiserver 還沒法徹底看到或調度它,在此以前還要執行一系列Initializers。Initializers是一種與資源類型相關聯的控制器,它會在資源對外可用以前執行某些邏輯。若是某個資源類型沒有Initializers,就會跳過此初始化步驟當即使資源對外可見。

正如大佬的博客指出的那樣,Initializers是一個強大的功能,由於它容許咱們執行通用引導操做。例如:

  • 將代理邊車容器注入到暴露 80 端口的 Pod 中,或者加上特定的 annotation
  • 將保存着測試證書的 volume 注入到特定命名空間的全部 Pod 中。
  • 若是 Secret 中的密碼小於 20 個字符,就組織其建立。

initializerConfiguration 資源對象容許你聲明某些資源類型應該運行哪些Initializers。若是你想每建立一個 Pod 時就運行一個自定義Initializers,你能夠這樣作:

apiVersion: admissionregistration.k8s.io/v1alpha1
kind: InitializerConfiguration
metadata:
 name: custom-pod-initializer
initializers:
 - name: podimage.example.com
 rules:
 - apiGroups:
 - ""
 apiVersions:
 - v1
 resources:
 - pods
複製代碼

經過該配置建立資源對象 InitializerConfiguration 以後,就會在每一個 Pod 的 metadata.initializers.pending 字段中添加 custom-pod-initializer 字段。該初始化控制器會按期掃描新的 Pod,一旦在 Pod 的 pending 字段中檢測到本身的名稱,就會執行其邏輯,執行完邏輯以後就會將 pending 字段下的本身的名稱刪除。

只有在 pending 字段下的列表中的第一個Initializers能夠對資源進行操做,當全部的Initializers執行完成,而且 pending 字段爲空時,該對象就會被認爲初始化成功。

你可能會注意到一個問題:若是 kube-apiserver 不能顯示這些資源,那麼用戶級控制器是如何處理資源的呢?

爲了解決這個問題,kube-apiserver 暴露了一個 ?includeUninitialized 查詢參數,它會返回全部的資源對象(包括未初始化的)。

5. 控制循環

Deployments controller

到了這個階段,咱們的 Deployment 記錄已經保存在 etcd 中,而且全部的初始化邏輯都執行完成,接下來的階段將會涉及到該資源所依賴的拓撲結構。在 Kubernetes 中,Deployment 實際上只是一系列 Replicaset 的集合,而 Replicaset 是一系列 Pod 的集合。那麼 Kubernetes 是如何從一個 HTTP 請求按照層級結構依次建立這些資源的呢?其實這些工做都是由 Kubernetes 內置的 Controller(控制器) 來完成的。

Kubernetes 在整個系統中使用了大量的 Controller,Controller 是一個用於將系統狀態從「當前狀態」修正到「指望狀態」的異步腳本。全部 Controller 都經過 kube-controller-manager 組件並行運行,每種 Controller 都負責一種具體的控制流程。首先介紹一下 Deployment Controller

將 Deployment 記錄存儲到 etcd 並初始化後,就能夠經過 kube-apiserver 使其可見,而後 Deployment Controller 就會檢測到它(它的工做就是負責監聽 Deployment 記錄的更改)。在咱們的例子中,控制器經過一個 Informer 註冊一個建立事件的特定回調函數(更多信息參加下文)。

當 Deployment 第一次對外可見時,該 Controller 就會將該資源對象添加到內部工做隊列,而後開始處理這個資源對象:

經過使用標籤選擇器查詢 kube-apiserver 來檢查該 Deployment 是否有與其關聯的 ReplicaSetPod 記錄。

有趣的是,這個同步過程是狀態不可知的,它覈對新記錄與覈對已經存在的記錄採用的是相同的方式。

在乎識到沒有與其關聯的 ReplicaSetPod 記錄後,Deployment Controller 就會開始執行彈性伸縮流程

建立 ReplicaSet 資源,爲其分配一個標籤選擇器並將其版本號設置爲 1。

ReplicaSet 的 PodSpec 字段從 Deployment 的 manifest 以及其餘相關元數據中複製而來。有時 Deployment 記錄在此以後也須要更新(例如,若是設置了 process deadline)。

當完成以上步驟以後,該 Deployment 的 status 就會被更新,而後從新進入與以前相同的循環,等待 Deployment 與指望的狀態相匹配。因爲 Deployment Controller 只關心 ReplicaSet,所以須要經過 ReplicaSet Controller 來繼續協調。

ReplicaSets controller

在前面的步驟中,Deployment Controller 建立了第一個 ReplicaSet,但仍然仍是沒有 Pod,這時候就該 ReplicaSet Controller 登場了!ReplicaSet Controller 的工做是監視 ReplicaSets 及其相關資源(Pod)的生命週期。和大多數其餘 Controller 同樣,它經過觸發某些事件的處理器來實現此目的。

當建立 ReplicaSet 時(由 Deployment Controller 建立),RS Controller 檢查新 ReplicaSet 的狀態,並檢查當前狀態與指望狀態之間存在的誤差,而後經過調整 Pod 的副本數來達到指望的狀態。

Pod 的建立也是批量進行的,從 SlowStartInitialBatchSize 開始,而後在每次成功的迭代中以一種 slow start 操做加倍。這樣作的目的是在大量 Pod 啓動失敗時(例如,因爲資源配額),能夠減輕 kube-apiserver 被大量沒必要要的 HTTP 請求吞沒的風險。若是建立失敗,最好可以優雅地失敗,而且對其餘的系統組件形成的影響最小!

Kubernetes 經過 Owner References(在子級資源的某個字段中引用其父級資源的 ID) 來構造嚴格的資源對象層級結構。這確保了一旦 Controller 管理的資源被刪除(級聯刪除),子資源就會被垃圾收集器刪除,同時還爲父級資源提供了一種有效的方式來避免他們競爭同一個子級資源(想象兩對父母都認爲他們擁有同一個孩子的場景)。

Owner References 的另外一個好處是:它是有狀態的。若是有任何 Controller 重啓了,那麼因爲資源對象的拓撲關係與 Controller 無關,該操做不會影響到系統的穩定運行。這種對資源隔離的重視也體如今 Controller 自己的設計中:Controller 不能對本身沒有明確擁有的資源進行操做,它們應該選擇對資源的全部權,互不干涉,互不共享。

有時系統中也會出現孤兒(orphaned)資源,一般由如下兩種途徑產生:

  • 父級資源被刪除,但子級資源沒有被刪除
  • 垃圾收集策略禁止刪除子級資源

當發生這種狀況時,Controller 將會確保孤兒資源擁有新的 Owner。多個父級資源能夠相互競爭同一個孤兒資源,但只有一個會成功(其餘父級資源會收到驗證錯誤)。

Informers

你可能已經注意到,某些 Controller(例如 RBAC 受權器或 Deployment Controller)須要先檢索集羣狀態而後才能正常運行。拿 RBAC 受權器舉例,當請求進入時,受權器會將用戶的初始狀態緩存下來,而後用它來檢索與 etcd 中的用戶關聯的全部 角色(Role)和 角色綁定(RoleBinding)。那麼問題來了,Controller 是如何訪問和修改這些資源對象的呢?事實上 Kubernetes 是經過 Informer 機制來解決這個問題的。

Infomer 是一種模式,它容許 Controller 查找緩存在本地內存中的數據(這份數據由 Informer 本身維護)並列出它們感興趣的資源。

雖然 Informer 的設計很抽象,但它在內部實現了大量的對細節的處理邏輯(例如緩存),緩存很重要,由於它不但能夠減小對 Kubenetes API 的直接調用,同時也能減小 Server 和 Controller 的大量重複性工做。經過使用 Informer,不一樣的 Controller 之間以線程安全(Thread safety)的方式進行交互,而沒必要擔憂多個線程訪問相同的資源時會產生衝突。

有關 Informer 的更多詳細解析,請參考這篇文章:Kubernetes: Controllers, Informers, Reflectors and Stores

Scheduler

當全部的 Controller 正常運行後,etcd 中就會保存一個 Deployment、一個 ReplicaSet 和 三個 Pod 資源記錄,而且能夠經過 kube-apiserver 查看。然而,這些 Pod 資源如今還處於 Pending 狀態,由於它們尚未被調度到集羣中合適的 Node 上運行。這個問題最終要靠調度器(Scheduler)來解決。

Scheduler 做爲一個獨立的組件運行在集羣控制平面上,工做方式與其餘 Controller 相同:監聽實際並將系統狀態調整到指望的狀態。具體來講,Scheduler 的做用是將待調度的 Pod 按照特定的算法和調度策略綁定(Binding)到集羣中某個合適的 Node 上,並將綁定信息寫入 etcd 中(它會過濾其 PodSpec 中 NodeName 字段爲空的 Pod),默認的調度算法的工做方式以下:

  1. 當 Scheduler 啓動時,會註冊一個默認的預選策略鏈,這些預選策略會對備選節點進行評估,判斷備選節點是否知足備選 Pod 的需求。例如,若是 PodSpec 字段限制了 CPU 和內存資源,那麼當備選節點的資源容量不知足備選 Pod 的需求時,備選 Pod 就不會被調度到該節點上(資源容量=備選節點資源總量-節點中已存在 Pod 的全部容器的需求資源(CPU 和內存)的總和

  2. 一旦篩選出符合要求的候選節點,就會採用優選策略計算出每一個候選節點的積分,而後對這些候選節點進行排序,積分最高者勝出。例如,爲了在整個系統中分攤工做負載,這些優選策略會從備選節點列表中選出資源消耗最小的節點。每一個節點經過優選策略時都會算出一個得分,計算各項得分,最終選出分值大的節點做爲優選的結果。

一旦找到了合適的節點,Scheduler 就會建立一個 Binding 對象,該對象的 NameUid 與 Pod 相匹配,而且其 ObjectReference 字段包含所選節點的名稱,而後經過 POST 請求發送給 apiserver

當 kube-apiserver 接收到此 Binding 對象時,註冊吧會將該對象反序列化並更新 Pod 資源中的如下字段:

  • NodeName 的值設置爲 ObjectReference 中的 NodeName。
  • 添加相關的註釋。
  • PodScheduledstatus 值設置爲 True。能夠經過 kubectl 來查看:
$ kubectl get <PODNAME> -o go-template='{{range .status.conditions}}{{if eq .type "PodScheduled"}}{{.status}}{{end}}{{end}}'
複製代碼

一旦 Scheduler 將 Pod 調度到某個節點上,該節點的 Kubelet 就會接管該 Pod 並開始部署。

預選策略和優選策略均可以經過 --policy-config-file 參數來擴展,若是默認的調度器不知足要求,還能夠部署自定義的調度器。若是 podSpec.schedulerName 的值設置爲其餘的調度器,則 Kubernetes 會將該 Pod 的調度轉交給那個調度器。

6. Kubelet

Pod 同步

如今,全部的 Controller 都完成了工做,咱們來總結一下:

  • HTTP 請求經過了認證、受權和准入控制階段。
  • 一個 Deployment、ReplicaSet 和三個 Pod 資源被持久化到 etcd 存儲中。
  • 而後運行了一系列Initializers。
  • 最後每一個 Pod 都被調度到合適的節點。

然而到目前爲止,全部的狀態變化僅僅只是針對保存在 etcd 中的資源記錄,接下來的步驟涉及到運行在工做節點之間的 Pod 的分佈情況,這是分佈式系統(好比 Kubernetes)的關鍵因素。這些任務都是由 Kubelet 組件完成的,讓咱們開始吧!

在 Kubernetes 集羣中,每一個 Node 節點上都會啓動一個 Kubelet 服務進程,該進程用於處理 Scheduler 下發到本節點的任務,管理 Pod 的生命週期,包括掛載卷、容器日誌記錄、垃圾回收以及其餘與 Pod 相關的事件。

若是換一種思惟模式,你能夠把 Kubelet 當成一種特殊的 Controller,它每隔 20 秒(能夠自定義)向 kube-apiserver 經過 NodeName 獲取自身 Node 上所要運行的 Pod 清單。一旦獲取到了這個清單,它就會經過與本身的內部緩存進行比較來檢測新增長的 Pod,若是有差別,就開始同步 Pod 列表。咱們來詳細分析一下同步過程:

  1. 若是 Pod 正在建立, Kubelet 就會記錄一些在 Prometheus 中用於追蹤 Pod 啓動延時的指標

  2. 而後生成一個 PodStatus 對象,它表示 Pod 當前階段的狀態。Pod 的狀態(Phase) 是 Pod 在其生命週期中的最精簡的概要,包括 PendingRunningSucceededFailedUnkown 這幾個值。狀態的產生過程很是過程,因此頗有必要深刻了解一下背後的原理:

    • 首先串行執行一系列 Pod 同步處理器(PodSyncHandlers),每一個處理器檢查檢查 Pod 是否應該運行在該節點上。當全部的處理器都認爲該 Pod 不該該運行在該節點上,則 Pod 的 Phase 值就會變成 PodFailed,而且將該 Pod 從該節點上驅逐出去。例如當你建立一個 Job 時,若是 Pod 失敗重試的時間超過了 spec.activeDeadlineSeconds 設置的值,就會將 Pod 從該節點驅逐出去。

    • 接下來,Pod 的 Phase 值由 init 容器 和應用容器的狀態共同來決定。由於目前容器尚未啓動,容器被視爲處於等待階段,若是 Pod 中至少有一個容器處於等待階段,則其 Phase 值爲 Pending

    • 最後,Pod 的 Condition 字段由 Pod 內全部容器的狀態決定。如今咱們的容器尚未被容器運行時建立,因此 PodReady 的狀態被設置爲 False。能夠經過 kubectl 查看:

      $ kubectl get <PODNAME> -o go-template='{{range .status.conditions}}{{if eq .type "Ready"}}{{.status}}{{end}}{{end}}'
      複製代碼
  3. 生成 PodStatus 以後(Pod 中的 status 字段),Kubelet 就會將它發送到 Pod 的狀態管理器,該管理器的任務是經過 apiserver 異步更新 etcd 中的記錄。

  4. 接下來運行一系列准入處理器來確保該 Pod 是否具備相應的權限(包括強制執行 AppArmor 配置文件和 NO_NEW_PRIVS),被准入控制器拒絕的 Pod 將一直保持 Pending 狀態。

  5. 若是 Kubelet 啓動時指定了 cgroups-per-qos 參數,Kubelet 就會爲該 Pod 建立 cgroup 並進行相應的資源限制。這是爲了更方便地對 Pod 進行服務質量(QoS)管理。

  6. 而後爲 Pod 建立相應的目錄,包括 Pod 的目錄(/var/run/kubelet/pods/<podID>),該 Pod 的卷目錄(<podDir>/volumes)和該 Pod 的插件目錄(<podDir>/plugins)。

  7. 卷管理器掛載 Spec.Volumes 中定義的相關數據卷,而後等待是否掛載成功。根據掛載卷類型的不一樣,某些 Pod 可能須要等待更長的時間(好比 NFS 卷)。

  8. 從 apiserver 中檢索 Spec.ImagePullSecrets 中定義的全部 Secret,而後將其注入到容器中。

  9. 最後經過容器運行時接口(Container Runtime Interface(CRI))開始啓動容器(下面會詳細描述)。

CRI 與 pause 容器

到了這個階段,大量的初始化工做都已經完成,容器已經準備好開始啓動了,而容器是由容器運行時(例如 DockerRkt)啓動的。

爲了更容易擴展,Kubelet 從 1.5.0 開始經過容器運行時接口與容器運行時(Container Runtime)交互。簡而言之,CRI 提供了 Kubelet 和特定的運行時之間的抽象接口,它們之間經過協議緩衝區(它像一個更快的 JSON)和 gRPC API(一種很是適合執行 Kubernetes 操做的 API)。這是一個很是酷的想法,經過使用 Kubelet 和運行時之間定義的契約關係,容器如何編排的具體實現細節已經變得可有可無。因爲不須要修改 Kubernetes 的核心代碼,開發者能夠以最小的開銷添加新的運行時。

很差意思有點跑題了,讓咱們繼續回到容器啓動的階段。第一次啓動 Pod 時,Kubelet 會經過 Remote Procedure Command(RPC) 協議調用 RunPodSandboxsandbox 用於描述一組容器,例如在 Kubernetes 中它表示的是 Pod。sandbox 是一個很寬泛的概念,因此對於其餘沒有使用容器的運行時仍然是有意義的(好比在一個基於 hypervisor 的運行時中,sandbox 可能指的就是虛擬機)。

咱們的例子中使用的容器運行時是 Docker,建立 sandbox 時首先建立的是 pause 容器。pause 容器做爲同一個 Pod 中全部其餘容器的基礎容器,它爲 Pod 中的每一個業務容器提供了大量的 Pod 級別資源,這些資源都是 Linux 命名空間(包括網絡命名空間,IPC 命名空間和 PID 命名空間)。

pause 容器提供了一種方法來管理全部這些命名空間並容許業務容器共享它們,在同一個網絡命名空間中的好處是:同一個 Pod 中的容器可使用 localhost 來相互通訊。pause 容器的第二個功能與 PID 命名空間的工做方式相關,在 PID 命名空間中,進程之間造成一個樹狀結構,一旦某個子進程因爲父進程的錯誤而變成了「孤兒進程」,其便會被 init 進程進行收養並最終回收資源。關於 pause 工做方式的詳細信息能夠參考:The Almighty Pause Container

一旦建立好了 pause 容器,下面就會開始檢查磁盤狀態而後開始啓動業務容器。

CNI 和 Pod 網絡

如今咱們的 Pod 已經有了基本的骨架:一個共享全部命名空間以容許業務容器在同一個 Pod 裏進行通訊的 pause 容器。但如今還有一個問題,那就是容器的網絡是如何創建的?

當 Kubelet 爲 Pod 建立網絡時,它會將建立網絡的任務交給 CNI 插件。CNI 表示容器網絡接口(Container Network Interface),和容器運行時的運行方式相似,它也是一種抽象,容許不一樣的網絡提供商爲容器提供不一樣的網絡實現。經過將 json 配置文件(默認在 /etc/cni/net.d 路徑下)中的數據傳送到相關的 CNI 二進制文件(默認在 /opt/cni/bin 路徑下)中,cni 插件能夠給 pause 容器配置相關的網絡,而後 Pod 中其餘的容器都使用 pause 容器的網絡。下面是一個簡單的示例配置文件:

{
    "cniVersion": "0.3.1",
    "name": "bridge",
    "type": "bridge",
    "bridge": "cnio0",
    "isGateway": true,
    "ipMasq": true,
    "ipam": {
        "type": "host-local",
        "ranges": [
          [{"subnet": "${POD_CIDR}"}]
        ],
        "routes": [{"dst": "0.0.0.0/0"}]
    }
}
複製代碼

CNI 插件還會經過 CNI_ARGS 環境變量爲 Pod 指定其餘的元數據,包括 Pod 名稱和命名空間。

下面的步驟因 CNI 插件而異,咱們以 bridge 插件舉例:

  • 該插件首先會在根網絡命名空間(也就是宿主機的網絡命名空間)中設置本地 Linux 網橋,以便爲該主機上的全部容器提供網絡服務。

  • 而後它會將一個網絡接口(veth 設備對的一端)插入到 pause 容器的網絡命名空間中,並將另外一端鏈接到網橋上。你能夠這樣來理解 veth 設備對:它就像一根很長的管道,一端鏈接到容器,一端鏈接到根網絡命名空間中,數據包就在管道中進行傳播。

  • 接下來 json 文件中指定的 IPAM Plugin 會爲 pause 容器的網絡接口分配一個 IP 並設置相應的路由,如今 Pod 就有了本身的 IP。

    • IPAM Plugin 的工做方式和 CNI Plugin 相似:經過二進制文件調用並具備標準化的接口,每個 IPAM Plugin 都必需要肯定容器網絡接口的 IP、子網以及網關和路由,並將信息返回給 CNI 插件。最多見的 IPAM Plugin 是 host-local,它從預約義的一組地址池中爲容器分配 IP 地址。它將地址池的信息以及分配信息保存在主機的文件系統中,從而確保了同一主機上每一個容器的 IP 地址的惟一性。
  • 最後 Kubelet 會將集羣內部的 DNS 服務器的 Cluster IP 地址傳給 CNI 插件,而後 CNI 插件將它們寫到容器的 /etc/resolv.conf 文件中。

一旦完成了上面的步驟,CNI 插件就會將操做的結果以 json 的格式返回給 Kubelet。

跨主機容器網絡

到目前爲止,咱們已經描述了容器如何與宿主機進行通訊,但跨主機之間的容器如何通訊呢?

一般狀況下使用 overlay 網絡來進行跨主機容器通訊,這是一種動態同步多個主機間路由的方法。 其中最經常使用的 overlay 網絡插件是 flannel,flannel 具體的工做方式能夠參考 CoreOS 的文檔

容器啓動

全部網絡都配置完成後,接下來就開始真正啓動業務容器了!

一旦 sanbox 完成初始化並處於 active 狀態,Kubelet 就能夠開始爲其建立容器了。首先啓動 PodSpec 中定義的 init 容器,而後再啓動業務容器。具體過程以下:

  1. 首先拉取容器的鏡像。若是是私有倉庫的鏡像,就會利用 PodSpec 中指定的 Secret 來拉取該鏡像。
  2. 而後經過 CRI 接口建立容器。Kubelet 向 PodSpec 中填充了一個 ContainerConfig 數據結構(在其中定義了命令,鏡像,標籤,掛載卷,設備,環境變量等待),而後經過 protobufs 發送給 CRI 接口。對於 Docker 來講,它會將這些信息反序列化並填充到本身的配置信息中,而後再發送給 Dockerd 守護進程。在這個過程當中,它會將一些元數據標籤(例如容器類型,日誌路徑,dandbox ID 等待)添加到容器中。
  3. 接下來會使用 CPU 管理器來約束容器,這是 Kubelet 1.8 中新添加的 alpha 特性,它使用 UpdateContainerResources CRI 方法將容器分配給本節點上的 CPU 資源池。
  4. 最後容器開始真正啓動
  5. 若是 Pod 中配置了容器生命週期鉤子(Hook),容器啓動以後就會運行這些 Hook。Hook 的類型包括兩種:Exec(執行一段命令) 和 HTTP(發送HTTP請求)。若是 PostStart Hook 啓動的時間過長、掛起或者失敗,容器將永遠不會變成 running 狀態。

7. 總結

若是上面一切順利,如今你的集羣上應該會運行三個容器,全部的網絡,數據卷和祕鑰都被經過 CRI 接口添加到容器中並配置成功。

上文所述的建立 Pod 整個過程的流程圖以下所示:

Kubelet 建立 Pod 的流程

8. 原文連接


相關文章
相關標籤/搜索