您好,登錄后才能下訂單哦!
作者 | 王思宇(酒祝) 阿里云技術(shù)專家
參與阿里巴巴云原生文末留言互動(dòng),即有機(jī)會(huì)獲得贈(zèng)書福利及作者答疑!
原地升級一詞中,“升級”不難理解,是將應(yīng)用實(shí)例的版本由舊版替換為新版。那么如何結(jié)合 Kubernetes 環(huán)境來理解“原地”呢?
我們先來看看 K8s 原生 workload 的發(fā)布方式。這里假設(shè)我們需要部署一個(gè)應(yīng)用,包括 foo、bar 兩個(gè)容器在 Pod 中。其中,foo 容器第一次部署時(shí)用的鏡像版本是 v1,我們需要將其升級為 v2 版本鏡像,該怎么做呢?
在本次升級過程中,原 Pod 對象被刪除,一個(gè)新 Pod 對象被創(chuàng)建。新 Pod 被調(diào)度到另一個(gè) Node 上,分配到一個(gè)新的 IP,并把 foo、bar 兩個(gè)容器在這個(gè) Node 上重新拉取鏡像、啟動(dòng)容器。
值得注意的是,盡管新舊兩個(gè) Pod 名字都叫 pod-0,但其實(shí)是兩個(gè)完全不同的 Pod 對象(uid也變了)。StatefulSet 等到原先的 pod-0 對象完全從 Kubernetes 集群中被刪除后,才會(huì)提交創(chuàng)建一個(gè)新的 pod-0 對象。而這個(gè)新的 Pod 也會(huì)被重新調(diào)度、分配IP、拉鏡像、啟動(dòng)容器。
在原地升級的過程中,我們僅僅更新了原 Pod 對象中 foo 容器的 image 字段來觸發(fā) foo 容器升級到新版本。而不管是 Pod 對象,還是 Node、IP 都沒有發(fā)生變化,甚至 foo 容器升級的過程中 bar 容器還一直處于運(yùn)行狀態(tài)。
總結(jié):這種只更新 Pod 中某一個(gè)或多個(gè)容器版本、而不影響整個(gè) Pod 對象、其余容器的升級方式,被我們稱為 Kubernetes 中的原地升級。
那么,我們?yōu)槭裁匆?Kubernetes 中引入這種原地升級的理念和設(shè)計(jì)呢?
首先,這種原地升級的模式極大地提升了應(yīng)用發(fā)布的效率,根據(jù)非完全統(tǒng)計(jì)數(shù)據(jù),在阿里環(huán)境下原地升級至少比完全重建升級提升了 80% 以上的發(fā)布速度。這其實(shí)很容易理解,原地升級為發(fā)布效率帶來了以下優(yōu)化點(diǎn):
其次,當(dāng)我們升級 Pod 中一些 sidecar 容器(如采集日志、監(jiān)控等)時(shí),其實(shí)并不希望干擾到業(yè)務(wù)容器的運(yùn)行。但面對這種場景,Deployment 或 StatefulSet 的升級都會(huì)將整個(gè) Pod 重建,勢必會(huì)對業(yè)務(wù)造成一定的影響。而容器級別的原地升級變動(dòng)的范圍非常可控,只會(huì)將需要升級的容器做重建,其余容器包括網(wǎng)絡(luò)、掛載盤都不會(huì)受到影響。
最后,原地升級也為我們帶來了集群的穩(wěn)定性和確定性。當(dāng)一個(gè) Kubernetes 集群中大量應(yīng)用觸發(fā)重建 Pod 升級時(shí),可能造成大規(guī)模的 Pod 飄移,以及對 Node 上一些低優(yōu)先級的任務(wù) Pod 造成反復(fù)的搶占遷移。這些大規(guī)模的 Pod 重建,本身會(huì)對 apiserver、scheduler、網(wǎng)絡(luò)/磁盤分配等中心組件造成較大的壓力,而這些組件的延遲也會(huì)給 Pod 重建帶來惡性循環(huán)。而采用原地升級后,整個(gè)升級過程只會(huì)涉及到 controller 對 Pod 對象的更新操作和 kubelet 重建對應(yīng)的容器。
在阿里巴巴內(nèi)部,絕大部分電商應(yīng)用在云原生環(huán)境都統(tǒng)一用原地升級的方式做發(fā)布,而這套支持原地升級的控制器就位于 OpenKruise 開源項(xiàng)目中。
也就是說,阿里內(nèi)部的云原生應(yīng)用都是統(tǒng)一使用 OpenKruise 中的擴(kuò)展 workload 做部署管理的,而并沒有采用原生 Deployment/StatefulSet 等。
那么 OpenKruise 是如何實(shí)現(xiàn)原地升級能力的呢?在介紹原地升級實(shí)現(xiàn)原理之前,我們先來看一些原地升級功能所依賴的原生 Kubernetes 功能:
每個(gè) Node 上的 Kubelet,會(huì)針對本機(jī)上所有 Pod.spec.containers 中的每個(gè) container 計(jì)算一個(gè) hash 值,并記錄到實(shí)際創(chuàng)建的容器中。
如果我們修改了 Pod 中某個(gè) container 的 image 字段,kubelet 會(huì)發(fā)現(xiàn) container 的 hash 發(fā)生了變化、與機(jī)器上過去創(chuàng)建的容器 hash 不一致,而后 kubelet 就會(huì)把舊容器停掉,然后根據(jù)最新 Pod spec 中的 container 來創(chuàng)建新的容器。
這個(gè)功能,其實(shí)就是針對單個(gè) Pod 的原地升級的核心原理。
在原生 kube-apiserver 中,對 Pod 對象的更新請求有嚴(yán)格的 validation 校驗(yàn)邏輯:
// validate updateable fields:
// 1. spec.containers[*].image
// 2. spec.initContainers[*].image
// 3. spec.activeDeadlineSeconds
簡單來說,對于一個(gè)已經(jīng)創(chuàng)建出來的 Pod,在 Pod Spec 中只允許修改 containers/initContainers 中的 image 字段,以及 activeDeadlineSeconds 字段。對 Pod Spec 中所有其他字段的更新,都會(huì)被 kube-apiserver 拒絕。
kubelet 會(huì)在 pod.status 中上報(bào) containerStatuses,對應(yīng) Pod 中所有容器的實(shí)際運(yùn)行狀態(tài):
apiVersion: v1
kind: Pod
spec:
containers:
- name: nginx
image: nginx:latest
status:
containerStatuses:
- name: nginx
image: nginx:mainline
imageID: docker-pullable://nginx@sha256:2f68b99bc0d6d25d0c56876b924ec20418544ff28e1fb89a4c27679a40da811b
絕大多數(shù)情況下,spec.containers[x].image 與 status.containerStatuses[x].image 兩個(gè)鏡像是一致的。
但是也有上述這種情況,kubelet 上報(bào)的與 spec 中的 image 不一致(spec 中是 nginx:latest,但 status 中上報(bào)的是 nginx:mainline)。
這是因?yàn)椋琸ubelet 所上報(bào)的 image 其實(shí)是從 CRI 接口中拿到的容器對應(yīng)的鏡像名。而如果 Node 機(jī)器上存在多個(gè)鏡像對應(yīng)了一個(gè) imageID,那么上報(bào)的可能是其中任意一個(gè):
$ docker images | grep nginx
nginx latest 2622e6cca7eb 2 days ago 132MB
nginx mainline 2622e6cca7eb 2 days ago
因此,一個(gè) Pod 中 spec 和 status 的 image 字段不一致,并不意味著宿主機(jī)上這個(gè)容器運(yùn)行的鏡像版本和期望的不一致。
在 Kubernetes 1.12 版本之前,一個(gè) Pod 是否處于 Ready 狀態(tài)只是由 kubelet 根據(jù)容器狀態(tài)來判定:如果 Pod 中容器全部 ready,那么 Pod 就處于 Ready 狀態(tài)。
但事實(shí)上,很多時(shí)候上層 operator 或用戶都需要能控制 Pod 是否 Ready 的能力。因此,Kubernetes 1.12 版本之后提供了一個(gè) readinessGates 功能來滿足這個(gè)場景。如下:
apiVersion: v1
kind: Pod
spec:
readinessGates:
- conditionType: MyDemo
status:
conditions:
- type: MyDemo
status: "True"
- type: ContainersReady
status: "True"
- type: Ready
status: "True"
目前 kubelet 判定一個(gè) Pod 是否 Ready 的兩個(gè)前提條件:
只有滿足上述兩個(gè)前提,kubelet 才會(huì)上報(bào) Ready condition 為 True。
了解了上面的四個(gè)背景之后,接下來分析一下 OpenKruise 是如何在 Kubernetes 中實(shí)現(xiàn)原地升級的原理。
由“背景 1”可知,其實(shí)我們對一個(gè)存量 Pod 的 spec.containers[x] 中字段做修改,kubelet 會(huì)感知到這個(gè) container 的 hash 發(fā)生了變化,隨即就會(huì)停掉對應(yīng)的舊容器,并用新的 container 來拉鏡像、創(chuàng)建和啟動(dòng)新容器。
由“背景 2”可知,當(dāng)前我們對一個(gè)存量 Pod 的 spec.containers[x] 中的修改,僅限于 image 字段。
因此,得出第一個(gè)實(shí)現(xiàn)原理:**對于一個(gè)現(xiàn)有的 Pod 對象,我們能且只能修改其中的 spec.containers[x].image 字段,來觸發(fā) Pod 中對應(yīng)容器升級到一個(gè)新的 image。
接下來的問題是,當(dāng)我們修改了 Pod 中的 spec.containers[x].image 字段后,如何判斷 kubelet 已經(jīng)將容器重建成功了呢?
由“背景 3”可知,比較 spec 和 status 中的 image 字段是不靠譜的,因?yàn)楹苡锌赡?status 中上報(bào)的是 Node 上存在的另一個(gè)鏡像名(相同 imageID)。
因此,得出第二個(gè)實(shí)現(xiàn)原理: 判斷 Pod 原地升級是否成功,相對來說比較靠譜的辦法,是在原地升級前先將 status.containerStatuses[x].imageID 記錄下來。在更新了 spec 鏡像之后,如果觀察到 Pod 的 status.containerStatuses[x].imageID 變化了,我們就認(rèn)為原地升級已經(jīng)重建了容器。
但這樣一來,我們對原地升級的 image 也有了一個(gè)要求: 不能用 image 名字(tag)不同、但實(shí)際對應(yīng)同一個(gè) imageID 的鏡像來做原地升級,否則可能一直都被判斷為沒有升級成功(因?yàn)?status 中 imageID 不會(huì)變化)。
當(dāng)然,后續(xù)我們還可以繼續(xù)優(yōu)化。OpenKruise 即將開源鏡像預(yù)熱的能力,會(huì)通過 DaemonSet 在每個(gè) Node 上部署一個(gè) NodeImage Pod。通過 NodeImage 上報(bào)我們可以得知 pod spec 中的 image 所對應(yīng)的 imageID,然后和 pod status 中的 imageID 比較即可準(zhǔn)確判斷原地升級是否成功。
在 Kubernetes 中,一個(gè) Pod 是否 Ready 就代表了它是否可以提供服務(wù)。因此,像 Service 這類的流量入口都會(huì)通過判斷 Pod Ready 來選擇是否能將這個(gè) Pod 加入 endpoints 端點(diǎn)中。
由“背景 4”可知,從 Kubernetes 1.12+ 之后,operator/controller 這些組件也可以通過設(shè)置 readinessGates 和更新 pod.status.conditions 中的自定義 type 狀態(tài),來控制 Pod 是否可用。
因此,得出第三個(gè)實(shí)現(xiàn)原理: 可以在 pod.spec.readinessGates 中定義一個(gè)叫 InPlaceUpdateReady 的 conditionType。
在原地升級時(shí):
原地升級結(jié)束后,再將 InPlaceUpdateReady condition 設(shè)為 “True”,使 Pod 重新回到 Ready 狀態(tài)。
另外在原地升級的兩個(gè)步驟中,第一步將 Pod 改為 NotReady 后,流量組件異步 watch 到變化并摘除端點(diǎn)可能是需要一定時(shí)間的。因此我們也提供優(yōu)雅原地升級的能力,即通過 gracePeriodSeconds 配置在修改 NotReady 狀態(tài)和真正更新 image 觸發(fā)原地升級兩個(gè)步驟之間的靜默期時(shí)間。
原地升級和 Pod 重建升級一樣,可以配合各種發(fā)布策略來執(zhí)行:
如上文所述, OpenKruise 結(jié)合 Kubernetes 原生提供的 kubelet 容器版本管理、readinessGates 等功能,實(shí)現(xiàn)了針對 Pod 的原地升級能力。
而原地升級也為應(yīng)用發(fā)布帶來大幅的效率、穩(wěn)定性提升。值得關(guān)注的是,隨著集群、應(yīng)用規(guī)模的增大,這種提升的收益越加明顯。正是這種原地升級能力,在近兩年幫助了阿里巴巴超大規(guī)模的應(yīng)用容器平穩(wěn)遷移到了基于 Kubernetes 的云原生環(huán)境,而原生 Deployment/StatefulSet 是完全無法在這種體量的環(huán)境下鋪開使用的。(歡迎加入釘釘交流群:23330762)
6 月 19 日 12:00 前在【阿里巴巴云原生公眾號】留言區(qū) 提出你的疑問,精選留言點(diǎn)贊第 1 名將免費(fèi)獲得此書,屆時(shí)我們還會(huì)請本文作者針對留言點(diǎn)贊前 5 名的問題進(jìn)行答疑!
為了更多開發(fā)者能夠享受到 Serverless 帶來的紅利,這一次,我們集結(jié)了 10+ 位阿里巴巴 Serverless 領(lǐng)域技術(shù)專家,打造出最適合開發(fā)者入門的 Serverless 公開課,讓你即學(xué)即用,輕松擁抱云計(jì)算的新范式——Serverless。
點(diǎn)擊即可免費(fèi)觀看課程: https://developer.aliyun.com/learning/roadmap/serverless
“ 阿里巴巴云原生關(guān)注微服務(wù)、Serverless、容器、Service Mesh 等技術(shù)領(lǐng)域、聚焦云原生流行技術(shù)趨勢、云原生大規(guī)模的落地實(shí)踐,做最懂云原生開發(fā)者的公眾號?!?/p>
免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。