從零開始入門 K8s | 應用編排與管理

  • 2019 年 10 月 3 日
  • 筆記

一、需求來源

背景問題

首先來看一下背景問題。如下圖所示:如果我們直接管理集群中所有的 Pod,應用 A、B、C 的 Pod,其實是散亂地分布在集群中。

file

現在有以下的問題:

  • 首先,如何保證集群內可用 Pod 的數量?也就是說我們應用 A 四個 Pod 如果出現了一些宿主機故障,或者一些網路問題,如何能保證它可用的數量?
  • 如何為所有 Pod 更新鏡像版本?我們是否要某一個 Pod 去重建新版本的 Pod?
  • 然後在更新過程中,如何保證服務的可用性?
  • 以及更新過程中,如果發現了問題,如何快速回滾到上一個版本?

Deployment:管理部署發布的控制器

這裡就引入了我們今天的主題:Deployment 管理部署發布的控制器。

file

可以看到我們通過 Deployment 將應用 A、B、C 分別規划到不同的 Deployment 中,每個 Deployment 其實是管理的一組相同的應用 Pod,這組 Pod 我們認為它是相同的一個副本,那麼 Deployment 能幫我們做什麼事情呢?

  1. 首先,Deployment 定義了一種 Pod 期望數量,比如說應用 A,我們期望 Pod 數量是四個,那麼這樣的話,controller 就會持續維持 Pod 數量為期望的數量。當我們與 Pod 出現了網路問題或者宿主機問題的話,controller 能幫我們恢復,也就是新擴出來對應的 Pod,來保證可用的 Pod 數量與期望數量一致;

  2. 配置 Pod 發布方式,也就是說 controller 會按照用戶給定的策略來更新 Pod,而且更新過程中,也可以設定不可用 Pod 數量在多少範圍內;

  3. 如果更新過程中發生問題的話,即所謂「一鍵」回滾,也就是說你通過一條命令或者一行修改能夠將 Deployment 下面所有 Pod 更新為某一個舊版本 。

二、用例解讀

Deployment 語法

下面我們用一個簡單的用例來解讀一下如何操作 Deployment。

file

上圖可以看到一個最簡單的 Deployment 的 yaml 文件。

「apiVersion:apps/v1」,也就是說 Deployment 當前所屬的組是 apps,版本是 v1。「metadata」是我們看到的 Deployment 元資訊,也就是往期回顧中的 Labels、Selector、Pod.image,這些都是在往期中提到的知識點。

Deployment 作為一個 K8s 資源,它有自己的 metadata 元資訊,這裡我們定義的 Deployment.name 是 nginx.Deployment。Deployment.spec 中首先要有一個核心的欄位,即 replicas,這裡定義期望的 Pod 數量為三個;selector 其實是 Pod 選擇器,那麼所有擴容出來的 Pod,它的 Labels 必須匹配 selector 層上的 image.labels,也就是 app.nginx。

就如上面的 Pod 模板 template 中所述,這個 template 它其實包含了兩部分內容:

  • 一部分是我們期望 Pod 的 metadata,其中包含了 labels,即跟 selector.matchLabels 相匹配的一個 Labels;

  • 第二部分是 template 包含的一個 Pod.spec。這裡 Pod.spec 其實是 Deployment 最終創建出來 Pod 的時候,它所用的 Pod.spec,這裡定義了一個 container.nginx,它的鏡像版本是 nginx:1.7.9。

下面是遇到的新知識點:

  • 第一個是 replicas,就是 Deployment 中期望的或者終態數量;
  • 第二個是 template,也就是 Pod 相關的一個模板。

查看 Deployment 狀態

當我們創建出一個 Deployment 的時候,可以通過 kubectl get deployment,看到 Deployment 總體的一個狀態。如下圖所示:

file

