k8s 深入篇———— 編排[八]

前言

簡單整理一下編排。

正文

一個deployment 例子:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx-deployment
spec:
  selector:
    matchLabels:
      app: nginx
  replicas: 2
  template:
    metadata:
      labels:
        app: nginx
    spec:
      containers:
      - name: nginx
        image: nginx:1.7.9
        ports:
        - containerPort: 80

然後看下結果:

出現了兩個pod。

這個 Deployment 定義的編排動作非常簡單,即:確保攜帶了 app=nginx 標籤的 Pod 的個數,永遠等於 spec.replicas 指定的個數,即 2 個。

這就意味着,如果在這個集羣中,攜帶 app=nginx 標籤的 Pod 的個數大於 2 的時候,就會有舊的 Pod 被刪除;反之,就會有新的 Pod 被創建。

究竟是 Kubernetes 項目中的哪個組件,在執行這些操作呢?

我在前面介紹 Kubernetes 架構的時候,曾經提到過一個叫作 kube-controller-manager 的組件。

實際上,這個組件,就是一系列控制器的集合。我們可以查看一下 Kubernetes 項目的 pkg/controller 目錄:

這個目錄下面的每一個控制器,都以獨有的方式負責某種編排功能。而我們的 Deployment,正是這些控制器中的一種。

實際上,這些控制器之所以被統一放在 pkg/controller 目錄下,就是因爲它們都遵循 Kubernetes 項目中的一個通用編排模式,即:控制循環(control loop)。

比如,現在有一種待編排的對象 X,它有一個對應的控制器。那麼,我就可以用一段 Go 語言風格的僞代碼,爲你描述這個控制循環:

for {
  實際狀態 := 獲取集羣中對象 X 的實際狀態(Actual State)
  期望狀態 := 獲取集羣中對象 X 的期望狀態(Desired State)
  if 實際狀態 == 期望狀態{
    什麼都不做
  } else {
    執行編排動作,將實際狀態調整爲期望狀態
  }
}

在具體實現中,實際狀態往往來自於 Kubernetes 集羣本身。

比如,kubelet 通過心跳彙報的容器狀態和節點狀態,或者監控系統中保存的應用監控數據,或者控制器主動收集的它自己感興趣的信息,這些都是常見的實際狀態的來源。

而期望狀態,一般來自於用戶提交的 YAML 文件。

比如,Deployment 對象中 Replicas 字段的值。很明顯,這些信息往往都保存在 Etcd 中。

接下來,以 Deployment 爲例,我和你簡單描述一下它對控制器模型的實現:

Deployment 控制器從 Etcd 中獲取到所有攜帶了“app: nginx”標籤的 Pod,然後統計它們的數量,這就是實際狀態;

Deployment 對象的 Replicas 字段的值就是期望狀態;

Deployment 控制器將兩個狀態做比較,然後根據比較結果,確定是創建 Pod,還是刪除

這個操作,通常被叫作調諧(Reconcile)。這個調諧的過程,則被稱作“Reconcile Loop”(調諧循環)或者“Sync Loop”(同步循環)。

其中,這個控制器對象本身,負責定義被管理對象的期望狀態。比如,Deployment 裏的 replicas=2 這個字段。

而被控制對象的定義,則來自於一個“模板”。比如,Deployment 裏的 template 字段。

可以看到,Deployment 這個 template 字段裏的內容,跟一個標準的 Pod 對象的 API 定義,絲毫不差。而所有被這個 Deployment 管理的 Pod 實例,其實都是根據這個 template 字段的內容創建出來的。

像 Deployment 定義的 template 字段,在 Kubernetes 項目中有一個專有的名字,叫作 PodTemplate(Pod 模板)。

這就是爲什麼,在所有 API 對象的 Metadata 裏,都有一個字段叫作 ownerReference,用於保存當前這個 API 對象的擁有者(Owner)的信息。

水平擴展

現在我們知道了,Deployment 可以控制pod。

那麼是不是deployment 直接控制了pod呢? 是不是pod 直接向deployment 彙報呢?

