K8s Pod優(yōu)雅關(guān)閉,沒你想象的那么簡(jiǎn)單!
更新部署服務(wù)時(shí),舊的 Pod 會(huì)終止,新 Pod 上位。如果在這個(gè)部署過程中老 Pod 有一個(gè)很長的操作,我們想在這個(gè)操作成功完成后殺死這個(gè) pod(優(yōu)雅關(guān)閉),如果無法做到的話,被殺死的 pod 可能會(huì)丟失一定的流量,或者外界無法感知到該 Pod 被殺死。特別是,如果我們有一個(gè)接收大量流量的 API,錯(cuò)誤率在部署過程中會(huì)顯著增加。
其實(shí)這也挺簡(jiǎn)單的,添加一個(gè)優(yōu)雅關(guān)閉就行了,之前寫過優(yōu)雅關(guān)閉的最佳實(shí)踐K8S Pod流量的優(yōu)雅無損切換實(shí)踐,后來在發(fā)現(xiàn)還是不夠優(yōu)雅........

當(dāng) Kubernetes 殺死一個(gè) pod 時(shí),會(huì)發(fā)生以下 5 個(gè)步驟:
1、 Pod 切換到終止?fàn)顟B(tài)并停止接收任何新流量,容器仍在 pod 內(nèi)運(yùn)行。
2、 preStop 鉤子是一個(gè)特殊的命令或 HTTP 請(qǐng)求被執(zhí)行,并被發(fā)送到 pod 內(nèi)的容器。
3、 SIGTERM 信號(hào)被發(fā)送到 pod,容器意識(shí)到它將很快關(guān)閉。
4、 Kubernetes 等待寬限期 (terminationGracePeriodSeconds)。此等待與 preStop hook 和 SIGTERM 信號(hào)執(zhí)行并行(默認(rèn) 30 秒)。因此,Kubernetes 不會(huì)等待這些完成。如果這段時(shí)間結(jié)束,則直接進(jìn)入下一步。正確設(shè)置寬限期的值非常重要。
5、向 pod 發(fā)送 SIGKILL 信號(hào),然后移除 pod。如果容器在寬限期后仍在運(yùn)行,則 Pod 被 SIGKILL 強(qiáng)行移除,終止完成。
總結(jié)下大致分為兩步,第一步定義 preStop,一般情況下可以休眠 30s,用于處理殘余流量;第二步發(fā)送 SIGTERM 信號(hào),服務(wù)收到信號(hào)后進(jìn)行服務(wù)的收尾工作處理。比如:關(guān)閉連接、通知第三方注冊(cè)中心服務(wù)關(guān)閉.....
有同學(xué)疑問,既然 pod 已經(jīng)終止了,同時(shí) K8s 的網(wǎng)絡(luò) endpoint 也摘除了,為什么還會(huì)進(jìn)來流量呢?
因?yàn)檫@個(gè)網(wǎng)絡(luò)接口的摘除是異步的,這也是為什么會(huì)首先執(zhí)行 preStop,然后發(fā)送 SIGTERM 信號(hào)的原因所在。
這樣做基本上能夠保證流量無損,但是這樣做的前提是服務(wù)能夠收到 SIGTERM 信號(hào)。
理想情況下,一個(gè)容器只有一個(gè)進(jìn)程,但是在現(xiàn)實(shí)場(chǎng)景下很難做到,比如,我會(huì)用一個(gè) shell 腳本去管理和啟動(dòng) Java 進(jìn)程,除了 shell 腳本主進(jìn)程之外,還要運(yùn)行監(jiān)控、日志收集等子進(jìn)程,這樣一個(gè)容器里面就運(yùn)行了多個(gè)進(jìn)程。

系統(tǒng)底層默認(rèn)會(huì)向主進(jìn)程發(fā)送 SIGTERM 信號(hào),而對(duì)剩余子進(jìn)程發(fā)送 SIGKILL 信號(hào)。系統(tǒng)這樣做的大概原因是因?yàn)榇蠹以谠O(shè)計(jì)主進(jìn)程腳本的時(shí)候都不會(huì)進(jìn)行信號(hào)的捕獲和傳遞,這會(huì)導(dǎo)致容器關(guān)閉時(shí),多個(gè)子進(jìn)程無法被正常終止,所以系統(tǒng)使用 SIGKILL 這個(gè)不可屏蔽信號(hào),而是為了能夠在沒有任何前提條件的情況下,能夠把容器中所有的進(jìn)程關(guān)掉。
具體可以使用strace -p pid去跟蹤服務(wù)調(diào)用情況。
也就是說如果主進(jìn)程自身不是服務(wù)本身,可能會(huì)導(dǎo)致是被強(qiáng)制Kill的,解決的方法也很簡(jiǎn)單,也就是在主進(jìn)程中對(duì)收到的信號(hào)做個(gè)轉(zhuǎn)發(fā),發(fā)送到容器中的其他子進(jìn)程,這樣容器中的所有進(jìn)程在停止時(shí),都會(huì)收到 SIGTERM,而不是 SIGKILL 信號(hào)了。
具體如何實(shí)現(xiàn)呢?比如下面的trap信號(hào),就是一種實(shí)現(xiàn)方式,這里有一篇最佳實(shí)踐http://veithen.io/2014/11/16/sigterm-propagation.html。
#startup.sh
...
trap 'kill -TERM $child' TERM
nohup java $JAVA_OPTS -jar ./xxx.jar --server.port=8080 &
child=$!
wait $child
wait $child
當(dāng)然很多成熟的框架都實(shí)現(xiàn)了優(yōu)雅關(guān)閉功能,比如spring的CustomHealthCheck類擴(kuò)展了AbstractHealthIndicator類,并允許我們通過覆蓋doHealthCheck()方法來構(gòu)建自定義健康檢查結(jié)構(gòu)。根據(jù)我們從HealthService收到的標(biāo)志,我們將系統(tǒng)的健康狀態(tài)設(shè)置為up或down。
這樣的話,我們可以通過preStop調(diào)用該接口實(shí)現(xiàn)另外一種方式的優(yōu)雅關(guān)閉。
lifecycle:
preStop:
httpGet:
path: /unhealthy
port: http
最后服務(wù)端收到優(yōu)雅關(guān)閉信號(hào)后可以進(jìn)行一些善后處理工作。
這就是K8s,自身很簡(jiǎn)單,但是它的低層牽涉了Linux內(nèi)核、進(jìn)程、網(wǎng)絡(luò)、存儲(chǔ)等方方面面的知識(shí),但并不會(huì)在Kubernetes的文檔中交代清楚??善褪撬鼈?,才是容器技術(shù)的精髓所在。
有收獲,點(diǎn)個(gè)在看 