上圖中可以看到:

  • DESIRED:期望的 Pod 數量是 3 個;
  • CURRENT:當前實際 Pod 數量是 3 個;
  • UP-TO-DATE:其實是到達最新的期望版本的 Pod 數量;
  • AVAILABLE:這個其實是運行過程中可用的 Pod 數量。後面會提到,這裡 AVAILABLE 並不簡單是可用的,也就是 Ready 狀態的,它其實包含了一些可用超過一定時間長度的 Pod;
  • AGE:deployment 創建的時長,如上圖 Deployment 就是已經創建了 80 分鐘。

查看 Pod

最後我們可以查看一下 Pod。如下圖所示:

file

上圖中有三個 Pod,Pod 名字格式我們不難看到。

最前面一段:nginx-deployment,其實是 Pod 所屬 Deployment.name;中間一段:template-hash,這裡三個 Pod 是一樣的,因為這三個 Pod 其實都是同一個 template 中創建出來的。

最後一段,是一個 random 的字元串,我們通過 get.pod 可以看到,Pod 的 ownerReferences 即 Pod 所屬的 controller 資源,並不是 Deployment,而是一個 ReplicaSet。這個 ReplicaSet 的 name,其實是 nginx-deployment 加上 pod.template-hash,後面會提到。所有的 Pod 都是 ReplicaSet 創建出來的,而 ReplicaSet 它對應的某一個具體的 Deployment.template 版本。

更新鏡像

接下來我們可以看一下,如何對一個給定的 Deployment 更新它所有Pod的鏡像版本呢?這裡我們可以執行一個 kubectl 命令:

kubectl set image deployment.v1.apps/nginx-deployment nginx=nginx:1.9.1

  • 首先 kubectl 後面有一個 set image 固定寫法,這裡指的是設定鏡像;

  • 其次是一個 deployment.v1.apps,這裡也是一個固定寫法,寫的是我們要操作的資源類型,deployment 是資源名、v1 是資源版本、apps 是資源組,這裡也可以簡寫為 deployment 或者 deployment.apps,比如說寫為 deployment 的時候,默認將使用 apps 組 v1 版本。

  • 第三部分是要更新的 deployment 的 name,也就是我們的 nginx-deployment;再往後的 nginx 其實指的是 template,也就是 Pod 中的 container.name;這裡我們可以注意到:一個 Pod 中,其實可能存在多個 container,而我們指定想要更新的鏡像的 container.name,就是 nginx。

  • 最後,指定我們這個容器期望更新的鏡像版本,這裡指的是 nginx: 1.9.1。如下圖所示:當執行完這條命令之後,可以看到 deployment 中的 template.spec 已經更新為 nginx: 1.9.1。

file

快速回滾

如果我們在發布過程中遇到了問題,也支援快速回滾。通過 kubectl 執行的話,其實是「kubectl rollout undo」這個命令,可以回滾到 Deployment 上一版本;通過「rollout undo」加上「to-revision」來指定可以回滾到某一個具體的版本。

file

DeploymeStatus

最後我們來看一下 DeploymeStatus。每一個資源都有它的 spec.Status。這裡可以看一下,deploymentStatus 中描述的三個其實是它的 conversion 狀態,也就是 Processing、Complete 以及 Failed。

file

以 Processing 為例:Processing 指的是 Deployment 正在處於擴容和發布中。比如說 Processing 狀態的 deployment,它所有的 replicas 及 Pod 副本全部達到最新版本,而且是 available,這樣的話,就可以進入 complete 狀態。而 complete 狀態如果發生了一些擴縮容的話,也會進入 processing 這個處理工作狀態。

如果在處理過程中遇到一些問題:比如說拉鏡像失敗了,或者說 readiness probe 檢查失敗了,就會進入 failed 狀態;如果在運行過程中即 complete 狀態,中間運行時發生了一些 pod readiness probe 檢查失敗,這個時候 deployment 也會進入 failed 狀態。進入 failed 狀態之後,除非所有點 replicas 均變成 available,而且是 updated 最新版本,deployment 才會重新進入 complete 狀態。

三、操作演示

Deployment 創建及狀態

下面我們來進行操作演示:這裡連接一個阿里雲服務集群。我們可以看到當前集群已經有幾個可用的 node。

file

