從零開始入門 K8s | 有狀態應用編排 - StatefulSet

簡介: 有狀態應用的部署交付向來都是應用運維領域的難點之一,常見的有狀態需求如在磁盤持久化狀態、每個機器需要獨立且穩定的網絡標識、發佈順序確定性等。針對這類問題 Kubernetes 提供了 StatefulSet 控制器,作爲幫助有狀態應用部署和在 K8s 環境落地的 Workload。

作者 | 酒祝  阿里巴巴技術專家

本文整理自《CNCF x Alibaba 雲原生技術公開課》第 22 講。

 

一、“有狀態”需求

我們之前講到過 Deployment 作爲一個應用編排管理工具,它爲我們提供了哪些功能?

如下圖所示:

1.png

  • 首先它支持定義一組 Pod 的期望數量,Controller 會爲我們維持 Pod 的數量在期望的版本以及期望的數量;
  • 第二它支持配置 Pod 發佈方式,配置完成後 Controller 會按照我們給出的策略來更新 Pod,同時在更新的過程中,也會保證不可用 Pod 數量在我們定義的範圍內;
  • 第三,如果我們在發佈的過程中遇到問題,Deployment 也支持一鍵來回滾。

可以簡單地說,Deployment 認爲:它管理的所有相同版本的 Pod 都是一模一樣的副本。也就是說,在 Deployment Controller 看來,所有相同版本的 Pod,不管是裏面部署的應用還是行爲,都是完全相同的。

這樣一種能力對於無狀態應用是支持滿足的,如果我們遇到一些有狀態應用呢?

需求分析

比如下圖所示的一些需求:
2.png

以上的這些需求都是 Deployment 無法滿足的,因此 Kubernetes 社區爲我們提供了一個叫 StatefluSet 的資源,用來管理有狀態應用。

StatefulSet:主要面向有狀態應用管理的控制器

其實現在社區很多無狀態應用也通過 StatefulSet 來管理,通過本文的學習,大家也會明白爲什麼我們將部分無狀態應用也通過 StatefulSet 來管理。

3.png

如上圖右側所示,StatefulSet 中的 Pod 都是有序號的,從 0 開始一直到定義的 replica 數量減一。每個 Pod 都有獨立的網絡標識:一個 hostname、一塊獨立的 pvc 以及 pv 存儲。這樣的話,同一個 StatefulSet 下不同的 Pod,有不同的網絡標識、有自己獨享的存儲盤,這就能很好地滿足了絕大部分有狀態應用的需求。

如上圖右側所示:

  • 首先,每個 Pod 會有 Order 序號,會按照序號來創建,刪除和更新 Pod;
  • 其次,通過配置一個 headless Service,使每個 Pod 有一個唯一的網絡標識 (hostname);
  • 第三,通過配置 pvc 模板,就是 pvc template,使每個 Pod 有一塊或者多塊 pv 存儲盤;
  • 最後,支持一定數量的灰度發佈。比如現在有三個副本的 StatefulSet,我們可以指定只升級其中的一個或者兩個,更甚至是三個到新版本。通過這樣的方式,來達到灰度升級的目的。

二、用例解讀

StatefulSet 範例創建

4.png

上圖左側是一個 Service 的配置,我們通過配置 headless Service,其實想要達到的目標是:期望 StatefulSet 裏面的 Pod 有獨立的網絡標識。這裏的 Service name 叫 nginx。

上圖右側是一個 StatefulSet 的配置,在 spec 中有個 serviceName 也叫 nginx。通過這個 serviceName 來指定這個 StatefulSet 要對應哪一個 Service。

這個 spec 中還有其它幾個很熟悉的字段,比如 selector 和 template。selector 是一個標籤選擇器,selector 定義的標籤選擇邏輯,必須匹配 template 中 metadata 中 labels 包含 app: nginx。在 template 中定義一個 nginx container,這個 container 用的 image 版本是 alpine 版本,對外暴露的 80 端口作爲一個 web 服務。

