您好,登錄后才能下訂單哦!
作者 | 王思宇(酒祝) 阿里云技術專家
參與阿里巴巴云原生文末留言互動,即有機會獲得贈書福利及作者答疑!
原地升級一詞中,“升級”不難理解,是將應用實例的版本由舊版替換為新版。那么如何結合 Kubernetes 環境來理解“原地”呢?
我們先來看看 K8s 原生 workload 的發布方式。這里假設我們需要部署一個應用,包括 foo、bar 兩個容器在 Pod 中。其中,foo 容器第一次部署時用的鏡像版本是 v1,我們需要將其升級為 v2 版本鏡像,該怎么做呢?
在本次升級過程中,原 Pod 對象被刪除,一個新 Pod 對象被創建。新 Pod 被調度到另一個 Node 上,分配到一個新的 IP,并把 foo、bar 兩個容器在這個 Node 上重新拉取鏡像、啟動容器。
值得注意的是,盡管新舊兩個 Pod 名字都叫 pod-0,但其實是兩個完全不同的 Pod 對象(uid也變了)。StatefulSet 等到原先的 pod-0 對象完全從 Kubernetes 集群中被刪除后,才會提交創建一個新的 pod-0 對象。而這個新的 Pod 也會被重新調度、分配IP、拉鏡像、啟動容器。
在原地升級的過程中,我們僅僅更新了原 Pod 對象中 foo 容器的 image 字段來觸發 foo 容器升級到新版本。而不管是 Pod 對象,還是 Node、IP 都沒有發生變化,甚至 foo 容器升級的過程中 bar 容器還一直處于運行狀態。
總結:這種只更新 Pod 中某一個或多個容器版本、而不影響整個 Pod 對象、其余容器的升級方式,被我們稱為 Kubernetes 中的原地升級。
那么,我們為什么要在 Kubernetes 中引入這種原地升級的理念和設計呢?
首先,這種原地升級的模式極大地提升了應用發布的效率,根據非完全統計數據,在阿里環境下原地升級至少比完全重建升級提升了 80% 以上的發布速度。這其實很容易理解,原地升級為發布效率帶來了以下優化點:
其次,當我們升級 Pod 中一些 sidecar 容器(如采集日志、監控等)時,其實并不希望干擾到業務容器的運行。但面對這種場景,Deployment 或 StatefulSet 的升級都會將整個 Pod 重建,勢必會對業務造成一定的影響。而容器級別的原地升級變動的范圍非常可控,只會將需要升級的容器做重建,其余容器包括網絡、掛載盤都不會受到影響。
最后,原地升級也為我們帶來了集群的穩定性和確定性。當一個 Kubernetes 集群中大量應用觸發重建 Pod 升級時,可能造成大規模的 Pod 飄移,以及對 Node 上一些低優先級的任務 Pod 造成反復的搶占遷移。這些大規模的 Pod 重建,本身會對 apiserver、scheduler、網絡/磁盤分配等中心組件造成較大的壓力,而這些組件的延遲也會給 Pod 重建帶來惡性循環。而采用原地升級后,整個升級過程只會涉及到 controller 對 Pod 對象的更新操作和 kubelet 重建對應的容器。
在阿里巴巴內部,絕大部分電商應用在云原生環境都統一用原地升級的方式做發布,而這套支持原地升級的控制器就位于 OpenKruise 開源項目中。
也就是說,阿里內部的云原生應用都是統一使用 OpenKruise 中的擴展 workload 做部署管理的,而并沒有采用原生 Deployment/StatefulSet 等。
那么 OpenKruise 是如何實現原地升級能力的呢?在介紹原地升級實現原理之前,我們先來看一些原地升級功能所依賴的原生 Kubernetes 功能:
每個 Node 上的 Kubelet,會針對本機上所有 Pod.spec.containers 中的每個 container 計算一個 hash 值,并記錄到實際創建的容器中。
如果我們修改了 Pod 中某個 container 的 image 字段,kubelet 會發現 container 的 hash 發生了變化、與機器上過去創建的容器 hash 不一致,而后 kubelet 就會把舊容器停掉,然后根據最新 Pod spec 中的 container 來創建新的容器。
這個功能,其實就是針對單個 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 拒絕。
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 字段不一致,并不意味著宿主機上這個容器運行的鏡像版本和期望的不一致。
在 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 的兩個前提條件:
只有滿足上述兩個前提,kubelet 才會上報 Ready condition 為 True。
了解了上面的四個背景之后,接下來分析一下 OpenKruise 是如何在 Kubernetes 中實現原地升級的原理。
由“背景 1”可知,其實我們對一個存量 Pod 的 spec.containers[x] 中字段做修改,kubelet 會感知到這個 container 的 hash 發生了變化,隨即就會停掉對應的舊容器,并用新的 container 來拉鏡像、創建和啟動新容器。
由“背景 2”可知,當前我們對一個存量 Pod 的 spec.containers[x] 中的修改,僅限于 image 字段。
因此,得出第一個實現原理:**對于一個現有的 Pod 對象,我們能且只能修改其中的 spec.containers[x].image 字段,來觸發 Pod 中對應容器升級到一個新的 image。
接下來的問題是,當我們修改了 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 比較即可準確判斷原地升級是否成功。
在 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。
在原地升級時:
原地升級結束后,再將 InPlaceUpdateReady condition 設為 “True”,使 Pod 重新回到 Ready 狀態。
另外在原地升級的兩個步驟中,第一步將 Pod 改為 NotReady 后,流量組件異步 watch 到變化并摘除端點可能是需要一定時間的。因此我們也提供優雅原地升級的能力,即通過 gracePeriodSeconds 配置在修改 NotReady 狀態和真正更新 image 觸發原地升級兩個步驟之間的靜默期時間。
原地升級和 Pod 重建升級一樣,可以配合各種發布策略來執行:
如上文所述, OpenKruise 結合 Kubernetes 原生提供的 kubelet 容器版本管理、readinessGates 等功能,實現了針對 Pod 的原地升級能力。
而原地升級也為應用發布帶來大幅的效率、穩定性提升。值得關注的是,隨著集群、應用規模的增大,這種提升的收益越加明顯。正是這種原地升級能力,在近兩年幫助了阿里巴巴超大規模的應用容器平穩遷移到了基于 Kubernetes 的云原生環境,而原生 Deployment/StatefulSet 是完全無法在這種體量的環境下鋪開使用的。(歡迎加入釘釘交流群:23330762)
6 月 19 日 12:00 前在【阿里巴巴云原生公眾號】留言區 提出你的疑問,精選留言點贊第 1 名將免費獲得此書,屆時我們還會請本文作者針對留言點贊前 5 名的問題進行答疑!
為了更多開發者能夠享受到 Serverless 帶來的紅利,這一次,我們集結了 10+ 位阿里巴巴 Serverless 領域技術專家,打造出最適合開發者入門的 Serverless 公開課,讓你即學即用,輕松擁抱云計算的新范式——Serverless。
點擊即可免費觀看課程: https://developer.aliyun.com/learning/roadmap/serverless
“ 阿里巴巴云原生關注微服務、Serverless、容器、Service Mesh 等技術領域、聚焦云原生流行技術趨勢、云原生大規模的落地實踐,做最懂云原生開發者的公眾號。”
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。