首先創建對應的 deployment。可以看到 deployment 中的 desired、current、up-to-date 以及 available 已經都達到了可用的期望狀態。

file

Deployment 的結構

這裡看到 spec 中的 replicas 是三個,selector 以及 template labels中定義的標籤都是 app:nginx,spec 中的 image 是我們期望的 nginx: 1.7.9;status 中的 available.replicas,readReplicas 以及 updatedReplicas 都是 3 個。

file

Pod 狀態

我們可以再選擇一個 Pod 看一下狀態:

可以看到:Pod 中 ownerReferences 的功能是 ReplicaSet;pod.spec.container 里的鏡像是 1.7.9。這個 Pod 已經是 Running 狀態,而且它的 conditions.status 是「true」,表示它的服務已經可用了。

file

更新升級

當前只有最新版本的 replicaset,那麼現在嘗試對 deployment 做一次升級。

file

「kubectl set image」這個操作命令,後面接 「deployment」,加 deployment.name,最後指定容器名,以及我們期望升級的鏡像版本。

file

接下來我們看下 deployment 中的 template 中的 image 已經更新為 1.9.1。

file

這個時候我們再 get pod 看一下狀態。

file

三個 pod 已經升級為新版本,pod 名字中的 pod-template-hash 也已更新。

file

可以看到:舊版本 replicaset 的 spec 數量以及 pod 數量是都是 0,新版本的 pod 數量是 3 個。

假設又做了一次更新,這個時候 get.pod 其實可以看到:當前的 pod 其實是有兩個舊版本的處於 running,另一個舊版本是在刪除中;而兩個新版本的 pod,一個已經進入 running,一個還在 creating 中。

這時我們可用的 pod 數量即非刪除狀態的 pod 數量,其實是 4 個,已經超過了 replica 原先在 deployment 設置的數量 3 個。這個原因是我們在 deployment 中有 maxavailable 和 maxsugar 兩個操作,這兩個配置可以限制我們在發布過程中的一些策略。在後面架構設計中會講到這個問題。

file

歷史版本保留 revisionHistoryLimit

上圖看到,我們當前最新版本的 replicaset 是 3 個 pod,另外還有兩個歷史版本的 replicaset,那麼會不會存在一種情況:隨著 deployment 持續的更新,這箇舊版本的 replicaset 會越積越多。其實 deployment 提供了一個機制來避免這個問題:在 deployment spec 中,有一個 revisionHistoryLimit,它的默認值為 10,它其實保證了保留歷史版本的 replicaset 的數量,我們嘗試把它改為 1。

file
file

由上面第二張圖,可以看到兩個 replicaset,也就是說,除了當前版本的 replicaset 之外,舊版本的 replicaset 其實只保留了一個。

回滾

最後再嘗試做一下回滾。首先再來看一下 replicaset,這時發現舊版本的 replicaset 數量從 0 個增到 2 個,而新版本的 replicaset 數量從 3 個削減為 1 個,表示它已經開始在做回滾的操作。然後再觀察一下, 舊版本的數量已經是 3 個,即已經回滾成功,而新版本的 pod 數量變為 0 個。

file

我們最後再 get pod 看一下:

file

這時,3 個 pod.template-hash 已經更新為舊版本的 hash,但其實這 3 個 pod 都是重新創建出來的,而並非我們在前一版本中創建的 3 個 pod。換句話說,也就是我們回滾的時候,其實是創建了 3 箇舊版本的 pod,而並非把先前的 3 個 pod 找回來。

四、架構設計

管理模式

file

我們來看一下架構設計。首先簡單看一下管理模式:Deployment 只負責管理不同版本的 ReplicaSet,由 ReplicaSet 來管理具體的 Pod 副本數,每個 ReplicaSet 對應 Deployment template 的一個版本。在上文的例子中可以看到,每一次修改 template,都會生成一個新的 ReplicaSet,這個 ReplicaSet 底下的 Pod 其實都是相同的版本。

如上圖所示:Deployment 創建 ReplicaSet,而 ReplicaSet 創建 Pod。他們的 OwnerRef 其實都對應了其控制器的資源。

