在Kubernetes中實(shí)現(xiàn)優(yōu)雅退出
隨著持續(xù)部署(Continously Deployment)在項(xiàng)目中的使用,之前定期或者固定時(shí)間的發(fā)布節(jié)奏變?yōu)榱穗S時(shí)高頻率的發(fā)布。這就要求每次發(fā)布都應(yīng)該是零停機(jī)部署(Zero Downtime Deployment),否則將會(huì)引入bug。k8s中有一套完整的機(jī)制保證我們的應(yīng)用能夠?qū)崿F(xiàn)零停機(jī)部署,本文將重點(diǎn)分析其中的優(yōu)雅退出部分。
本文需要對(duì)k8s的架構(gòu)和核心組件的職責(zé)有一定的了解,如不了解可參考 Kubernetes Components。
發(fā)現(xiàn)問題
對(duì)于Kubernetes Deployment的每次部署過程,都是新版本的Pod創(chuàng)建、老版本的Pod刪除的過程。
在這個(gè)過程中如果不使用優(yōu)雅退出,則會(huì)引發(fā)兩個(gè)問題:
- 問題1:可能會(huì)出現(xiàn)Pod未將正在處理的請(qǐng)求處理完成的情況下被刪除,如果該請(qǐng)求不是冪等性的,則會(huì)導(dǎo)致狀態(tài)不一致的bug。
- 問題2:可能會(huì)出現(xiàn)Pod已經(jīng)被刪除,Kubernetes仍然將流量導(dǎo)向該P(yáng)od,從而出現(xiàn)用戶請(qǐng)求處理失敗,帶來比較差的用戶體驗(yàn)。
分析問題
在Kubernetes Pod 的刪除過程中,同時(shí)會(huì)存在兩條并行的時(shí)間線,如下圖所示,其中一條時(shí)間線是網(wǎng)絡(luò)規(guī)則的更新過程,另一條時(shí)間線是Pod的刪除過程。
當(dāng)用戶執(zhí)行 kubectl delete pod
命令時(shí),
網(wǎng)絡(luò)規(guī)則生效流程:
- Kube-apiserver會(huì)收到Pod刪除的請(qǐng)求,在Etcd中更新Pod的狀態(tài)為Terminating;
- Endpoint Controller將該P(yáng)od的ip從Endpoint對(duì)象中刪除;
- Kube-proxy根據(jù)Endpoint對(duì)象的改變更新iptables規(guī)則,不再將流量路由到被刪除的Pod。
Pod刪除流程:
- Kube-apiserver 會(huì)收到Pod刪除的請(qǐng)求,在Etcd中更新Pod的狀態(tài)為Terminating;
- Kubelet在節(jié)點(diǎn)上清理容器的相關(guān)資源,例如存儲(chǔ),網(wǎng)絡(luò);
- Kubelet發(fā)送SIGTERM進(jìn)程給容器,如果容器中的進(jìn)程未做任何配置,則容器立即退出;
- 如果容器未在默認(rèn)的30秒時(shí)間內(nèi)退出,Kubelet發(fā)送SIGKILL給容器,強(qiáng)制讓容器退出。
從Pod的刪除過程可以知道,如果不對(duì)容器內(nèi)的進(jìn)程進(jìn)行任何配置,容器會(huì)立即退出,會(huì)導(dǎo)致問題1出現(xiàn)。
由于網(wǎng)絡(luò)規(guī)則的更新和Pod的刪除是并行的,所以并不能保證網(wǎng)絡(luò)規(guī)則的更新時(shí)間一定會(huì)早于Pod的刪除時(shí)間,所以,有可能出現(xiàn)問題2。
解決問題
如果要解決以上兩個(gè)問題,我們需要做如下配置
-
設(shè)置容器中進(jìn)程的優(yōu)雅退出;
-
增加preStopHook;
-
修改terminationGracePeriodSeconds。
配置后的時(shí)間線如下圖所示:
設(shè)置容器中進(jìn)程的優(yōu)雅退出
在我們項(xiàng)目中,使用的是Springboot,只需要在Springboot的配置文件中增加配置
- server:
- shutdown: graceful
- spring:
- lifecycle:
- timeout-per-shutdown-phase: 30s
進(jìn)行該配置后,Springboot會(huì)保證在接收到SIGTERM之后不會(huì)再接受新的請(qǐng)求[^1], 并在超時(shí)時(shí)間內(nèi)處理完所有正在處理的請(qǐng)求,如果不能處理完成,也會(huì)打印出相應(yīng)的信息并強(qiáng)制退出。超時(shí)時(shí)間的具體值應(yīng)該參考系統(tǒng)中最大允許的請(qǐng)求時(shí)長(zhǎng),所以理論上所有的請(qǐng)求都應(yīng)該在30s內(nèi)處理完,對(duì)于沒有在30s內(nèi)處理完成的請(qǐng)求,我們可以通過監(jiān)控日志然后發(fā)Alert的方式,根據(jù)實(shí)際情況去處理。通過增加此配置,可以解決問題1。對(duì)于使用其它的語(yǔ)言和框架的項(xiàng)目,應(yīng)該也存在類似的配置。
增加preStopHook
針對(duì)問題2,需保證網(wǎng)絡(luò)規(guī)則更新后,也就是說新的流量不再路由到要?jiǎng)h除的Pod后,再開始Pod的刪除。所以需要在 Kubernetes 的Yaml文件中增加 preStopHook[^3],讓Kubelet接收到Pod刪除事件后等待一段時(shí)間,給Kube-proxy足夠的時(shí)間更新iptables網(wǎng)絡(luò)規(guī)則后,再開始刪除Pod。
- lifecycle:
- preStop:
- exec:
- command: ["sh", "-c", "sleep 10"] # set prestop hook
這里在我們項(xiàng)目中設(shè)置的10s是參照Springboot官網(wǎng)的配置[^2]。
修改terminationGracePeriodSeconds
參照之前分析的Pod刪除的流程,Kubernetes會(huì)給容器最大的刪除時(shí)長(zhǎng)是30秒[^3],如果我們?cè)赟pring中優(yōu)雅退出的超時(shí)時(shí)長(zhǎng)和在Kubernetes中的preStopHook時(shí)長(zhǎng)大于30s,則可能會(huì)出現(xiàn)Springboot還未處理完所有的請(qǐng)求Kubernetes已經(jīng)開始強(qiáng)制刪除容器。所以如果這個(gè)時(shí)長(zhǎng)大于30秒,我們需要修改 terminationGracePeriodSeconds使其大于Springboot的優(yōu)雅退出超時(shí)時(shí)間和preStopHook之和
- terminationGracePeriodSeconds: 45
最終Kubernetes中的Yaml文件如下所示:
- apiVersion: apps/v1
- kind: Deployment
- metadata:
- name: graceful-shutdown-test-exit-graceful-30s
- spec:
- replicas: 2
- selector:
- matchLabels:
- app: graceful-shutdown-test-exit-graceful-30s
- template:
- metadata:
- labels:
- app: graceful-shutdown-test-exit-graceful-30s
- spec:
- containers:
- - name: graeceful-shutdonw-test
- image: graceful-shutdown-test-exit-graceful-30s:latest
- ports:
- - containerPort: 8080
- lifecycle:
- preStop:
- exec:
- command: ["sh", "-c", "sleep 10"] # set prestop hook
- terminationGracePeriodSeconds: 45 # terminationGracePeriodSeconds
可以看出,通過設(shè)置Springboot的優(yōu)雅退出,保證了正在處理的請(qǐng)求能夠處理完成,通過設(shè)置preStopHook,保證了Pod刪除和網(wǎng)絡(luò)規(guī)則更新的時(shí)序關(guān)系。通過配置terminationGracePeriodSeconds,給了容器中進(jìn)程足夠的時(shí)間處理所有的請(qǐng)求。綜合以上三個(gè)步驟,可以解決之前發(fā)現(xiàn)的兩個(gè)問題。
總結(jié)
本文通過結(jié)合Kubernetes的優(yōu)雅退出和Springboot的優(yōu)雅退出機(jī)制,保證在隨時(shí)高頻率部署的情況下,服務(wù)也可以正確處理所有的請(qǐng)求,減少了bug的出現(xiàn),提升了用戶的體驗(yàn)。