![](https://img.laitimes.com/img/_0nNw4CM6IyYiwiM6ICdiwiI0gTMx81dsQWZ4lmZf1GLlpXazVmcvwFciV2dsQXYtJ3bm9CX9s2RkBnVHFmb1clWvB3MaVnRtp1XlBXe0xCMy81dvRWYoNHLwEzX5xCMx8FesU2cfdGLwMzX0xiRGZkRGZ0Xy9GbvNGLpZTY1EmMZVDUSFTU4VFRR9Fd4VGdsYTMfVmepNHLrJXYtJXZ0F2dvwVZnFWbp1zczV2YvJHctM3cv1Ce-cmbw5SOykDNyIzY0IGOwUjN2ImZyYzX2MTM0cTM2EzLchDMyIDMy8CXn9Gbi9CXzV2Zh1WavwVbvNmLvR3YxUjLyM3Lc9CX6MHc0RHaiojIsJye.png)
Pod Hook
Kubernetes 為我們的容器提供了生命周期鈎子,就是我們說的
Pod Hook
,Pod Hook 是由 kubelet 發起的,當容器中的程序啟動前或者容器中的程序終止之前運作,這是包含在容器的生命周期之中。我們可以同時為 Pod 中的所有容器都配置 hook。
Kubernetes 為我們提供了兩種鈎子函數:
- PostStart:這個鈎子在容器建立後立即執行。但是,并不能保證鈎子将在容器
之前運作,因為沒有參數傳遞給處理程式。主要用于資源部署、環境準備等。不過需要注意的是如果鈎子花費太長時間以至于不能運作或者挂起, 容器将不能達到ENTRYPOINT
狀态。(PostStart 可以在容器啟動之後就執行。但需要注意的是,此 hook 和容器裡的 ENTRYPOINT 指令的執行順序是不确定的。)running
- PreStop:這個鈎子在容器終止之前立即被調用。它是阻塞的,意味着它是同步的, 是以它必須在删除容器的調用發出之前完成。主要用于優雅關閉應用程式、通知其他系統等。如果鈎子在執行期間挂起, Pod階段将停留在
狀态并且永不會達到running
狀态。(PreStop 則在容器被終止之前被執行,是一種阻塞式的方式。執行完成後,Kubelet 才真正開始銷毀容器。)failed
如果
PostStart
或者
PreStop
鈎子失敗, 它會殺死容器。是以我們應該讓鈎子函數盡可能的輕量。當然有些情況下,長時間運作指令是合理的, 比如在停止容器之前預先儲存狀态。
prestop
當使用者請求删除含有 pod 的資源對象時,K8S 為了讓應用程式優雅關閉(即讓應用程式完成正在處理的請求後,再關閉軟體),K8S提供兩種資訊通知:
- 預設:K8S 通知 node 執行
指令,docker 會先向容器中docker stop
為1的程序發送系統信号PID
,然後等待容器中的應用程式終止執行,如果等待時間達到設定的逾時時間,或者預設逾時時間(30s),會繼續發送SIGTERM
的系統信号強行 kill 掉程序。SIGKILL
- 使用 pod 生命周期(利用
回調函數),它執行在發送終止信号之前。PreStop
預設所有的優雅退出時間都在30秒内。kubectl delete 指令支援
--grace-period=<seconds>
選項,這個選項允許使用者用他們自己指定的值覆寫預設值。值’0’代表 強制删除 pod. 在 kubectl 1.5 及以上的版本裡,執行強制删除時必須同時指定
--force --grace-period=0
。
強制删除一個 pod 是從叢集狀态還有 etcd 裡立刻删除這個 pod。 當 Pod 被強制删除時, api 伺服器不會等待來自 Pod 所在節點上的 kubelet 的确認資訊:pod 已經被終止。在 API 裡 pod 會被立刻删除,在節點上, pods 被設定成立刻終止後,在強行殺掉前還會有一個很小的寬限期。
同 readinessProbe一樣,hook 也有類似的 Handler:
- Exec 用來執行 Shell 指令;
- HTTPGet 可以執行 HTTP 請求。
我們來看個例子:
[root@k8s-master ~]# mkdir -p /data/nginx/html
[root@k8s-master ~]# cat hook.yml
apiVersion: v1
kind: Pod
metadata:
name: lifecycle-demo
namespace: default
spec:
containers:
- name: lifecycle-demo-container
image: nginx:1.19
ports:
- containerPort: 80
volumeMounts:
- name: message
mountPath: /usr/share/nginx/html
lifecycle:
postStart:
exec:
command: ["/bin/sh", "-c", "echo Hello from the postStart handler > /usr/share/nginx/html/index.html"]
preStop:
exec:
command: ["/bin/sh","-c","echo Hello from the preStop handler > /usr/share/nginx/html/index.html"]
volumes:
- name: message
hostPath:
path: /data/nginx/html
[root@k8s-master ~]# kubectl get pod -o wide
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
lifecycle-demo 1/1 Running 0 13s 10.244.0.18 k8s-master <none> <none>
[root@k8s-master ~]# curl 10.244.0.18:80
Hello from the postStart handler
[root@k8s-master html]# cat index.html
Hello from the postStart handler
如果将容器銷毀 ,PreStop這個鈎子在容器終止之前立即被調用,可以看到結果如下
[root@k8s-master ~]# kubectl delete -f hook.yml
pod "lifecycle-demo" deleted
[root@k8s-master html]# cat index.html
Hello from the preStop handler
我們可以借助
preStop
以優雅的方式停掉 Nginx 服務,進而避免強制停止容器,造成正在處理的請求無法響應。
preStop:
exec:
command: ["/usr/sbin/nginx","-s","quit"]
總結
建立容器後,Kubernetes立即發送postStart事件。但是,不能保證在調用Container的入口點之前先調用postStart處理程式。postStart處理程式相對于Container的代碼異步運作,但是Kubernetes對容器的管理會阻塞,直到postStart處理程式完成。在postStart處理程式完成之前,容器的狀态不會設定為RUNNING。
Kubernetes會在容器終止之前立即發送preStop事件。除非Pod的寬限期到期,否則Kubernetes對Container的管理将一直保持到preStop處理程式完成為止。
注意: Kubernetes僅在Pod終止時才發送preStop事件。這意味着在Pod完成時不會調用preStop挂鈎。
Ingress Controller 的yml檔案裡面定義(使用鈎子在程式關閉之前做了一定的處理)
lifecycle:
preStop:
exec:
command:
- /wait-shutdown
[root@master ~]# kubectl exec -it nginx-ingress-controller-pmmzd -n ingress-nginx bash
bash-5.0$ cd /
bash-5.0$ pwd
/
bash-5.0$ ls wait-shutdown
wait-shutdown
Q:滾動更新期間造成流量丢失
A:滾動更新觸發,Pod在删除過程中,有些節點kube-proxy還沒來得及同步iptables規則,進而部分流量請求到Terminating的Pod上,導緻請求出錯。
解決辦法:配置preStop回調,在容器終止前優雅暫停5秒,給kube-proxy多預留一點時間。
當我們更新deployment的鏡像的時候,會觸發滾動更新,這是預設的更新政策,關掉一個處于term狀态,也就是不能處理請求,馬上關閉了
Kube-proxy是負責更新規則的,是有周期性的更新規則,如果還沒有來得及更新規則還是用的之前的IP,那麼後面來的流量再配置設定過來就會失效導緻請求的出錯
這個問題的産生是下線一個pod與kube-proxy重新整理轉發的規則這個時間差并不是保持同步的,也就是這個pod被下線掉了,馬上就如kube-proxy去更新轉發的規則。這不是同步的,因為kube-proxy是周期性的去同步。
這種情況主要适用于走service的服務,微服務沒有走service是沒有的,部署單體應用是很容易碰見這種情況
優雅停止(Gracful Shutdown)與 502/504 報錯
如果 Pod 正在處理大量請求(比如 1000 QPS+)時,因為節點故障或「競價節點」被回收等原因被重新排程, 你可能會觀察到在容器被 terminate 的一段時間内出現少量 502/504。
為了搞清楚這個問題,需要先了解清楚 terminate 一個 Pod 的流程:
- Pod 的狀态被設為
,(幾乎)同時該 Pod 被從所有關聯的 Service Endpoints 中移除Terminating
-
鈎子被執行preStop
- 它的執行階段很好了解:在容器被 stop 之前執行
- 它可以是一個指令,或者一個對 Pod 中容器的 http 調用
- 如果在收到 SIGTERM 信号時,無法優雅退出,要支援優雅退出比較麻煩的話,用
實作優雅退出是一個非常好的方式preStop
- preStop 的定義位置:https://github.com/kubernetes/api/blob/master/core/v1/types.go#L2515
-
執行完畢後,SIGTERM 信号被發送給 Pod 中的所有容器preStop
- 繼續等待,直到容器停止,或者逾時
,這個值預設為 30sspec.terminationGracePeriodSeconds
- 需要注意的是,這個優雅退出的等待計時是與
同步開始的!而且它也不會等待 preStop
結束!preStop
- 如果超過了
容器仍然沒有停止,k8s 将會發送 SIGKILL 信号給容器spec.terminationGracePeriodSeconds
- 程序全部終止後,整個 Pod 完全被清理掉
注意:1 跟 2 兩個工作是異步發生的,是以在未設定
preStop
時,可能會出現「Pod 還在 Service Endpoints 中,但是
SIGTERM
已經被發送給 Pod 導緻容器都挂掉」的情況,我們需要考慮到這種狀況的發生。
了解了上面的流程後,我們就能分析出兩種錯誤碼出現的原因:
- 502:應用程式在收到 SIGTERM 信号後直接終止了運作,導緻部分還沒有被處理完的請求直接中斷,代理層傳回 502 表示這種情況
- 504:Service Endpoints 移除不夠及時,在 Pod 已經被終止後,仍然有個别請求被路由到了該 Pod,得不到響應導緻 504
通常的解決方案是,在 Pod 的
preStop
步驟加一個 15s 的等待時間。其原理是:在 Pod 處理 terminating 狀态的時候,就會被從 Service Endpoints 中移除,也就不會再有新的請求過來了。在
preStop
等待 15s,基本就能保證所有的請求都在容器死掉之前被處理完成(一般來說,絕大部分請求的處理時間都在 300ms 以内吧)。
一個簡單的示例如下,它使 Pod 被 Terminate 時,總是在 stop 前先等待 15s,再發送 SIGTERM 信号給容器:
containers:
- name: my-app
# 添加下面這部分
lifecycle:
preStop:
exec:
command:
- /bin/sleep
- "15"
更好的解決辦法,是直接等待所有 tcp 連接配接都關閉(需要鏡像中有 netstat):
containers:
- name: my-app
# 添加下面這部分
lifecycle:
preStop:
exec:
command:
- /bin/sh
- -c
- "while [ $(netstat -plunt | grep tcp | wc -l | xargs) -ne 0 ]; do sleep 1; done"
如果我的服務還使用了 Sidecar 代理網絡請求,該怎麼處理?
以服務網格 Istio 為例,在 Envoy 代理了 Pod 流量的情況下,502/504 的問題會變得更複雜一點——還需要考慮 Sidecar 與主容器的關閉順序:
- 如果在 Envoy 已關閉後,有新的請求再進來,将會導緻 504(沒人響應這個請求了)
- 是以 Envoy 最好在 Terminating 至少 3s 後才能關,確定 Istio 網格配置已完全更新
- 如果在 Envoy 還沒停止時,主容器先關閉,然後又有新的請求再進來,Envoy 将因為無法連接配接到 upstream 導緻 503
- 是以主容器也最好在 Terminating 至少 3s 後,才能關閉。
- 如果主容器處理還未處理完遺留請求時,Envoy 或者主容器的其中一個停止了,會因為 tcp 連接配接直接斷開連接配接導緻 502
- 是以 Envoy 必須在主容器處理完遺留請求後(即沒有 tcp 連接配接時),才能關閉
containers:
- name: istio-proxy
# 添加下面這部分
lifecycle:
preStop:
exec:
command:
- /bin/sh
- -c
- "while [ $(netstat -plunt | grep tcp | grep -v envoy | wc -l | xargs) -ne 0 ]; do sleep 1; done"
參考
- Kubernetes best practices: terminating with grace
- Graceful shutdown in Kubernetes is not always trivial