如何優(yōu)雅地關閉Kubernetes集群中的Pod
原文標題:Gracefully Shutting Down Pods in a Kubernetes Cluster
發(fā)布時間:Jan 26, 2019
原文鏈接:https://blog.gruntwork.io/zero-downtime-server-updates-for-your-kubernetes-cluster-902009df5b33
文章作者:yorinasub17
這是我們實現(xiàn) Kubernetes 集群零停機時間更新的第二部分。在本系列的第一部分中,我們列舉出了簡單粗暴地使用kubectl drain 命令清除集群節(jié)點上的 Pod 的問題和挑戰(zhàn)。在這篇文章中,我們將介紹解決這些問題和挑戰(zhàn)的手段之一:優(yōu)雅地關閉 Pod。
Pod驅(qū)逐的生命周期
默認情況下,kubectl drain命令驅(qū)逐節(jié)點上的 Pod 時會遵循 Pod 的生命周期,這意味著整個過程會遵守以下規(guī)則:
kubectl drain將向控制中心發(fā)出刪除目標節(jié)點上的 Pod 的請求。隨后,請求將通知目標節(jié)點上的kubelet開始關閉 Pod。節(jié)點上的 kubelet將會調(diào)用 Pod 里的preStop鉤子。當 preStop鉤子執(zhí)行完成后,節(jié)點上的kubelet會向Pod容器中運行的程序發(fā)送TERM信號 (SIGTERM)。節(jié)點上的 kubelet將最多等待指定的寬限期(在pod上指定,或從命令行傳入;默認為30秒)然后關閉容器,然后強行終止進程(使用SIGKILL)。注意,這個寬限期包括執(zhí)行preStop鉤子的時間。
譯注:Kubelet 終止Pod前的等待寬限期有兩種方式指定
在Pod定義里通過Pod模板的spec.terminationGracePeriodSeconds 設定 kubectl delete pod {podName} --grace-period=60
基于此流程,我們可以利用應用程序 Pod 中的preStop鉤子和信號處理來正常關閉應用程序,以便在最終終止應用程序之前對其進行“清理”。例如,假如有一個工作進程從隊列中讀取信息然后處理任務,我們可以讓應用程序捕獲 TERM 系統(tǒng)信號,以指示該應用程序應停止接受新任務,并在所有當前任務完成后停止運行?;蛘撸绻\行的應用程序無法修改以捕獲 TERM 信號(例如第三方應用程序),則可以使用preStop鉤子來實現(xiàn)該服務提供的自定義API,來正常關閉應用。
在我們的示例中,Nginx 默認情況下不能處理 TERM 信號,因此,我們將改為依靠 Pod 的 preStop鉤子實現(xiàn)正常停止Nginx。我們將修改資源定義,將生命周期鉤子添加到容器的spec定義中,如下所示:
lifecycle:
preStop:
exec:
command: [
# Gracefully shutdown nginx
"/usr/sbin/nginx", "-s", "quit"
]
應用此配置后,在將 TERM 信號發(fā)送給容器中的Nginx進程之前,kebulet 調(diào)用 Pod 的生命周期鉤子發(fā)出命令 / usr / sbin / nginx -s quit。請注意,由于該命令將會正常停止 Nginx 進程和 Pod,因此 TERM 信號實際上在這個例子中是一個空操作。
在定義文件添加了生命周期鉤子后,整個 Deployment 資源的定義變成了下面這樣
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx-deployment
labels:
app: nginx
spec:
replicas: 2
selector:
matchLabels:
app: nginx
template:
metadata:
labels:
app: nginx
spec:
containers:
- name: nginx
image: nginx:1.15
ports:
- containerPort: 80
lifecycle:
preStop:
exec:
command: [
# Gracefully shutdown nginx
"/usr/sbin/nginx", "-s", "quit"
]
停機后的后續(xù)流量
使用上面的preStop鉤子正常關閉 Pod 可以確保 Nginx 在處理完現(xiàn)存流量有才會停止。但是,你可能會發(fā)現(xiàn),Nginx 容器在關閉后仍會繼續(xù)接收到流量,從而導致服務出現(xiàn)停機時間。
為了了解造成這個問題的原因,讓我們來看一個示例圖。假定該節(jié)點已接收到來自客戶端的流量。應用程序會產(chǎn)生一個工作線程來處理請求。我們用在 Nginx Pod 示例圖內(nèi)的圓圈表示該工作線程。

假設在工作線程處理請求的同時,集群的運維人員決定對 Node1 進行維護。運維運行了kubectl drain node-1 后,節(jié)點上的kubelet 會執(zhí)行 Pod 設置的preStop鉤子,開始進入Nginx進程正常關閉的流程。

由于 Nginx 仍要處理已存流量的請求,所以進入正常關閉流程后 Nginx 不會馬上終止進程,但是會拒絕處理后續(xù)到達的流量,向新請求返回錯誤。
在這個時間點,假設一個新的服務請求到達了 Pod 上層的 Service,因為此時 Pod 仍然是上層 Service 的Endpoint,所以這個即將關閉的 Pod 仍然可能會接收到 Service 分發(fā)過來的請求。如果 Pod 真的接收到了分發(fā)過來的新請求 Nginx 就會拒絕處理并返回錯誤。
譯注:推薦閱讀學練結(jié)合快速掌握K8s Service控制器

最終 Nginx 將完成對原始已存請求的處理,隨后kubelet會刪除 Pod,節(jié)點完成排空。


為什么會這樣呢?如何避免在Pod執(zhí)行關閉期間接受到來自客戶端的請求呢?在本系列的下一部分中,我們會更詳細地介紹 Pod 的生命周期,并給出如何在 preStop 鉤子中引入延遲為 Pod 進行摘流,以減輕來自 Service 的后續(xù)流量的影響。