最後,template.spec 裏面定義了一個 volumeMounts,這個 volumeMounts 並不是來源於 spec 中的一個 Volumes,而是來自於 volumeClaimTemplates,也就是 pvc 模板。我們在 pvc 模板中定義了一個叫 www-storage 的 pvc 名稱。這個 pvc 名稱,我們也會寫到 volumeMounts 作爲一個 volume name,掛載到  /usr/share/nginx/html 這個目錄下。通過這樣的方式來達到每個 Pod 都有獨立的一個 pvc,並且掛載到容器中對應目錄的一個需求。

Service、StatefulSet 狀態

5.png

通過將上文中的兩個對象創建之後,我們可以通過 get 命令可以看到 Service nginx 資源已經創建成功。

同時可以通過查看 endpoints 看到,這個後端已經註冊了三個 IP 和端口,這三個 IP 對應了 Pod 的 IP,端口對應了之前 spec 中配置的 80 端口。

最後通過 get sts (StatefulSet 縮寫) nginx-web。從結果可以看到有一列叫做 READY,值爲 3/3。分母 3 是 StatefulSet 中期望的數量,而分子 3 表示 Pod 已經達到期望 READY 的狀態數量。

Pod、PVC 狀態

下圖中的 get pod 可以看到三個 Pod 的狀態都是 Running 狀態,並且已經 READY。它的 IP 就是前面看到的 endpoint 地址。

6.png

通過 get pvc 可以看到 NAME 那一列名稱,前綴爲 www-storage,中間是 nginx-web,後綴是一個序號。通過分析可以知道 www-storage 是 volumeClaimTemplates 中定義的 name,中間爲 StatefulSet 定義的 name,末尾的序號對應着 Pod 的序號,也就是三個 PVC 分別被三個 Pod 綁定。通過這樣一種方式,達到不同的 Pod 享有不同的 PVC;PVC 也會綁定相應的一個 PV, 來達到不同的 Pod 綁定不同 PV 的目的。

Pod 的版本

7.png

之前我們學到 Deployment 使用 ReplicaSet 來管理 Pod 的版本和所期望的 Pod 數量,但是在 StatefulSet 中,是由 StatefulSet Controller 來管理下屬的 Pod,因此 StatefulSet 通過 Pod 的 label 來標識這個 Pod 所屬的版本,這裏叫 controller-revision-hash。這個 label 標識和 Deployment 以及 StatefulSet 在 Pod 中注入的 Pod template hash 是類似的。

如上圖所示,通過 get pod 查看到 controller-revision-hash,這裏的 hash 就是第一次創建 Pod 對應的 template 版本,可以看到後綴是 677759c9b8。這裏先記錄一下,接下來會做 Pod 升級,再來看一下 controller-revision-hash 會不會發生改變。

更新鏡像

8.png

通過執行上圖的命令,可以看到上圖下方的 StatefulSet 配置中,已經把 StatefulSet 中的 image 更新到了 mainline 新版本。

查看新版本狀態

9.png

通過 get pod 命令查詢 Revision hash,可以看到三個 Pod 後面的 controller-revision-hash 都已經升級到了新的 Revision hash,後面變成了 7c55499668。通過這三個 Pod 創建的時間可以發現:序號爲 2 的 Pod 創建的是最早的,之後是序號是 1 和 0。這表示在升級的過程中,真實的升級順序爲 2-1-0,通過這麼一個倒序的順序來逐漸把 Pod 升級爲新版本,並且我們升級的 Pod,還複用了之前 Pod 使用的 PVC。所以之前在 PV 存儲盤中的數據,仍然會掛載到新的 Pod 上。

上圖右上方是在 StatefulSet 的 status 中看到的數據,這裏有幾個重要的字段:

  • currentReplica:表示當前版本的數量
  • currentRevision: 表示當前版本號
  • updateReplicas:表示新版本的數量
  • updateRevision:表示當前要更新的版本號