答案是不是,中間還有一個replicaset。

apiVersion: apps/v1
kind: ReplicaSet
metadata:
  name: nginx-set
  labels:
    app: nginx
spec:
  replicas: 3
  selector:
    matchLabels:
      app: nginx
  template:
    metadata:
      labels:
        app: nginx
    spec:
      containers:
      - name: nginx
        image: nginx:1.7.9

創建一個replicaset。

創建一個replicaset,裏面指定了個數是3個,

從這個 YAML 文件中,我們可以看到,一個 ReplicaSet 對象,其實就是由副本數目的定義和一個 Pod 模板組成的。不難發現,它的定義其實是 Deployment 的一個子集。

更重要的是,Deployment 控制器實際操縱的,正是這樣的 ReplicaSet 對象,而不是 Pod 對象。

再來看上面這個例子:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx-deployment
  labels:
    app: nginx
spec:
  replicas: 3
  selector:
    matchLabels:
      app: nginx
  template:
    metadata:
      labels:
        app: nginx
    spec:
      containers:
      - name: nginx
        image: nginx:1.7.9
        ports:
        - containerPort: 80

可以看到,這就是一個我們常用的 nginx-deployment,它定義的 Pod 副本個數是 3(spec.replicas=3)。

那麼,在具體的實現上,這個 Deployment,與 ReplicaSet,以及 Pod 的關係是怎樣的呢?

我們可以用一張圖把它描述出來:

通過這張圖,我們就很清楚的看到,一個定義了 replicas=3 的 Deployment,與它的 ReplicaSet,以及 Pod 的關係,實際上是一種“層層控制”的關係。

其中,ReplicaSet 負責通過“控制器模式”,保證系統中 Pod 的個數永遠等於指定的個數(比如,3 個)。這也正是 Deployment 只允許容器的 restartPolicy=Always 的主要原因:只有在容器能保證自己始終是 Running 狀態的前提下,ReplicaSet 調整 Pod 的個數纔有意義。

而在此基礎上,Deployment 同樣通過“控制器模式”,來操作 ReplicaSet 的個數和屬性,進而實現“水平擴展 / 收縮”和“滾動更新”這兩個編排動作。

其中,“水平擴展 / 收縮”非常容易實現,Deployment Controller 只需要修改它所控制的 ReplicaSet 的 Pod 副本個數就可以了。

比如,把這個值從 3 改成 4,那麼 Deployment 所對應的 ReplicaSet,就會根據修改後的值自動創建一個新的 Pod。這就是“水平擴展”了;“水平收縮”則反之。

而用戶想要執行這個操作的指令也非常簡單,就是 kubectl scale,比如:

kubectl scale deployment nginx-deployment --replicas=4

DESIRED:用戶期望的 Pod 副本個數(spec.replicas 的值);

CURRENT:當前處於 Running 狀態的 Pod 的個數;

UP-TO-DATE:當前處於最新版本的 Pod 的個數,所謂最新版本指的是 Pod 的 Spec 部分與 Deployment 裏 Pod 模板裏定義的完全一致;

AVAILABLE:當前已經可用的 Pod 的個數,即:既是 Running 狀態,又是最新版本,並且已經處於 Ready(健康檢查正確)狀態的 Pod 的個數。

可以看到,只有這個 AVAILABLE 字段,描述的纔是用戶所期望的最終狀態。

而 Kubernetes 項目還爲我們提供了一條指令,讓我們可以實時查看 Deployment 對象的狀態變化。這個指令就是 kubectl rollout status.

如上所示,在用戶提交了一個 Deployment 對象後,Deployment Controller 就會立即創建一個 Pod 副本個數爲 3 的 ReplicaSet。這個 ReplicaSet 的名字,則是由 Deployment 的名字和一個隨機字符串共同組成。

這個隨機字符串叫作 pod-template-hash,在我們這個例子裏就是:3167673210。ReplicaSet 會把這個隨機字符串加在它所控制的所有 Pod 的標籤裏,從而保證這些 Pod 不會與集羣裏的其他 Pod 混淆。