Deployment 控制器

我們先簡單看一下控制器實現原理。

首先,我們所有的控制器都是通過 Informer 中的 Event 做一些 Handler 和 Watch。這個地方 Deployment 控制器,其實是關注 Deployment 和 ReplicaSet 中的 event,收到事件後會加入到隊列中。而 Deployment controller 從隊列中取出來之後,它的邏輯會判斷 Check Paused,這個 Paused 其實是 Deployment 是否需要新的發布,如果 Paused 設置為 true 的話,就表示這個 Deployment 只會做一個數量上的維持,不會做新的發布。

file

如上圖,可以看到如果 Check paused 為 Yes 也就是 true 的話,那麼只會做 Sync replicas。也就是說把 replicas sync 同步到對應的 ReplicaSet 中,最後再 Update Deployment status,那麼 controller 這一次的 ReplicaSet 就結束了。

那麼如果 paused 為 false 的話,它就會做 Rollout,也就是通過 Create 或者是 Rolling 的方式來做更新,更新的方式其實也是通過 Create/Update/Delete 這種 ReplicaSet 來做實現的。

ReplicaSet 控制器

file

當 Deployment 分配 ReplicaSet 之後,ReplicaSet 控制器本身也是從 Informer 中 watch 一些事件,這些事件包含了 ReplicaSet 和 Pod 的事件。從隊列中取出之後,ReplicaSet controller 的邏輯很簡單,就只管理副本數。也就是說如果 controller 發現 replicas 比 Pod 數量大的話,就會擴容,而如果發現實際數量超過期望數量的話,就會刪除 Pod。

上面 Deployment 控制器的圖中可以看到,Deployment 控制器其實做了更複雜的事情,包含了版本管理,而它把每一個版本下的數量維持工作交給 ReplicaSet 來做。

擴/縮容模擬

下面來看一些操作模擬,比如說擴容模擬。這裡有一個 Deployment,它的副本數是 2,對應的 ReplicaSet 有 Pod1 和 Pod2。這時如果我們修改 Deployment replicas, controller 就會把 replicas 同步到當前版本的 ReplicaSet 中,這個 ReplicaSet 發現當前有 2 個 Pod,不滿足當前期望 3 個,就會創建一個新的 Pod3。

file

發布模擬

我們再模擬一下發布,發布的情況會稍微複雜一點。這裡可以看到 Deployment 當前初始的 template,比如說 template1 這個版本。template1 這個 ReplicaSet 對應的版本下有三個 Pod:Pod1,Pod2,Pod3。

這時修改 template 中一個容器的 image, Deployment controller 就會新建一個對應 template2 的 ReplicaSet。創建出來之後 ReplicaSet 會逐漸修改兩個 ReplicaSet 的數量,比如它會逐漸增加 ReplicaSet2 中 replicas 的期望數量,而逐漸減少 ReplicaSet1 中的 Pod 數量。

那麼最終達到的效果是:新版本的 Pod 為 Pod4、Pod5 和 Pod6,舊版本的 Pod 已經被刪除了,這裡就完成了一次發布。

file

回滾模擬

來看一下回滾模擬,根據上面的發布模擬可以知道 Pod4、Pod5、Pod6 已經發布完成。這時發現當前的業務版本是有問題的,如果做回滾的話,不管是通過 rollout 命令還是通過回滾修改 template,它其實都是把 template 回滾為舊版本的 template1。

這個時候 Deployment 會重新修改 ReplicaSet1 中 Pod 的期望數量,把期望數量修改為 3 個,且會逐漸減少新版本也就是 ReplicaSet2 中的 replica 數量,最終的效果就是把 Pod 從舊版本重新創建出來。

file

發布模擬的圖中可以看到,其實初始版本中 Pod1、Pod2、Pod3 是舊版本,而回滾之後其實是 Pod7、Pod8、Pod9。就是說它的回滾並不是把之前的 Pod 重新找出來,而是說重新創建出符合舊版本 template 的 Pod。

spec 欄位解析