當然這裏也能看到 currentReplica 和 updateReplica,以及 currentRevision 和 updateRevision 都是一樣的,這就表示所有 Pod 已經升級到了所需要的版本。

三、操作演示

StatefulSet 編排文件

首先這裏已經連接到了阿里雲的一個集羣,集羣中有三個節點。
10.png

現在開始創建一個 StatefulSet 和對應的 Service,首先看一下對應的編排文件。
11.png

如上圖中的例子所示,Service 對應的 nginx 對外暴露的 80 端口。StatefulSet 配置中 metadata 定義了 name 爲 nginx-web;template 中的 containers 定義了鏡像信息;最後定義了一個 volumeClaimTemplates 作爲 PVC 模板。

開始創建

12.png

執行上面的命令後,我們就把 Service 和 StatefulSet 創建成功了。通過 get pod 可以看到首先創建的 Pod 序號爲 0;通過 get pvc 可以看到序號爲 0 的 PVC 已經和 PV 進行了綁定。

13.png

此時序號爲 0 的 Pod 已經開始創建了,狀態爲 ContainerCreating。

14.png

當序號爲 0 的 Pod 創建完成後,開始創建序號爲 1 的 Pod,然後看到新的 PVC 也已經創建成功,緊接着是序號爲 2 的 Pod。

15.png

可以看到每一個 Pod 創建之前,會先創建 PVC。PVC 創建完成後,Pod 從 Pending 狀態和 PV 進行綁定,然後變成了 ContainerCreating,最後達到 Running。

查看狀態

然後通過 kubectl get sts nginx-web -o yaml 查看 StatefulSet 的狀態。

16.png

如上圖所示,期望的 replicas 數量爲 3 個,當前可用數量爲 3 個,並且達到最新的版本。

17.png

接着來看一下 Service 和 endpoints,可以看到 Service 的 Port 爲 80,endpoint 有三個對應的 IP 地址。

18.png

再來 get pod ,可以看到三個 Pod 對應了上面的 endpoints 的 IP 地址。

以上的操作結果爲:三個 PVC 和三個 Pod 已經達到了所期望的狀態,並且 StatefulSet 上報的 status 中,replicas 以及 currentReplicas 都爲三個。

升級操作

19.png

這裏重複說一下,kubectl set image 爲聲明鏡像的固定寫法;StatefulSet 表示自願類型;nginx-web是資源名稱;nginx=nginx:mainline,等號前面的 nginx 是我們在 template 中定義的 container 名稱,後面的nginx:mainline是所期望更新的鏡像版本。

通過上面的命令,已經成功的將 StatefulSet 中的鏡像更新爲新的版本。

20.png

通過 get pod 看一下狀態,nginx-web-1,nginx-web-2 已經進入了 Running 狀態。對應的 controller-revision-hash 已經是新的版本。那麼 nginx-web-0 這個 Pod,舊的 Pod 已經被刪除了,新 Pod 還在 Createing 狀態。

21.png

再次查看一下狀態,所有的 Pod 都已經 Running 狀態了。

22.png

查看一下 StatefulSet 信息,目前 StatefulSet 中的 status 裏定義的 currentRevision 已經更新到了新的版本,表示 StatefulSet 已經獲取到的三個 Pod 都已經進入了新版本。

23.png

如何查看這三個 Pod 是否還複用了之前的網絡標識和存儲盤呢?

其實 headless Service 配置的 hostname 只是和 Pod name 掛鉤的,所以只要升級後的 Pod 名稱和舊的 Pod 名稱相同,那麼就可以沿用之前 Pod 使用的網絡標識。

關於存儲盤,由上圖可以看到 PVC 的狀態,它們的創建時間一直沒有改變,還是第一次創建 Pod 時的時間,所以現在升級後的 Pod 使用的還是舊 Pod 中使用的 PVC。

24.png

