你的容器程式退出是自願的還是被自願的
最近注意到有時候執行kubectl delete pod xxx
的時候,這條命令要很長時間才返回,用time 命令看一下 居然要32秒,簡直令人髮指. 本著不知道就問,問不到就放棄的精神來分析一下原因.
1 刪除pod 的背後發生了什麼?
簡單來講,當用刪除pod 的時候,實際上是給pod 傳送了一個SIGTERM
訊號,告訴pod裡面PID為1的程序,給你一段時間,抓緊退出,類似kill -15
, 超過這個時間閾值PID為1的程序還沒退出的話,那不好意思, 直接殺死,這個對應的就是kill -9
,傳遞的訊號就是SIGKILL
.
說到這,就解釋的通為什麼刪除一個pod要這個長時間了,明顯是pod裡面的程序沒有在規定時間退出,超時後被強殺了,再一看delete pod
花的時間差不多,好了 問題解決,文章到此結束,謝謝大家.
要是放在我剛工作的時候,排錯到這裡就結束了,但是作為一名資深重啟工程師,我們還要繼續研究來裝做上班時間很忙,提升KPI.
2 為什麼容器裡面PID 為1的程序沒有處理SIGTERM訊號
這裡再把問題細分一下,容器裡面的PID 為1的程序是誰?他為什麼不處理SIGTERM訊號?
2.1 容器中的PID為1的程序是誰?
在非容器環境,也就是Linux作業系統中,PID為1的是systemd
程序,也是init程序,這個PID 使用者無法使用,但是在容器中PID為1的程序是可以指定的.
!!!!!下面這段話是
對於容器來說,init 系統不是必須的,當你通過命令 docker stop mycontainer 來停止容器時,docker CLI 會將 TERM 訊號傳送給 mycontainer 的 PID 為 1 的程序。 如果 PID 1 是 init 程序 - 那麼 PID 1 會將 TERM 訊號轉發給子程序,然後子程序開始關閉,最後容器終止。 如果沒有 init 程序 - 那麼容器中的應用程序(Dockerfile 中的 ENTRYPOINT 或 CMD 指定的應用)就是 PID 1,應用程序直接負責響應 TERM 訊號。這時又分為兩種情況: 應用不處理 SIGTERM - 如果應用沒有監聽 SIGTERM 訊號,或者應用中沒有實現處理 SIGTERM 訊號的邏輯,應用就不會停止,容器也不會終止。 容器停止時間很長 - 執行命令 docker stop mycontainer 之後,Docker 會等待 10s,如果 10s 後容器還沒有終止,Docker 就會繞過容器應用直接向核心傳送 SIGKILL,核心會強行殺死應用,從而終止容器。
!!!!!上面這段話是抄的!!!!!
我們不會討論容器裡面有init
程序的情況,在生產中不會這麼用,沒什麼意義.所以容器裡的PID為1的程序是由Dockerfile
裡面的ENTRYPOINT
或者CMD
定義的.
這兩個的使用有什麼區別呢?
2.1.1 ENTRYPOINT 和CMD的使用
牆裂推薦看看官方文件,什麼部落格都不如官方文件來的準確.
我這邊只講重要的知識點.
- 相同點
都是定義容器啟動後執行的一條命令
一個Dockerfile
中,如果定義多個,只有最後一個生效,
比如下面兩個Dockerfile
build 出來的映象執行後跑的第一條命令就是sleep
:
FROM ubuntu
ENTRYPOINT ["sleep", "1000"]
FROM ubuntu
CMD ["sleep", "1000"]
- 不同點
CMD
可作為ENTRYPOINT
的引數
CMD
可以被在docker run
的時候傳遞的命令覆蓋,ENTRYPOINT
不可以(為了方便debug,一般使用CMD
)
重點來了!
CMD
和ENTRYPOINT
都有兩種格式:
#exec 格式
1. ENTRYPOINT ["executable", "param1", "param2"]
#exec格式最終會被解析成json陣列,所以這裡必須是雙引號! exec格式的命令在容器執行時的PID 就是1
#shell格式
2. ENTRYPOINT command param1 param2
# shell格式的命令在容器執行時,會先執行/bin/sh -c, 然後把ENTRYPOINT定義的命令作為/bin/sh -c 的引數,
#比如:
FROM ubuntu
ENTRYPOINT sleep 1000
# ENTRYPOINT sleep 10000,在容器中的程序是/bin/sh -c slepp 10000
root@44aa8dfe6b74:/# ps aux
USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND
root 1 0.1 0.0 2600 648 pts/0 Ss+ 07:47 0:00 /bin/sh -c sleep 1000
root 6 0.0 0.0 2500 376 pts/0 S+ 07:47 0:00 sleep 1000
root 7 0.6 0.0 4100 2160 pts/1 Ss 07:47 0:00 bash
root 14 0.0 0.0 5888 1476 pts/1 R+ 07:47 0:00 ps aux
可以看出,使用shell格式的ENTRYPOINT,容器中PID 為1的程序是/bin/sh,我們的程式的PID為6,需要注意的是,/bin/sh -c 是不會傳遞unix訊號到我們的程式,當執行docker stop <container>
的時候,sleep程序是不會接收到SIGTERM訊號的,只能等待docker的10秒超時,然後傳送SIGKILL訊號強制刪除容器.
在shell
格式和exec
格式的使用上ENTRYPOINT
和CMD
並沒有什麼區別,我只是用ENTRYPOINT
舉例,換成CMD
是一樣的效果,推薦使用exec
格式的CMD
2.2 容器中PID為1的程序為什麼不處理SIGTERM訊號?
上面我們講到shell格式的ENTRYPOINT
在容器啟動後不會傳遞SIGTERM
訊號,所以我們用exec
格式的,那麼exec
格式的就一定能傳遞訊號了嗎?
話不多說,上程式碼,直接以生產中場景來介紹
場景1: 預設tomcat 映象
#在一個終端啟動tomcat 官方映象
docker run -it --rm tomcat
# 開啟另一個終端,
[root@localhost ~]# docker ps -l
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
0fcbf65caf6b tomcat "catalina.sh run" 4 seconds ago Up 3 seconds 8080/tcp elegant_tesla
從圖中可以看出PID為1的程序就是tomcat程序,由此可以推斷tomcat的Dockerfile
使用的是exec
格式.
來證明一下我們的推斷,
再去github 裡面檢視一下Dockerfile
# verify Tomcat Native is working properly
RUN set -eux; \
nativeLines="$(catalina.sh configtest 2>&1)"; \
nativeLines="$(echo "$nativeLines" | grep 'Apache Tomcat Native')"; \
nativeLines="$(echo "$nativeLines" | sort -u)"; \
if ! echo "$nativeLines" | grep -E 'INFO: Loaded( APR based)? Apache Tomcat Native library' >&2; then \
echo >&2 "$nativeLines"; \
exit 1; \
fi
EXPOSE 8080
CMD ["catalina.sh", "run"]
很明顯,官方的tomcat:latest
映象用了exec
格式的CMD
,那麼PID
為1的程序應該可以接收到SIGTERM
訊號,並且優雅的退出
果然,容器退接收到了訊號,退出只用了0.3秒, 如過容器沒有接收到SIGTERM
訊號的話,stop 容器應該在10秒以上
結論:
使用exec
格式的CMD
命令在容器中的PID為1,如果這個程序可以處理SIGTERM
訊號,那麼在stop 容器時,容器可以優雅的退出
場景2 : 封裝預設tomcat 映象
線上使用tomcat映象的時候, 不會直接使用官方的,會以官方的為基礎映象,增加些自定義的東西,比如修改配置檔案,升級映象安裝包,整合監控,日誌元件等等
啟動命令也不會用預設的CMD
裡面的,在啟動之前可能會配置一寫安全相關或者密碼相關的東西,比如從vault 獲取DB 密碼
這裡簡單模擬一下:
[root@localhost test]# ll
total 8
-rw-r--r-- 1 root root 47 Dec 25 10:59 Dockerfile
-rwxr-xr-x 1 root root 69 Dec 25 11:01 start.sh
[root@localhost test]# cat Dockerfile
FROM tomcat
COPY start.sh /
CMD ["/start.sh"] #這裡的CMD 會覆蓋FROM 映象的Dockerfile中的CDM,如果tomcatDockerfile中用的是ENTRYPOINT,那麼這裡的CMD將會作為ENTRYPOINT的引數,所以還是推薦用CMD方便擴充套件
[root@localhost test]# cat start.sh
#!/bin/bash
echo "do job1"
/usr/local/tomcat/bin/catalina.sh run
程式碼很簡單,把tomcat官方預設映象做為基礎映象,把我們自定義的start.sh
指令碼作為容器啟動命令,然後在start.sh
裡做環境準備,然後啟動tomncat.
下面來build 映象並且啟動
開啟另外一個終端,檢視容器中的程序情況
可以看到,我們使用了exec
格式的CMD
,start.sh
程序的PID為1,那麼這個容器是否能接收或者處理SIGTERM
訊號呢?
可以看出,docker stop
命令用了10秒鐘以上,證明這個容器是被強制刪除的
為什麼?不合理啊?
我們上面說到,docker stop
會向容器中PID為1的程序傳遞SIGTERM
訊號,但是這個PID為1的程序是不是要能接收並且處理這個訊號呢,如果不能訊號,那等於沒傳.
再來看一眼start.sh
[root@localhost test]# cat start.sh
#!/bin/bash
echo "do job1"
/usr/local/tomcat/bin/catalina.sh run
並沒有任何接收處理訊號的程式碼, 按照docker官方文件的做法,可以在start.sh
指令碼中用命令trap
捕獲SIGTERM
訊號,然後做相應的處理,有關trap的用法可參考這裡
下面是官方給的例子,
#!/bin/sh
# Note: I've written this using sh so it works in the busybox container too
# USE the trap if you need to also do manual cleanup after the service is stopped,
# or need to start multiple services in the one container
trap "echo TRAPed signal" HUP INT QUIT TERM
# start service in background here
/usr/sbin/apachectl start
echo "[hit enter key to exit] or run 'docker stop <container>'"
read
# stop service and clean up here
echo "stopping apache"
/usr/sbin/apachectl stop
echo "exited $0"
不過這裡要說的是,在啟動指令碼中去捕獲訊號沒什麼意義,因為有Kubernetes
大殺器,可以實現優雅退出容器.
總結
在官方映象之上修改如果自定義的啟動指令碼不能很好的處理SIGTERM訊號的話,容器仍然會被強制刪除,可以用trap去捕獲處理訊號,但是在kubernetes上有更好的元件處理這個問題
3 Kubernetes 中處理容器Graceful shutdown
3.1 復現30秒刪除容器問題
把上面build的映象放到Kubernetes裡面,yaml 如下
[root@localhost ~]# cat tomcat.yaml
apiVersion: apps/v1 # for versions before 1.9.0 use apps/v1beta2
kind: Deployment
metadata:
name: tomcat-deployment
spec:
selector:
matchLabels:
app: tomcat
replicas: 1
template:
metadata:
labels:
app: tomcat
spec:
containers:
- name: tomcat
image: tomcat:exec
imagePullPolicy: IfNotPresent
ports:
- containerPort: 8080
---
apiVersion: v1
kind: Service
metadata:
name: tomcat
spec:
selector:
app: tomcat
ports:
- protocol: TCP
port: 8080
targetPort: 8080
注意這裡的映象叫tomcat:exec
,就是上面build的映象,exec
就是tag
,你需要改成你自己的tag.
#建立資源
[root@localhost ~]# kubectl apply -f tomcat.yaml
deployment.apps/tomcat-deployment created
service/tomcat created
#檢視資源
[root@localhost ~]# kubectl get pod
NAME READY STATUS RESTARTS AGE
tomcat-deployment-77764cb96d-75fqh 1/1 Running 0 3m26s
這個時候開啟另外一個終端,我們要實時檢視容器的狀態
首先獲取tomcat:exec
的 image ID
然後 while true ; do docker ps -a|grep 6e3e4186246d; sleep 0.1; done
,實時檢視pod狀態
要注意觀看,關鍵資訊稍縱即逝.
切回到原來的終端刪除pod
[root@localhost ~]# time kubectl delete po tomcat-deployment-77764cb96d-75fqh
pod "tomcat-deployment-77764cb96d-75fqh" deleted
real 0m32.389s
user 0m0.079s
sys 0m0.043s
另外一個終端捕獲的資訊
可以看出刪除pod用了32秒,同時這個容器的退出狀態碼為137, 137表示容器中的程序是被SIGKILL
的,也就是強制刪除的.
狀態碼詳細介紹請看這裡
我們已經復現了了30秒刪除容器的問題,這個問題產生的原因有2點
- 容器
start.sh
沒有處理SIGTERM
訊號 - Kubernetes沒有在pod的生命週期中做處理
下面我們來了解一下pod的生命週期
3.2 容器生命週期及preStop
上圖展示了一個pod的生命週期,這裡我們只關注preStop
,它是在容器退出之前執行,並且只有在preSstop
執行完成之後,PID為1的程序才能接受到SIGTERM
訊號
換句話說,如果preStop
中的命令執行時間超過Kubernetes中預設的terminationGracePeriodSeconds
30秒,那麼容器中PID為1的程序將不能接收到SIGTERM
訊號,這個容器將會被SIGKILL
強制刪除
官方介紹:
This hook is called immediately before a container is terminated due to an API request or management event such as liveness probe failure,
preemption, resource contention and others. A call to the preStop hook fails if the container is already in terminated or completed state.
It is blocking, meaning it is synchronous, so it must complete before the signal to stop the container can be sent.
No parameters are passed to the handler.
我們來修改一下tomcat.yaml
,增加preStop
,再刪除容器,觀察它的退出狀態
[root@localhost ~]# cat tomcat.yaml
apiVersion: apps/v1 # for versions before 1.9.0 use apps/v1beta2
kind: Deployment
metadata:
name: tomcat-deployment
spec:
selector:
matchLabels:
app: tomcat
replicas: 1
template:
metadata:
labels:
app: tomcat
spec:
containers:
- name: tomcat
image: tomcat:exec
imagePullPolicy: IfNotPresent
ports:
- containerPort: 8080
lifecycle:
preStop:
exec:
command: ['/usr/local/tomcat/bin/catalina.sh',"stop"]
---
apiVersion: v1
kind: Service
metadata:
name: tomcat
spec:
selector:
app: tomcat
ports:
- protocol: TCP
port: 8080
targetPort: 8080
建立之前先刪除,當然也可以直接apply
,會自動覆蓋原有的配置
[root@localhost ~]# kubectl delete -f tomcat.yaml
[root@localhost ~]# kubectl apply -f tomcat.yaml
再次刪除pod, 同時觀察容器的退出狀態碼
這次刪除pod只用了4秒鐘,容器退出的狀態碼為0,意思是該容器沒有附加的前臺程序,表明容器完成了它的工作正常退出了
不過這個0有點出乎我意料,難道是退出的姿勢不對?
把preStop
修改一下
lifecycle:
preStop:
exec:
command: ['sh','-c',"kill -15 $(ps axu |grep tomcat |grep -v grep |awk '{print $2}')"]
這次直接給kill -15
的方式殺掉tomcat 程序,
檢視容器退出狀態碼
143,表示容器接收到了SIGTERM
訊號,這個訊號是Kubernetes傳的嗎? 不是! 是我們定義的preStop
裡面kill -15
傳的
感興趣的可以把preStop
裡面kill -15
換成kill -9
,試一下
那麼為什麼這個SIGTERM
訊號用的是preSstop
裡面的呢? 這就要研究一下刪除容器時究竟偷偷發生了什麼
總結:
preStop
定義的命令是在容器退出前執行的,容器優雅的退出狀態碼是143,強制刪除的狀態碼是137,至於上面退出狀態碼是0,我猜測可能是catalina.sh
導致的,需要再研究一下
3.3 刪除容器背後發生了什麼?
我們是不是需要知道pod建立是究竟建立了什麼, 才能知道要刪除什麼?
簡單列一下pod建立的過程:
- POST yaml 到API-server, API-server將其儲存到ETCD
- scheduler 經過一系列的演算法,選擇適合pod的節點, 將節點資訊彙報給API-server,存入ETCD
- 節點上的kubelet監聽到有pod被排程到自己節點,呼叫本機的CRI建立容器,繫結CNI(網路),掛載CSI(儲存)
- CNI為pod分配IP,kubelet將IP資訊彙報給API-server
- API-server更新pod狀態
如果pod
屬於一個service
- kubelet 等待pod readiness 成功
- Endpoints 新增pod ip:port 列表
- Kube-proxy根據Endpoints變更,更新防火牆規則
- 其他使用Endpoints的資源更新做相應更新,如DNS,Ingress,Cloud Loadbalancer等等
我們將上面這些步驟分了兩類,
第一: 管事類, 如Endpoints,API-server,ETCD,kube-proxy,iptables和一些controller的資源物件變更
第二: 一線搬磚類 kubelet 所在節點資源的變更,主要是容器
那麼刪除pod也應該刪除這兩個分類裡面的資源,這裡要注意的點是,兩個分類的資源是同時刪除的
上圖中1 和2的關係就像負載均衡後面的節點,我們要把節點從負載均衡中移除,是先把節點從負載均衡列表中刪除,在刪除節點,如果順序錯了就會導致502
如果kubelet 刪除pod的時候,endpoint
資源並沒有將pod的ip 從列表中剔除,這個時候仍然有流量到被分配到被刪除的pod的ip就會出問題了,一般情況下endpoint
會在pod刪除之前完成變更
但是如果叢集的API server
太繁忙,這個就不能保證了,怎麼辦? 等!
來看一下kubernetes
中的Graceful shutdown
前文已經提到,kubernetes
中的terminationGracePeriodSeconds
30秒是從preStop
開始計算的,所以preStop
的執行時間不能超過terminationGracePeriodSeconds
的預設值
還記得上面說的退出狀態碼143的pod中,SIGTERM
訊號是我們定義在preStop
中的kill -15
傳遞的疑問嗎?
當kubelet開始刪除pod的時候,會先執行preStop
,只有執行完preStop
才會向PID為1的程序傳遞SIGTERM
訊號,但是我們的yaml中在preStop
中就向程式傳遞了SIGTERM
(kill -15)訊號,程式就優雅退出了,
根本走不到上圖中的Graceful shutdown
這一步.
下面看一下優化後的tomcat.yaml
[root@localhost ~]# cat tomcat.yaml
apiVersion: apps/v1 # for versions before 1.9.0 use apps/v1beta2
kind: Deployment
metadata:
name: tomcat-deployment
spec:
selector:
matchLabels:
app: tomcat
replicas: 1
template:
metadata:
labels:
app: tomcat
spec:
containers:
- name: tomcat
image: tomcat:exec
imagePullPolicy: IfNotPresent
ports:
- containerPort: 8080
lifecycle:
preStop:
exec:
command: ['sh','-c',"sleep 10 && kill -15 $(ps axu |grep tomcat |grep -v grep |awk '{print $2}')"]
---
apiVersion: v1
kind: Service
metadata:
name: tomcat
spec:
selector:
app: tomcat
ports:
- protocol: TCP
port: 8080
targetPort: 8080
我們在preStop
中加了sleep 10
,然後再kill -15
容器中的程序,這樣給了10秒鐘的時間等待endpoint kube-proxy
以及其他一下"管事的"更新資源物件.
需要說明的是,如果使用了exec
格式的CMD
,並且容器的啟動指令碼能夠處理SIGTERM
訊號,那麼就不需要在preStop
中加kill -15
.
如果你的應用停止的時間超過預設的30s,可以在yaml中設定terminationGracePeriodSeconds
來看一張完整的圖
總結
推薦在Dockerfile
中使用exec
格式
可以通過容器的退出狀態碼檢視容器是否是graceful shutdown
容器中PID為了的程序需要能處理SIGTERM
訊號,如果不能處理, 就要借用Kubernetes
中的preStop
preStop
的執行時間不能超過terminationGracePeriodSeconds
的值,
為了防止pod在endpoint之前刪除,可以在preStop
中sleep
一段時間
參考
https://www.openshift.com/blog/kubernetes-pods-life
https://learnk8s.io/graceful-shutdown
https://cloud.google.com/blog/products/gcp/kubernetes-best-practices-terminating-with-grace
https://www.cnblogs.com/ryanyangcs/p/13036095.html