最後再來簡單看一些 Deployment 中的欄位解析。首先看一下 Deployment 中其他的 spec 欄位:

  • MinReadySeconds:Deployment 會根據 Pod ready 來看 Pod 是否可用,但是如果我們設置了 MinReadySeconds 之後,比如設置為 30 秒,那 Deployment 就一定會等到 Pod ready 超過 30 秒之後才認為 Pod 是 available 的。Pod available 的前提條件是 Pod ready,但是 ready 的 Pod 不一定是 available 的,它一定要超過 MinReadySeconds 之後,才會判斷為 available;

  • revisionHistoryLimit:保留歷史 revision,即保留歷史 ReplicaSet 的數量,默認值為 10 個。這裡可以設置為一個或兩個,如果回滾可能性比較大的話,可以設置數量超過 10;

  • paused:paused 是標識,Deployment 只做數量維持,不做新的發布,這裡在 Debug 場景可能會用到;

  • progressDeadlineSeconds:前面提到當 Deployment 處於擴容或者發布狀態時,它的 condition 會處於一個 processing 的狀態,processing 可以設置一個超時時間。如果超過超時時間還處於 processing,那麼 controller 將認為這個 Pod 會進入 failed 的狀態。

file

升級策略欄位解析

最後來看一下升級策略欄位解析。

Deployment 在 RollingUpdate 中主要提供了兩個策略,一個是 MaxUnavailable,另一個是 MaxSurge。這兩個欄位解析的意思,可以看下圖中詳細的 comment,或者簡單解釋一下:

  • MaxUnavailable:滾動過程中最多有多少個 Pod 不可用;
  • MaxSurge:滾動過程中最多存在多少個 Pod 超過預期 replicas 數量。

上文提到,ReplicaSet 為 3 的 Deployment 在發布的時候可能存在一種情況:新版本的 ReplicaSet 和舊版本的 ReplicaSet 都可能有兩個 replicas,加在一起就是 4 個,超過了我們期望的數量三個。這是因為我們默認的 MaxUnavailable 和 MaxSurge 都是 25%,默認 Deployment 在發布的過程中,可能有 25% 的 replica 是不可用的,也可能超過 replica 數量 25% 是可用的,最高可以達到 125% 的 replica 數量。

這裡其實可以根據用戶實際場景來做設置。比如當用戶的資源足夠,且更注重發布過程中的可用性,可設置 MaxUnavailable 較小、MaxSurge 較大。但如果用戶的資源比較緊張,可以設置 MaxSurge 較小,甚至設置為 0,這裡要注意的是 MaxSurge 和 MaxUnavailable 不能同時為 0。

理由不難理解,當 MaxSurge 為 0 的時候,必須要刪除 Pod,才能擴容 Pod;如果不刪除 Pod 是不能新擴 Pod 的,因為新擴出來的話,總共的 Pod 數量就會超過期望數量。而兩者同時為 0 的話,MaxSurge 保證不能新擴 Pod,而 MaxUnavailable 不能保證 ReplicaSet 中有 Pod 是 available 的,這樣就會產生問題。所以說這兩個值不能同時為 0。用戶可以根據自己的實際場景來設置對應的、合適的值。

file

本文總結

這裡為大家簡單總結一下本文的主要內容:

  • Deployment 是 Kubernetes 中常見的一種 Workload,支援部署管理多版本的 Pod;
  • Deployment 管理多版本的方式,是針對每個版本的 template 創建一個 ReplicaSet,由 ReplicaSet 維護一定數量的 Pod 副本,而 Deployment 只需要關心不同版本的 ReplicaSet 里要指定多少數量的 Pod;
  • 因此,Deployment 發布部署的根本原理,就是 Deployment 調整不同版本 ReplicaSet 里的終態副本數,以此來達到多版本 Pod 的升級和回滾。

「阿里巴巴雲原生微信公眾號(ID:Alicloudnative)關注微服務、Serverless、容器、Service Mesh等技術領域、聚焦雲原生流行技術趨勢、雲原生大規模的落地實踐,做最懂雲原生開發者的技術公眾號。」