比如可以查看其中的某一個 Pod,這個 Pod 裏面同樣有個聲明的 volumes,這個 persistentVolumeClaim 裏的名稱 www-storage-nginx-web-0,對應着 PVC 列表中看到的序號爲 0 的 PVC,之前是被舊的 Pod 所使用。升級過程中 Controller 刪除舊 Pod,並且創建了一個同名的新 Pod,新的 Pod 仍然複用了舊 Pod 所使用的 PVC。

通過這種方式來達到升級前後,網絡存儲都能複用的目的。

四、架構設計

管理模式

StatefulSet 可能會創建三種類型的資源。 

  • 第一種資源:ControllerRevision

通過這個資源,StatefulSet 可以很方便地管理不同版本的 template 模板。

舉個例子:比如上文中提到的 nginx,在創建之初擁有的第一個 template 版本,會創建一個對應的 ControllerRevision。而當修改了 image 版本之後,StatefulSet Controller 會創建一個新的 ControllerRevision,大家可以理解爲每一個 ControllerRevision 對應了每一個版本的 Template,也對應了每一個版本的 ControllerRevision hash。其實在 Pod label 中定義的 ControllerRevision hash,就是 ControllerRevision 的名字。通過這個資源 StatefulSet Controller 來管理不同版本的 template 資源。

  • 第二個資源:PVC

如果在 StatefulSet 中定義了 volumeClaimTemplates,StatefulSet 會在創建 Pod 之前,先根據這個模板創建 PVC,並把 PVC 加到 Pod volume 中。

如果用戶在 spec 的 pvc 模板中定義了 volumeClaimTemplates,StatefulSet 在創建 Pod 之前,根據模板創建 PVC,並加到 Pod 對應的 volume 中。當然也可以在 spec 中不定義 pvc template,那麼所創建出來的 Pod 就不會掛載單獨的一個 pv。

  • 第三個資源:Pod

StatefulSet 按照順序創建、刪除、更新 Pod,每個 Pod 有唯一的序號。
25.png

如上圖所示,StatefulSet Controller 是 Owned 三個資源:ControllerRevision、Pod、PVC。

這裏不同的地方在於,當前版本的 StatefulSet 只會在 ControllerRevision 和 Pod 中添加 OwnerReference,而不會在 PVC 中添加 OwnerReference。之前的系列文章中提到過,擁有 OwnerReference 的資源,在管理的這個資源進行刪除的默認情況下,會關聯級聯刪除下屬資源。因此默認情況下刪除 StatefulSet 之後,StatefulSet 創建的 ControllerRevision 和 Pod 都會被刪除,但是 PVC 因爲沒有寫入 OwnerReference,PVC 並不會被級聯刪除。

StatefulSet 控制器

26.png

上圖爲 StatefulSet 控制器的工作流程,下面來簡單介紹一下整個工作處理流程。

首先通過註冊 Informer 的 Event Handler(事件處理),來處理 StatefulSet 和 Pod 的變化。在 Controller 邏輯中,每一次收到 StatefulSet 或者是 Pod 的變化,都會找到對應的 StatefulSet 放到隊列。緊接着從隊列取出來處理後,先做的操作是 Update Revision,也就是先查看當前拿到的 StatefulSet 中的 template,有沒有對應的 ControllerRevision。如果沒有,說明 template 已經更新過,Controller 就會創建一個新版本的 Revision,也就有了一個新的 ControllerRevision hash 版本號。

然後 Controller 會把所有版本號拿出來,並且按照序號整理一遍。這個整理的過程中,如果發現有缺少的 Pod,它就會按照序號去創建,如果發現有多餘的 Pod,就會按照序號去刪除。當保證了 Pod 數量和 Pod 序號滿足 Replica 數量之後,Controller 會去查看是否需要更新 Pod。也就是說這兩步的區別在於,Manger pods in order 去查看所有的 Pod 是否滿足序號;而後者 Update in order 查看 Pod 期望的版本是否符合要求,並且通過序號來更新。

