天天看點

如何為 Kubernetes 實作原地更新

如何為 Kubernetes 實作原地更新

鏡像下載下傳、域名解析、時間同步請點選

阿裡巴巴開源鏡像站

一、概念介紹

原地更新一詞中,“更新”不難了解,是将應用執行個體的版本由舊版替換為新版。那麼如何結合 Kubernetes 環境來了解“原地”呢?

我們先來看看 K8s 原生 workload 的釋出方式。這裡假設我們需要部署一個應用,包括 foo、bar 兩個容器在 Pod 中。其中,foo 容器第一次部署時用的鏡像版本是 v1,我們需要将其更新為 v2 版本鏡像,該怎麼做呢?

  • 如果這個應用使用 Deployment 部署,那麼更新過程中 Deployment 會觸發新版本 ReplicaSet 建立 Pod,并删除舊版本 Pod。如下圖所示:
    如何為 Kubernetes 實作原地更新

在本次更新過程中,原 Pod 對象被删除,一個新 Pod 對象被建立。新 Pod 被排程到另一個 Node 上,配置設定到一個新的 IP,并把 foo、bar 兩個容器在這個 Node 上重新拉取鏡像、啟動容器。

  • 如果這個應該使用 StatefulSet 部署,那麼更新過程中 StatefulSet 會先删除舊 Pod 對象,等删除完成後用同樣的名字在建立一個新的 Pod 對象。如下圖所示:
    如何為 Kubernetes 實作原地更新

值得注意的是,盡管新舊兩個 Pod 名字都叫 pod-0,但其實是兩個完全不同的 Pod 對象(uid也變了)。StatefulSet 等到原先的 pod-0 對象完全從 Kubernetes 叢集中被删除後,才會送出建立一個新的 pod-0 對象。而這個新的 Pod 也會被重新排程、配置設定IP、拉鏡像、啟動容器。

而所謂原地更新模式,就是在應用更新過程中避免将整個 Pod 對象删除、建立,而是基于原有的 Pod 對象更新其中某一個或多個容器的鏡像版本:

如何為 Kubernetes 實作原地更新

在原地更新的過程中,我們僅僅更新了原 Pod 對象中 foo 容器的 image 字段來觸發 foo 容器更新到新版本。而不管是 Pod 對象,還是 Node、IP 都沒有發生變化,甚至 foo 容器更新的過程中 bar 容器還一直處于運作狀态。

總結:這種隻更新 Pod 中某一個或多個容器版本、而不影響整個 Pod 對象、其餘容器的更新方式,被我們稱為 Kubernetes 中的原地更新。

二、收益分析

那麼,我們為什麼要在 Kubernetes 中引入這種原地更新的理念和設計呢?

首先,這種原地更新的模式極大地提升了應用釋出的效率,根據非完全統計資料,在阿裡環境下原地更新至少比完全重建更新提升了 80% 以上的釋出速度。這其實很容易了解,原地更新為釋出效率帶來了以下優化點:

1.節省了排程的耗時,Pod 的位置、資源都不發生變化;

2.節省了配置設定網絡的耗時,Pod 還使用原有的 IP;

3.節省了配置設定、挂載遠端盤的耗時,Pod 還使用原有的 PV(且都是已經在 Node 上挂載好的);

4.節省了大部分拉取鏡像的耗時,因為 Node 上已經存在了應用的舊鏡像,當拉取新版本鏡像時隻需要下載下傳很少的幾層 layer。

其次,當我們更新 Pod 中一些 sidecar 容器(如采集日志、監控等)時,其實并不希望幹擾到業務容器的運作。但面對這種場景,Deployment 或 StatefulSet 的更新都會将整個 Pod 重建,勢必會對業務造成一定的影響。而容器級别的原地更新變動的範圍非常可控,隻會将需要更新的容器做重建,其餘容器包括網絡、挂載盤都不會受到影響。

最後,原地更新也為我們帶來了叢集的穩定性和确定性。當一個 Kubernetes 叢集中大量應用觸發重建 Pod 更新時,可能造成大規模的 Pod 飄移,以及對 Node 上一些低優先級的任務 Pod 造成反複的搶占遷移。這些大規模的 Pod 重建,本身會對 apiserver、scheduler、網絡/磁盤配置設定等中心元件造成較大的壓力,而這些元件的延遲也會給 Pod 重建帶來惡性循環。而采用原地更新後,整個更新過程隻會涉及到 controller 對 Pod 對象的更新操作和 kubelet 重建對應的容器。

三、技術背景

在阿裡巴巴内部,絕大部分電商應用在雲原生環境都統一用原地更新的方式做釋出,而這套支援原地更新的控制器就位于 OpenKruise 開源項目中。