而 ReplicaSet 的 DESIRED、CURRENT 和 READY 字段的含義,和 Deployment 中是一致的。所以,相比之下,Deployment 只是在 ReplicaSet 的基礎上,添加了 UP-TO-DATE 這個跟版本有關的狀態字段。

這個時候,如果我們修改了 Deployment 的 Pod 模板,“滾動更新”就會被自動觸發。

修改 Deployment 有很多方法。比如,我可以直接使用 kubectl edit 指令編輯 Etcd 裏的 API 對象。

我們可以直接修改:

kubectl edit deployment/nginx-deployment

這個 kubectl edit 指令,會幫你直接打開 nginx-deployment 的 API 對象。然後,你就可以修改這裏的 Pod 模板部分了。比如,在這裏,我將 nginx 鏡像的版本升級到了 1.9.1。

備註:kubectl edit 並不神祕,它不過是把 API 對象的內容下載到了本地文件,讓你修改完成後再提交上去。

kubectl edit 指令編輯完成後,保存退出,Kubernetes 就會立刻觸發“滾動更新”的過程。你還可以通過 kubectl rollout status 指令查看 nginx-deployment 的狀態變化:

出現了兩個rs。

其中,舊 ReplicaSet已經被“水平收縮”成了 0 個副本, 怎麼看它是舊的呢? 看age 時間。

這時,你可以通過查看 Deployment 的 Events,看到這個“滾動更新”的流程:

這種“滾動更新”的好處是顯而易見的。

比如,在升級剛開始的時候,集羣裏只有 1 個新版本的 Pod。如果這時,新版本 Pod 有問題啓動不起來,那麼“滾動更新”就會停止,從而允許開發和運維人員介入。而在這個過程中,由於應用本身還有兩個舊版本的 Pod 在線,所以服務並不會受到太大的影響。

當然,這也就要求你一定要使用 Pod 的 Health Check 機制檢查應用的運行狀態,而不是簡單地依賴於容器的 Running 狀態。要不然的話,雖然容器已經變成 Running 了,但服務很有可能尚未啓動,“滾動更新”的效果也就達不到了。

而爲了進一步保證服務的連續性,Deployment Controller 還會確保,在任何時間窗口內,只有指定比例的 Pod 處於離線狀態。同時,它也會確保,在任何時間窗口內,只有指定比例的新 Pod 被創建出來。這兩個比例的值都是可以配置的,默認都是 DESIRED 值的 25%。

所以,在上面這個 Deployment 的例子中,它有 3 個 Pod 副本,那麼控制器在“滾動更新”的過程中永遠都會確保至少有 2 個 Pod 處於可用狀態,至多隻有 4 個 Pod 同時存在於集羣中。這個策略,是 Deployment 對象的一個字段,名叫 RollingUpdateStrategy,如下所示:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx-deployment
  labels:
    app: nginx
spec:
...
  strategy:
    type: RollingUpdate
    rollingUpdate:
      maxSurge: 1
      maxUnavailable: 1

在上面這個 RollingUpdateStrategy 的配置中,maxSurge 指定的是除了 DESIRED 數量之外,在一次“滾動”中,Deployment 控制器還可以創建多少個新 Pod;而 maxUnavailable 指的是,在一次“滾動”中,Deployment 控制器可以刪除多少箇舊 Pod。

同時,這兩個配置還可以用前面我們介紹的百分比形式來表示,比如:maxUnavailable=50%,指的是我們最多可以一次刪除“50%*DESIRED 數量”個 Pod。

如上所示,Deployment 的控制器,實際上控制的是 ReplicaSet 的數目,以及每個 ReplicaSet 的屬性。

而一個應用的版本,對應的正是一個 ReplicaSet;這個版本應用的 Pod 數量,則由 ReplicaSet 通過它自己的控制器(ReplicaSet Controller)來保證。

通過這樣的多個 ReplicaSet 對象,Kubernetes 項目就實現了對多個“應用版本”的描述。