Update in order 其更新過程如上圖所示,其實這個過程比較簡單,就是刪除 Pod。刪除 Pod 之後,其實是在下一次觸發事件,Controller 拿到這個 success 之後會發現缺少 Pod,然後再從前一個步驟 Manger pod in order 中把新的 Pod 創建出來。在這之後 Controller 會做一次 Update status,也就是之前通過命令行看到的 status 信息。

通過整個這樣的一個流程,StatefulSet 達到了管理有狀態應用的能力。

擴容模擬

27

假設 StatefulSet 初始配置 replicas 爲 1,有一個 Pod0。那麼將 replicas 從 1 修改到 3 之後,其實我們是先創建 Pod1,默認情況是等待 Pod1 狀態 READY 之後,再創建 Pod2。

通過上圖可以看到每個 StatefulSet 下面的 Pod 都是從序號 0 開始創建的。因此一個 replicas 爲 N 的 StatefulSet,它創建出來的 Pod 序號爲 [0,N),0 是開曲線,N 是閉曲線,也就是當 N>0 的時候,序號爲 0 到 N-1。

擴縮容管理策略

28.png

可能有的同學會有疑問:如果我不想按照序號創建和刪除,那 StatefulSet 也支持其它的創建和刪除的邏輯,這也就是爲什麼社區有些人把無狀態應用也通過 StatefulSet 來管理。它的好處是它能擁有唯一的網絡標識以及網絡存儲,同時也能通過併發的方式進行擴縮容。

StatefulSet.spec 中有個字段叫 podMangementPolicy 字段,這個字段的可選策略爲 OrderedReady 和 Parallel,默認情況下爲前者。

如我們剛纔創建的例子,沒有在 spec 中定義 podMangementPolicy。那麼 Controller 默認 OrderedReady 作爲策略,然後在 OrderedReady 情況下,擴縮容就嚴格按照 Order 順序來執行,必須要等前面的 Pod 狀態爲 Ready 之後,才能擴容下一個 Pod。在縮容的時候,倒序刪除,序號從大到小進行刪除。

舉個例子,上圖右側中,從 Pod0 擴容到 Pod0、Pod1、Pod2 的時候,必須先創建 Pod1,等 Pod1 Ready 之後再創建 Pod2。其實還存在一種可能性:比如在創建 Pod1 的時候,Pod0 因爲某些原因,可能是宿主機的原因或者是應用本身的原因,Pod0 變成 NotReady 狀態。這時 Controller 也不會創建 Pod2,所以不只是我們所創建 Pod 的前一個 Pod 要 Ready,而是前面所有的 Pod 都要 Ready 之後,纔會創建下一個 Pod。上圖中的例子,如果要創建 Pod2,那麼 Pod0、Pod1 都要 ready。

另一種策略叫做 Parallel,顧名思義就是並行擴縮容,不需要等前面的 Pod 都 Ready 或者刪除後再處理下一個。

發佈模擬

29.png

假設這裏的 StatefulSet template1 對應邏輯上的 Revision1,這時 StatefulSet 下面的三個 Pod 都屬於 Revision1 版本。在我們修改了 template,比如修改了鏡像之後,Controller 是通過倒序的方式逐一升級 Pod。上圖中可以看到 Controller 先創建了一個 Revision2,對應的就是創建了 ControllerRevision2 這麼一個資源,並且將 ControllerRevision2 這個資源的 name 作爲一個新的 Revision hash。在把 Pod2 升級爲新版本後,逐一刪除 Pod0、Pod1,再去創建 Pod0、Pod1。

它的邏輯其實很簡單,在升級過程中 Controller 會把序號最大並且符合條件的 Pod 刪除掉,那麼刪除之後在下一次 Controller 在做 reconcile 的時候,它會發現缺少這個序號的 Pod,然後再按照新版本把 Pod 創建出來。

spec 字段解析

30.png