也就是說,阿裡内部的雲原生應用都是統一使用 OpenKruise 中的擴充 workload 做部署管理的,而并沒有采用原生Deployment/StatefulSet 等。

那麼 OpenKruise 是如何實作原地更新能力的呢?在介紹原地更新實作原理之前,我們先來看一些原地更新功能所依賴的原生 Kubernetes 功能:

背景 1:Kubelet 針對 Pod 容器的版本管理

每個 Node 上的 Kubelet,會針對本機上所有 Pod.spec.containers 中的每個 container 計算一個 hash 值,并記錄到實際建立的容器中。

如果我們修改了 Pod 中某個 container 的 image 字段,kubelet 會發現 container 的 hash 發生了變化、與機器上過去建立的容器 hash 不一緻,而後 kubelet 就會把舊容器停掉,然後根據最新 Pod spec 中的 container 來建立新的容器。

這個功能,其實就是針對單個 Pod 的原地更新的核心原理。

背景 2:Pod 更新限制

在原生 kube-apiserver 中,對 Pod 對象的更新請求有嚴格的 validation 校驗邏輯:

// validate updateable fields:
// 1.  spec.containers[*].image
// 2.  spec.initContainers[*].image
// 3.  spec.activeDeadlineSeconds           

簡單來說,對于一個已經建立出來的 Pod,在 Pod Spec 中隻允許修改 containers/initContainers 中的 image 字段,以及 activeDeadlineSeconds 字段。對 Pod Spec 中所有其他字段的更新,都會被 kube-apiserver 拒絕。

背景 3:containerStatuses 上報

kubelet 會在 pod.status 中上報 containerStatuses,對應 Pod 中所有容器的實際運作狀态:

apiVersion: v1
kind: Pod
spec:
  containers:
  - name: nginx
    image: nginx:latest
status:
  containerStatuses:
  - name: nginx
    image: nginx:mainline
    imageID: docker-pullable://nginx@sha256:2f68b99bc0d6d25d0c56876b924ec20418544ff28e1fb89a4c27679a40da811b           

絕大多數情況下,spec.containers[x].image 與 status.containerStatuses[x].image 兩個鏡像是一緻的。

但是也有上述這種情況,kubelet 上報的與 spec 中的 image 不一緻(spec 中是 nginx:latest,但 status 中上報的是 nginx:mainline)。

這是因為,kubelet 所上報的 image 其實是從 CRI 接口中拿到的容器對應的鏡像名。而如果 Node 機器上存在多個鏡像對應了一個 imageID,那麼上報的可能是其中任意一個:

$ docker images | grep nginx
nginx            latest              2622e6cca7eb        2 days ago          132MB
nginx            mainline            2622e6cca7eb        2 days ago           

是以,一個 Pod 中 spec 和 status 的 image 字段不一緻,并不意味着主控端上這個容器運作的鏡像版本和期望的不一緻。

背景 4:ReadinessGate 控制 Pod 是否 Ready

在 Kubernetes 1.12 版本之前,一個 Pod 是否處于 Ready 狀态隻是由 kubelet 根據容器狀态來判定:如果 Pod 中容器全部 ready,那麼 Pod 就處于 Ready 狀态。

但事實上,很多時候上層 operator 或使用者都需要能控制 Pod 是否 Ready 的能力。是以,Kubernetes 1.12 版本之後提供了一個 readinessGates 功能來滿足這個場景。如下:

apiVersion: v1
kind: Pod
spec:
  readinessGates:
  - conditionType: MyDemo
status:
  conditions:
  - type: MyDemo
    status: "True"
  - type: ContainersReady
    status: "True"
  - type: Ready
    status: "True"           

目前 kubelet 判定一個 Pod 是否 Ready 的兩個前提條件:

1.Pod 中容器全部 Ready(其實對應了 ContainersReady condition 為 True);

2.如果 pod.spec.readinessGates 中定義了一個或多個 conditionType,那麼需要這些 conditionType 在 pod.status.conditions 中都有對應的 status: "true" 的狀态。

隻有滿足上述兩個前提,kubelet 才會上報 Ready condition 為 True。

四、實作原理

了解了上面的四個背景之後,接下來分析一下 OpenKruise 是如何在 Kubernetes 中實作原地更新的原理。

1. 單個 Pod 如何原地更新?

由“背景 1”可知,其實我們對一個存量 Pod 的 spec.containers[x] 中字段做修改,kubelet 會感覺到這個 container 的 hash 發生了變化,随即就會停掉對應的舊容器,并用新的 container 來拉鏡像、建立和啟動新容器。