而明白了“應用版本和 ReplicaSet 一一對應”的設計思想之後,我就可以爲你講解一下Deployment 對應用進行版本控制的具體原理了。

這一次,我會使用一個叫kubectl set image的指令,直接修改 nginx-deployment 所使用的鏡像。這個命令的好處就是,你可以不用像 kubectl edit 那樣需要打開編輯器。

不過這一次,我把這個鏡像名字修改成爲了一個錯誤的名字,比如:nginx:1.91。這樣,這個 Deployment 就會出現一個升級失敗的版本。

通過這個返回結果,我們可以看到,新版本的 ReplicaSet(hash=2156724341)的“水平擴展”已經停止。而且此時,它已經創建了兩個 Pod,但是它們都沒有進入 READY 狀態。這當然是因爲這兩個 Pod 都拉取不到有效的鏡像。

與此同時,舊版本的 ReplicaSet 的“水平收縮”,也自動停止了。此時,已經有一箇舊 Pod 被刪除,還剩下3箇舊 Pod。

那麼怎麼回滾呢?

這樣就回到了4個。

很容易想到,在具體操作上,Deployment 的控制器,其實就是讓這個舊 ReplicaSet(hash=1764197365)再次“擴展”成 4 個 Pod,而讓新的 ReplicaSet(hash=2156724341)重新“收縮”到 0 個 Pod。

首先,我需要使用 kubectl rollout history 命令,查看每次 Deployment 變更對應的版本。

當然,你還可以通過這個 kubectl rollout history 指令,看到每個版本對應的 Deployment 的 API 對象的細節,具體命令如下所示:

kubectl rollout undo deployment/nginx-deployment --to-revision=2

這樣,Deployment Controller 還會按照“滾動更新”的方式,完成對 Deployment 的降級操作。

不過,你可能已經想到了一個問題:我們對 Deployment 進行的每一次更新操作,都會生成一個新的 ReplicaSet 對象,是不是有些多餘,甚至浪費資源呢?

沒錯。

所以,Kubernetes 項目還提供了一個指令,使得我們對 Deployment 的多次更新操作,最後 只生成一個 ReplicaSet。

具體的做法是,在更新 Deployment 前,你要先執行一條 kubectl rollout pause 指令。它的用法如下所示:

kubectl rollout pause deployment/nginx-deployment

這個 kubectl rollout pause 的作用,是讓這個 Deployment 進入了一個“暫停”狀態。

所以接下來,你就可以隨意使用 kubectl edit 或者 kubectl set image 指令,修改這個 Deployment 的內容了。

由於此時 Deployment 正處於“暫停”狀態,所以我們對 Deployment 的所有修改,都不會觸發新的“滾動更新”,也不會創建新的 ReplicaSet。

而等到我們對 Deployment 修改操作都完成之後,只需要再執行一條 kubectl rollout resume 指令,就可以把這個 Deployment“恢復”回來,如下所示:

kubectl rollout resume deploy/nginx-deployment

而在這個 kubectl rollout resume 指令執行之前,在 kubectl rollout pause 指令之後的這段時間裏,我們對 Deployment 進行的所有修改,最後只會觸發一次“滾動更新”。

當然,我們可以通過檢查 ReplicaSet 狀態的變化,來驗證一下 kubectl rollout pause 和 kubectl rollout resume 指令的執行效果,如下所示:

不過,即使你像上面這樣小心翼翼地控制了 ReplicaSet 的生成數量,隨着應用版本的不斷增加,Kubernetes 中還是會爲同一個 Deployment 保存很多很多不同的 ReplicaSet。

那麼,我們又該如何控制這些“歷史”ReplicaSet 的數量呢?

很簡單,Deployment 對象有一個字段,叫作 spec.revisionHistoryLimit,就是 Kubernetes 爲 Deployment 保留的“歷史版本”個數。所以,如果把它設置爲 0,你就再也不能做回滾操作了。

下一節容器化守護進程 DaemonSet.

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章