首先來看一下 spec 中前幾個字段,Replica 和 Selector 都是我們比較熟悉的字段。

  • Replica 主要是期望的數量;
  • Selector 是事件選擇器,必須匹配 spec.template.metadata.labels 中定義的條件;
  • Template:Pod 模板,定義了所要創建的 Pod 的基礎信息模板;
  • VolumeClaimTemplates:PVC 模板列表,如果在 spec 中定義了這個,PVC 會先於 Pod 模板 Template 進行創建。在 PVC 創建完成後,把創建出來的 PVC name 作爲一個 volume 注入到根據 Template 創建出來的 Pod 中。

31.png

  • ServiceName:對應 Headless Service 的名字。當然如果有人不需要這個功能的時候,會給 Service 定一個不存在的 value,Controller 也不會去做校驗,所以可以寫一個 fake 的 ServiceName。但是這裏推薦每一個 Service 都要配置一個 Headless Service,不管 StatefulSet 下面的 Pod 是否需要網絡標識;
  • PodMangementPolicy:Pod 管理策略。前面提到過這個字段的可選策略爲 OrderedReady 和 Parallel,默認情況下爲前者;
  • UpdataStrategy:Pod 升級策略。這是一個結構體,下面再詳細介紹;
  • RevisionHistoryLimit:保留歷史 ControllerRevision 的數量限制(默認爲 10)。需要注意的一點是,這裏清楚的版本,必須沒有相關的 Pod 對應這些版本,如果有 Pod 還在這個版本中,這個 ControllerRevision 是不能被刪除的。

升級策略字段解析

32.png

在上圖右側可以看到 StatefulSetUpdateStrategy 有個 type 字段,這個 type 定義了兩個類型:一個是 RollingUpdate;一個是OnDelete。

  • RollingUpdate 其實跟 Deployment 中的升級是有點類似的,就是根據滾動升級的方式來升級;
  • OnDelete 是在刪除的時候升級,叫做禁止主動升級,Controller 並不會把存活的 Pod 做主動升級,而是通過 OnDelete 的方式。比如說當前有三個舊版本的 Pod,但是升級策略是 OnDelete,所以當更新 spec 中鏡像的時候,Controller 並不會把三個 Pod 逐一升級爲新版本,而是當我們縮小 Replica 的時候,Controller 會先把 Pod 刪除掉,當我們下一次再進行擴容的時候,Controller 纔會擴容出來新版本的 Pod。

在 RollingUpdateStatefulSetSetStrategy 中,可以看到有個字段叫 Partition。這個 Partition 表示滾動升級時,保留舊版本 Pod 的數量。很多剛結束 StatefulSet 的同學可能會認爲這個是灰度新版本的數量,這是錯誤的。

舉個例子:假設當前有個 replicas 爲 10 的 StatefulSet,當我們更新版本的時候,如果 Partition 是 8,並不是表示要把 8 個 Pod 更新爲新版本,而是表示需要保留 8 個 Pod 爲舊版本,只更新 2 個新版本作爲灰度。當 Replica 爲 10 的時候,下面的 Pod 序號爲 [0,9),因此當我們配置 Partition 爲 8 的時候,其實還是保留 [0,7) 這 8個 Pod 爲舊版本,只有 [8,9) 進入新版本。

總結一下,假設 replicas=N,Partition=M (M

五、本節總結

本文的主要內容就到此爲止了,這裏爲大家簡單總結一下:

  • StatefulSet 是 Kubernetes 中常見的一種 Workload,其初始目標是面向有狀態應用部署,但也支持部署無狀態應用;
  • 與 Deployment 不同,StatefulSet 是直接操作 Pod 來做擴縮容/發佈,並沒有通過類似 ReplicaSet 的其他 workload 來管控;
  • StatefulSet 的特點是:支持每個 Pod 獨享 PVC、有一個唯一網絡標識,且在升級發佈後還能複用 PVC 和網絡標識;

 

發佈了270 篇原創文章 · 獲贊 907 · 訪問量 46萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章