作者 | 王思宇(酒祝) 阿里云技術(shù)專家
創(chuàng)新互聯(lián)建站-專業(yè)網(wǎng)站定制、快速模板網(wǎng)站建設(shè)、高性價(jià)比清原網(wǎng)站開發(fā)、企業(yè)建站全套包干低至880元,成熟完善的模板庫,直接使用。一站式清原網(wǎng)站制作公司更省心,省錢,快速模板網(wǎng)站建設(shè)找我們,業(yè)務(wù)覆蓋清原地區(qū)。費(fèi)用合理售后完善,十多年實(shí)體公司更值得信賴。
參與阿里巴巴云原生文末留言互動(dòng),即有機(jī)會(huì)獲得贈(zèng)書福利及作者答疑!
原地升級(jí)一詞中,“升級(jí)”不難理解,是將應(yīng)用實(shí)例的版本由舊版替換為新版。那么如何結(jié)合 Kubernetes 環(huán)境來理解“原地”呢?
我們先來看看 K8s 原生 workload 的發(fā)布方式。這里假設(shè)我們需要部署一個(gè)應(yīng)用,包括 foo、bar 兩個(gè)容器在 Pod 中。其中,foo 容器第一次部署時(shí)用的鏡像版本是 v1,我們需要將其升級(jí)為 v2 版本鏡像,該怎么做呢?
在本次升級(jí)過程中,原 Pod 對(duì)象被刪除,一個(gè)新 Pod 對(duì)象被創(chuàng)建。新 Pod 被調(diào)度到另一個(gè) Node 上,分配到一個(gè)新的 IP,并把 foo、bar 兩個(gè)容器在這個(gè) Node 上重新拉取鏡像、啟動(dòng)容器。
值得注意的是,盡管新舊兩個(gè) Pod 名字都叫 pod-0,但其實(shí)是兩個(gè)完全不同的 Pod 對(duì)象(uid也變了)。StatefulSet 等到原先的 pod-0 對(duì)象完全從 Kubernetes 集群中被刪除后,才會(huì)提交創(chuàng)建一個(gè)新的 pod-0 對(duì)象。而這個(gè)新的 Pod 也會(huì)被重新調(diào)度、分配IP、拉鏡像、啟動(dòng)容器。
在原地升級(jí)的過程中,我們僅僅更新了原 Pod 對(duì)象中 foo 容器的 image 字段來觸發(fā) foo 容器升級(jí)到新版本。而不管是 Pod 對(duì)象,還是 Node、IP 都沒有發(fā)生變化,甚至 foo 容器升級(jí)的過程中 bar 容器還一直處于運(yùn)行狀態(tài)。
總結(jié):這種只更新 Pod 中某一個(gè)或多個(gè)容器版本、而不影響整個(gè) Pod 對(duì)象、其余容器的升級(jí)方式,被我們稱為 Kubernetes 中的原地升級(jí)。
那么,我們?yōu)槭裁匆?Kubernetes 中引入這種原地升級(jí)的理念和設(shè)計(jì)呢?
首先,這種原地升級(jí)的模式極大地提升了應(yīng)用發(fā)布的效率,根據(jù)非完全統(tǒng)計(jì)數(shù)據(jù),在阿里環(huán)境下原地升級(jí)至少比完全重建升級(jí)提升了 80% 以上的發(fā)布速度。這其實(shí)很容易理解,原地升級(jí)為發(fā)布效率帶來了以下優(yōu)化點(diǎn):
其次,當(dāng)我們升級(jí) Pod 中一些 sidecar 容器(如采集日志、監(jiān)控等)時(shí),其實(shí)并不希望干擾到業(yè)務(wù)容器的運(yùn)行。但面對(duì)這種場景,Deployment 或 StatefulSet 的升級(jí)都會(huì)將整個(gè) Pod 重建,勢必會(huì)對(duì)業(yè)務(wù)造成一定的影響。而容器級(jí)別的原地升級(jí)變動(dòng)的范圍非??煽?,只會(huì)將需要升級(jí)的容器做重建,其余容器包括網(wǎng)絡(luò)、掛載盤都不會(huì)受到影響。
最后,原地升級(jí)也為我們帶來了集群的穩(wěn)定性和確定性。當(dāng)一個(gè) Kubernetes 集群中大量應(yīng)用觸發(fā)重建 Pod 升級(jí)時(shí),可能造成大規(guī)模的 Pod 飄移,以及對(duì) Node 上一些低優(yōu)先級(jí)的任務(wù) Pod 造成反復(fù)的搶占遷移。這些大規(guī)模的 Pod 重建,本身會(huì)對(duì) apiserver、scheduler、網(wǎng)絡(luò)/磁盤分配等中心組件造成較大的壓力,而這些組件的延遲也會(huì)給 Pod 重建帶來惡性循環(huán)。而采用原地升級(jí)后,整個(gè)升級(jí)過程只會(huì)涉及到 controller 對(duì) Pod 對(duì)象的更新操作和 kubelet 重建對(duì)應(yīng)的容器。
在阿里巴巴內(nèi)部,絕大部分電商應(yīng)用在云原生環(huán)境都統(tǒng)一用原地升級(jí)的方式做發(fā)布,而這套支持原地升級(jí)的控制器就位于 OpenKruise 開源項(xiàng)目中。
也就是說,阿里內(nèi)部的云原生應(yīng)用都是統(tǒng)一使用 OpenKruise 中的擴(kuò)展 workload 做部署管理的,而并沒有采用原生 Deployment/StatefulSet 等。
那么 OpenKruise 是如何實(shí)現(xiàn)原地升級(jí)能力的呢?在介紹原地升級(jí)實(shí)現(xiàn)原理之前,我們先來看一些原地升級(jí)功能所依賴的原生 Kubernetes 功能:
每個(gè) Node 上的 Kubelet,會(huì)針對(duì)本機(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í)就是針對(duì)單個(gè) Pod 的原地升級(jí)的核心原理。
在原生 kube-apiserver 中,對(duì) Pod 對(duì)象的更新請(qǐng)求有嚴(yán)格的 validation 校驗(yàn)邏輯:
// validate updateable fields:
// 1. spec.containers[*].image
// 2. spec.initContainers[*].image
// 3. spec.activeDeadlineSeconds
簡單來說,對(duì)于一個(gè)已經(jīng)創(chuàng)建出來的 Pod,在 Pod Spec 中只允許修改 containers/initContainers 中的 image 字段,以及 activeDeadlineSeconds 字段。對(duì) Pod Spec 中所有其他字段的更新,都會(huì)被 kube-apiserver 拒絕。
kubelet 會(huì)在 pod.status 中上報(bào) containerStatuses,對(duì)應(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 接口中拿到的容器對(duì)應(yīng)的鏡像名。而如果 Node 機(jī)器上存在多個(gè)鏡像對(duì)應(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)原地升級(jí)的原理。
由“背景 1”可知,其實(shí)我們對(duì)一個(gè)存量 Pod 的 spec.containers[x] 中字段做修改,kubelet 會(huì)感知到這個(gè) container 的 hash 發(fā)生了變化,隨即就會(huì)停掉對(duì)應(yīng)的舊容器,并用新的 container 來拉鏡像、創(chuàng)建和啟動(dòng)新容器。
由“背景 2”可知,當(dāng)前我們對(duì)一個(gè)存量 Pod 的 spec.containers[x] 中的修改,僅限于 image 字段。
因此,得出第一個(gè)實(shí)現(xiàn)原理:**對(duì)于一個(gè)現(xiàn)有的 Pod 對(duì)象,我們能且只能修改其中的 spec.containers[x].image 字段,來觸發(fā) Pod 中對(duì)應(yīng)容器升級(jí)到一個(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 原地升級(jí)是否成功,相對(duì)來說比較靠譜的辦法,是在原地升級(jí)前先將 status.containerStatuses[x].imageID 記錄下來。在更新了 spec 鏡像之后,如果觀察到 Pod 的 status.containerStatuses[x].imageID 變化了,我們就認(rèn)為原地升級(jí)已經(jīng)重建了容器。
但這樣一來,我們對(duì)原地升級(jí)的 image 也有了一個(gè)要求: 不能用 image 名字(tag)不同、但實(shí)際對(duì)應(yīng)同一個(gè) imageID 的鏡像來做原地升級(jí),否則可能一直都被判斷為沒有升級(jí)成功(因?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 所對(duì)應(yīng)的 imageID,然后和 pod status 中的 imageID 比較即可準(zhǔn)確判斷原地升級(jí)是否成功。
在 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。
在原地升級(jí)時(shí):
原地升級(jí)結(jié)束后,再將 InPlaceUpdateReady condition 設(shè)為 “True”,使 Pod 重新回到 Ready 狀態(tài)。
另外在原地升級(jí)的兩個(gè)步驟中,第一步將 Pod 改為 NotReady 后,流量組件異步 watch 到變化并摘除端點(diǎn)可能是需要一定時(shí)間的。因此我們也提供優(yōu)雅原地升級(jí)的能力,即通過 gracePeriodSeconds 配置在修改 NotReady 狀態(tài)和真正更新 image 觸發(fā)原地升級(jí)兩個(gè)步驟之間的靜默期時(shí)間。
原地升級(jí)和 Pod 重建升級(jí)一樣,可以配合各種發(fā)布策略來執(zhí)行:
如上文所述, OpenKruise 結(jié)合 Kubernetes 原生提供的 kubelet 容器版本管理、readinessGates 等功能,實(shí)現(xiàn)了針對(duì) Pod 的原地升級(jí)能力。
而原地升級(jí)也為應(yīng)用發(fā)布帶來大幅的效率、穩(wěn)定性提升。值得關(guān)注的是,隨著集群、應(yīng)用規(guī)模的增大,這種提升的收益越加明顯。正是這種原地升級(jí)能力,在近兩年幫助了阿里巴巴超大規(guī)模的應(yīng)用容器平穩(wěn)遷移到了基于 Kubernetes 的云原生環(huán)境,而原生 Deployment/StatefulSet 是完全無法在這種體量的環(huán)境下鋪開使用的。(歡迎加入釘釘交流群:23330762)
6 月 19 日 12:00 前在【阿里巴巴云原生公眾號(hào)】留言區(qū) 提出你的疑問,精選留言點(diǎn)贊第 1 名將免費(fèi)獲得此書,屆時(shí)我們還會(huì)請(qǐng)本文作者針對(duì)留言點(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ā)者的公眾號(hào)。”