由“背景 2”可知,目前我們對一個存量 Pod 的 spec.containers[x] 中的修改,僅限于 image 字段。

是以,得出第一個實作原理:**對于一個現有的 Pod 對象,我們能且隻能修改其中的 spec.containers[x].image 字段,來觸發 Pod 中對應容器更新到一個新的 image。

2. 如何判斷 Pod 原地更新成功?

接下來的問題是,當我們修改了 Pod 中的 spec.containers[x].image 字段後,如何判斷 kubelet 已經将容器重建成功了呢?

由“背景 3”可知,比較 spec 和 status 中的 image 字段是不靠譜的,因為很有可能 status 中上報的是 Node 上存在的另一個鏡像名(相同 imageID)。

是以,得出第二個實作原理:判斷 Pod 原地更新是否成功,相對來說比較靠譜的辦法,是在原地更新前先将status.containerStatuses[x].imageID 記錄下來。在更新了 spec 鏡像之後,如果觀察到 Pod 的 status.containerStatuses[x].imageID 變化了,我們就認為原地更新已經重建了容器。

但這樣一來,我們對原地更新的 image 也有了一個要求:不能用 image 名字(tag)不同、但實際對應同一個 imageID 的鏡像來做原地更新,否則可能一直都被判斷為沒有更新成功(因為 status 中 imageID 不會變化)。

當然,後續我們還可以繼續優化。OpenKruise 即将開源鏡像預熱的能力,會通過 DaemonSet 在每個 Node 上部署一個 NodeImage Pod。通過 NodeImage 上報我們可以得知 pod spec 中的 image 所對應的 imageID,然後和 pod status 中的 imageID 比較即可準确判斷原地更新是否成功。

3. 如何確定原地更新過程中流量無損?

在 Kubernetes 中,一個 Pod 是否 Ready 就代表了它是否可以提供服務。是以,像 Service 這類的流量入口都會通過判斷 Pod Ready 來選擇是否能将這個 Pod 加入 endpoints 端點中。

由“背景 4”可知,從 Kubernetes 1.12+ 之後,operator/controller 這些元件也可以通過設定 readinessGates 和更新pod.status.conditions 中的自定義 type 狀态,來控制 Pod 是否可用。

是以,得出第三個實作原理:可以在 pod.spec.readinessGates 中定義一個叫 InPlaceUpdateReady 的 conditionType。

在原地更新時:

先将 pod.status.conditions 中的 InPlaceUpdateReady condition 設為 "False",這樣就會觸發 kubelet 将 Pod 上報為 NotReady,進而使流量元件(如 endpoint controller)将這個 Pod 從服務端點摘除;

再更新 pod spec 中的 image 觸發原地更新。

原地更新結束後,再将 InPlaceUpdateReady condition 設為 "True",使 Pod 重新回到 Ready 狀态。

另外在原地更新的兩個步驟中,第一步将 Pod 改為 NotReady 後,流量元件異步 watch 到變化并摘除端點可能是需要一定時間的。是以我們也提供優雅原地更新的能力,即通過 gracePeriodSeconds 配置在修改 NotReady 狀态和真正更新 image 觸發原地更新兩個步驟之間的靜默期時間。

4. 組合釋出政策

原地更新和 Pod 重建更新一樣,可以配合各種釋出政策來執行:

  • partition:如果配置 partition 做灰階,那麼隻會将 replicas-partition 數量的 Pod 做原地更新;
  • maxUnavailable:如果配置 maxUnavailable,那麼隻會将滿足 unavailable 數量的 Pod 做原地更新;
  • maxSurge:如果配置 maxSurge 做彈性,那麼當先擴出來 maxSurge 數量的 Pod 之後,存量的 Pod 仍然使用原地更新;
  • priority/scatter:如果配置了釋出優先級/打散政策,會按照政策順序對 Pod 做原地更新。

五、總結

如上文所述,OpenKruise 結合 Kubernetes 原生提供的 kubelet 容器版本管理、readinessGates 等功能,實作了針對 Pod 的原地更新能力。

而原地更新也為應用釋出帶來大幅的效率、穩定性提升。值得關注的是,随着叢集、應用規模的增大,這種提升的收益越加明顯。正是這種原地更新能力,在近兩年幫助了阿裡巴巴超大規模的應用容器平穩遷移到了基于 Kubernetes 的雲原生環境,而原生 Deployment/StatefulSet 是完全無法在這種體量的環境下鋪開使用的。

作者:王思宇(酒祝)

來源:

掘金
提供全面,高效和穩定的鏡像下載下傳服務。釘釘搜尋 ' 21746399 ‘ 加入鏡像站官方使用者交流群。”

繼續閱讀