今天就跟大家聊聊有關(guān)Kubernetes中如何保證優(yōu)雅地停止Pod,可能很多人都不太了解,為了讓大家更加了解,小編給大家總結(jié)了以下內(nèi)容,希望大家根據(jù)這篇文章可以有所收獲。
一直以來(lái)我對(duì)優(yōu)雅地停止 Pod 這件事理解得很單純:不就利用是 PreStop hook 做優(yōu)雅退出嗎?但最近發(fā)現(xiàn)很多場(chǎng)景下 PreStop Hook 并不能很好地完成需求,這篇文章就簡(jiǎn)單分析一下“優(yōu)雅地停止 Pod”這回事兒。
優(yōu)雅停止(Graceful shutdown)這個(gè)說(shuō)法來(lái)自于操作系統(tǒng),我們執(zhí)行關(guān)機(jī)之后都得 OS 先完成一些清理操作,而與之相對(duì)的就是硬中止(Hard shutdown),比如拔電源。
到了分布式系統(tǒng)中,優(yōu)雅停止就不僅僅是單機(jī)上進(jìn)程自己的事了,往往還要與系統(tǒng)中的其它組件打交道。比如說(shuō)我們起一個(gè)微服務(wù),網(wǎng)關(guān)把一部分流量分給我們,這時(shí):
假如我們一聲不吭直接把進(jìn)程殺了,那這部分流量就無(wú)法得到正確處理,部分用戶受到影響。不過(guò)還好,通常來(lái)說(shuō)網(wǎng)關(guān)或者服務(wù)注冊(cè)中心會(huì)和我們的服務(wù)保持一個(gè)心跳,過(guò)了心跳超時(shí)之后系統(tǒng)會(huì)自動(dòng)摘除我們的服務(wù),問(wèn)題也就解決了;這是硬中止,雖然我們整個(gè)系統(tǒng)寫得不錯(cuò)能夠自愈,但還是會(huì)產(chǎn)生一些抖動(dòng)甚至錯(cuò)誤。
假如我們先告訴網(wǎng)關(guān)或服務(wù)注冊(cè)中心我們要下線,等對(duì)方完成服務(wù)摘除操作再中止進(jìn)程,那不會(huì)有任何流量受到影響;這是優(yōu)雅停止,將單個(gè)組件的啟停對(duì)整個(gè)系統(tǒng)影響最小化。
按照慣例,SIGKILL 是硬終止的信號(hào),而 SIGTERM 是通知進(jìn)程優(yōu)雅退出的信號(hào),因此很多微服務(wù)框架會(huì)監(jiān)聽 SIGTERM 信號(hào),收到之后去做反注冊(cè)等清理操作,實(shí)現(xiàn)優(yōu)雅退出。
回到 Kubernetes(下稱 K8s),當(dāng)我們想干掉一個(gè) Pod 的時(shí)候,理想狀況當(dāng)然是 K8s 從對(duì)應(yīng)的 Service(假如有的話)把這個(gè) Pod 摘掉,同時(shí)給 Pod 發(fā) SIGTERM 信號(hào)讓 Pod 中的各個(gè)容器優(yōu)雅退出就行了。但實(shí)際上 Pod 有可能犯各種幺蛾子:
已經(jīng)卡死了,處理不了優(yōu)雅退出的代碼邏輯或需要很久才能處理完成。
優(yōu)雅退出的邏輯有 BUG,自己死循環(huán)了。
代碼寫得野,根本不理會(huì) SIGTERM。
因此,K8s 的 Pod 終止流程中還有一個(gè)“最多可以容忍的時(shí)間”,即 grace period(在 Pod 的 .spec.terminationGracePeriodSeconds
字段中定義),這個(gè)值默認(rèn)是 30 秒,我們?cè)趫?zhí)行 kubectl delete
的時(shí)候也可通過(guò) --grace-period
參數(shù)顯式指定一個(gè)優(yōu)雅退出時(shí)間來(lái)覆蓋 Pod 中的配置。而當(dāng) grace period 超出之后,K8s 就只能選擇 SIGKILL 強(qiáng)制干掉 Pod 了。
很多場(chǎng)景下,除了把 Pod 從 K8s 的 Service 上摘下來(lái)以及進(jìn)程內(nèi)部的優(yōu)雅退出之外,我們還必須做一些額外的事情,比如說(shuō)從 K8s 外部的服務(wù)注冊(cè)中心上反注冊(cè)。這時(shí)就要用到 PreStop Hook 了,K8s 目前提供了 Exec
和 HTTP
兩種 PreStop Hook,實(shí)際用的時(shí)候,需要通過(guò) Pod 的 .spec.containers[].lifecycle.preStop
字段為 Pod 中的每個(gè)容器單獨(dú)配置,比如:
spec: contaienrs: - name: my-awesome-container lifecycle: preStop: exec: command: ["/bin/sh","-c","/pre-stop.sh"] 復(fù)制代碼
/pre-stop.sh
腳本里就可以寫我們自己的清理邏輯。
最后我們串起來(lái)再整個(gè)表述一下 Pod 退出的流程(官方文檔里更嚴(yán)謹(jǐn)哦):
用戶刪除 Pod。
2.1. Pod 進(jìn)入 Terminating 狀態(tài)。
2.2. 與此同時(shí),K8s 會(huì)將 Pod 從對(duì)應(yīng)的 service 上摘除。
2.3. 與此同時(shí),針對(duì)有 PreStop Hook 的容器,kubelet 會(huì)調(diào)用每個(gè)容器的 PreStop Hook,假如 PreStop Hook 的運(yùn)行時(shí)間超出了 grace period,kubelet 會(huì)發(fā)送 SIGTERM 并再等 2 秒。
2.4. 與此同時(shí),針對(duì)沒(méi)有 PreStop Hook 的容器,kubelet 發(fā)送 SIGTERM。
grace period 超出之后,kubelet 發(fā)送 SIGKILL 干掉尚未退出的容器。
這個(gè)過(guò)程很不錯(cuò),但它存在一個(gè)問(wèn)題就是我們無(wú)法預(yù)測(cè) Pod 會(huì)在多久之內(nèi)完成優(yōu)雅退出,也無(wú)法優(yōu)雅地應(yīng)對(duì)“優(yōu)雅退出”失敗的情況。而在我們的產(chǎn)品 TiDB Operator 中,這就是一個(gè)無(wú)法接受的事情。
為什么說(shuō)無(wú)法接受這個(gè)流程呢?其實(shí)這個(gè)流程對(duì)無(wú)狀態(tài)應(yīng)用來(lái)說(shuō)通常是 OK 的,但下面這個(gè)場(chǎng)景就稍微復(fù)雜一點(diǎn):
TiDB 中有一個(gè)核心的分布式 KV 存儲(chǔ)層 TiKV。TiKV 內(nèi)部基于 Multi-Raft 做一致性存儲(chǔ),這個(gè)架構(gòu)比較復(fù)雜,這里我們可以簡(jiǎn)化描述為一主多從的架構(gòu),Leader 寫入,F(xiàn)ollower 同步。而我們的場(chǎng)景是要對(duì) TiKV 做計(jì)劃性的運(yùn)維操作,比如滾動(dòng)升級(jí),遷移節(jié)點(diǎn)。
在這個(gè)場(chǎng)景下,盡管系統(tǒng)可以接受小于半數(shù)的節(jié)點(diǎn)宕機(jī),但對(duì)于預(yù)期性的停機(jī),我們要盡量做到優(yōu)雅停止。這是因?yàn)閿?shù)據(jù)庫(kù)場(chǎng)景本身就是非常嚴(yán)苛的,基本上都處于整個(gè)架構(gòu)的核心部分,因此我們要把抖動(dòng)做到越小越好。要做到這點(diǎn),就得做不少清理工作,比如說(shuō)我們要在停機(jī)前將當(dāng)前節(jié)點(diǎn)上的 Leader 全部遷移到其它節(jié)點(diǎn)上。
得益于系統(tǒng)的良好設(shè)計(jì),大多數(shù)時(shí)候這類操作都很快,然而分布式系統(tǒng)中異常是家常便飯,優(yōu)雅退出耗時(shí)過(guò)長(zhǎng)甚至失敗的場(chǎng)景是我們必須要考慮的。假如類似的事情發(fā)生了,為了業(yè)務(wù)穩(wěn)定和數(shù)據(jù)安全,我們就不能強(qiáng)制關(guān)閉 Pod,而應(yīng)該停止操作過(guò)程,通知工程師介入。 這時(shí),上面所說(shuō)的 Pod 退出流程就不再適用了。
這個(gè)問(wèn)題其實(shí) K8s 本身沒(méi)有開箱即用的解決方案,于是我們?cè)谧约旱?Controller 中(TiDB 對(duì)象本身就是一個(gè) CRD)與非常細(xì)致地控制了各種操作場(chǎng)景下的服務(wù)啟停邏輯。
拋開細(xì)節(jié)不談,最后的大致邏輯是在每次停服務(wù)前,由 Controller 通知集群進(jìn)行節(jié)點(diǎn)下線前的各種遷移操作,操作完成后,才真正下線節(jié)點(diǎn),并進(jìn)行下一個(gè)節(jié)點(diǎn)的操作。
而假如集群無(wú)法正常完成遷移等操作或耗時(shí)過(guò)久,我們也能“守住底線”,不會(huì)強(qiáng)行把節(jié)點(diǎn)干掉,這就保證了諸如滾動(dòng)升級(jí),節(jié)點(diǎn)遷移之類操作的安全性。
但這種辦法存在一個(gè)問(wèn)題就是實(shí)現(xiàn)起來(lái)比較復(fù)雜,我們需要自己實(shí)現(xiàn)一個(gè)控制器,在其中實(shí)現(xiàn)細(xì)粒度的控制邏輯并且在 Controller 的控制循環(huán)中不斷去檢查能否安全停止 Pod。
復(fù)雜的邏輯總是沒(méi)有簡(jiǎn)單的邏輯好維護(hù),同時(shí)寫 CRD 和 Controller 的開發(fā)量也不小,能不能有一種更簡(jiǎn)潔,更通用的邏輯,能實(shí)現(xiàn)“保證優(yōu)雅關(guān)閉(否則不關(guān)閉)”的需求呢?
有,辦法就是 ValidatingAdmissionWebhook。
這里先介紹一點(diǎn)點(diǎn)背景知識(shí),Kubernetes 的 apiserver 一開始就有 AdmissionController 的設(shè)計(jì),這個(gè)設(shè)計(jì)和各類 Web 框架中的 Filter 或 Middleware 很像,就是一個(gè)插件化的責(zé)任鏈,責(zé)任鏈中的每個(gè)插件針對(duì) apiserver 收到的請(qǐng)求做一些操作或校驗(yàn)。舉兩個(gè)插件的例子:
DefaultStorageClass
,為沒(méi)有聲明 storageClass 的 PVC 自動(dòng)設(shè)置 storageClass。
ResourceQuota
,校驗(yàn) Pod 的資源使用是否超出了對(duì)應(yīng) Namespace 的 Quota。
雖然說(shuō)這是插件化的,但在 1.7 之前,所有的 plugin 都需要寫到 apiserver 的代碼中一起編譯,很不靈活。而在 1.7 中 K8s 就引入了 Dynamic Admission Control 機(jī)制,允許用戶向 apiserver 注冊(cè) webhook,而 apiserver 則通過(guò) webhook 調(diào)用外部 server 來(lái)實(shí)現(xiàn) filter 邏輯。1.9 中,這個(gè)特性進(jìn)一步做了優(yōu)化,把 webhook 分成了兩類: MutatingAdmissionWebhook
和 ValidatingAdmissionWebhook
,顧名思義,前者就是操作 api 對(duì)象的,比如上文例子中的 DefaultStroageClass
,而后者是校驗(yàn) api 對(duì)象的,比如 ResourceQuota
。拆分之后,apiserver 就能保證在校驗(yàn)(Validating)之前先做完所有的修改(Mutating),下面這個(gè)示意圖非常清晰:
而我們的辦法就是,利用 ValidatingAdmissionWebhook
,在重要的 Pod 收到刪除請(qǐng)求時(shí),先在 webhook server 上請(qǐng)求集群進(jìn)行下線前的清理和準(zhǔn)備工作,并直接返回拒絕。這時(shí)候重點(diǎn)來(lái)了,Control Loop 為了達(dá)到目標(biāo)狀態(tài)(比如說(shuō)升級(jí)到新版本),會(huì)不斷地進(jìn)行 reconcile,嘗試刪除 Pod,而我們的 webhook 則會(huì)不斷拒絕,除非集群已經(jīng)完成了所有的清理和準(zhǔn)備工作。
下面是這個(gè)流程的分步描述:
用戶更新資源對(duì)象。
controller-manager watch 到對(duì)象變更。
controller-manager 開始同步對(duì)象狀態(tài),嘗試刪除第一個(gè) Pod。
apiserver 調(diào)用外部 webhook。
webhook server 請(qǐng)求集群做 tikv-1 節(jié)點(diǎn)下線前的準(zhǔn)備工作(這個(gè)請(qǐng)求是冪等的),并查詢準(zhǔn)備工作是否完成,假如準(zhǔn)備完成,允許刪除,假如沒(méi)有完成,則拒絕,整個(gè)流程會(huì)因?yàn)?controller manager 的控制循環(huán)回到第 2 步。
好像一下子所有東西都清晰了,這個(gè) webhook 的邏輯很清晰,就是要保證所有相關(guān)的 Pod 刪除操作都要先完成優(yōu)雅退出前的準(zhǔn)備,完全不用關(guān)心外部的控制循環(huán)是怎么跑的,也因此它非常容易編寫和測(cè)試,非常優(yōu)雅地滿足了我們“保證優(yōu)雅關(guān)閉(否則不關(guān)閉)”的需求,目前我們正在考慮用這種方式替換線上的舊方案。
其實(shí) Dynamic Admission Control 的應(yīng)用很廣,比如 Istio 就是用 MutatingAdmissionWebhook
來(lái)實(shí)現(xiàn) envoy 容器的注入的。從上面的例子中我們也可以看到它的擴(kuò)展能力很強(qiáng),而且常常能站在一個(gè)正交的視角上,非常干凈地解決問(wèn)題,與其它邏輯做到很好的解耦。
當(dāng)然了,Kubernetes 中還有 非常多的擴(kuò)展點(diǎn),從 kubectl 到 apiserver,scheduler,kubelet(device plugin,flexvolume),自定義 Controller 再到集群層面的網(wǎng)絡(luò)(CNI),存儲(chǔ)(CSI)可以說(shuō)是處處可以做事情。以前做一些常規(guī)的微服務(wù)部署對(duì)這些并不熟悉也沒(méi)用過(guò),而現(xiàn)在面對(duì) TiDB 這樣復(fù)雜的分布式系統(tǒng),尤其在 Kubernetes 對(duì)有狀態(tài)應(yīng)用和本地存儲(chǔ)的支持還不夠好的情況下,得在每一個(gè)擴(kuò)展點(diǎn)上去悉心考量,做起來(lái)非常有意思。
看完上述內(nèi)容,你們對(duì)Kubernetes中如何保證優(yōu)雅地停止Pod有進(jìn)一步的了解嗎?如果還想了解更多知識(shí)或者相關(guān)內(nèi)容,請(qǐng)關(guān)注億速云行業(yè)資訊頻道,感謝大家的支持。
免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場(chǎng),如果涉及侵權(quán)請(qǐng)聯(lián)系站長(zhǎng)郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。