原文鏈接:Docker 容器優雅終止方案
作為一名系統重啟工程師(SRE),你可能經常需要重啟容器,畢竟 Kubernetes 的優勢就是快速彈性伸縮和故障恢復,遇到問題先重啟容器再說,幾秒鍾即可恢復,實在不行再重啟系統,這就是系統重啟工程師的殺手鐧。然而現實並沒有理論上那么美好,某些容器需要花費 10s
左右才能停止,這是為啥?有以下幾種可能性:
- 容器中的進程沒有收到 SIGTERM 信號。
- 容器中的進程收到了信號,但忽略了。
- 容器中應用的關閉時間確實就是這么長。
對於第 3 種可能性我們無能為力,本文主要解決 1 和 2。
如果要構建一個新的 Docker 鏡像,肯定希望鏡像越小越好,這樣它的下載和啟動速度都很快,一般我們都會選擇一個瘦了身的操作系統(例如 Alpine
,Busybox
等)作為基礎鏡像。
問題就在這里,這些基礎鏡像的 init 系統也被抹掉了,這就是問題的根源!
init
系統有以下幾個特點:
- 它是系統的第一個進程,負責產生其他所有用戶進程。
- init 以守護進程方式存在,是所有其他進程的祖先。
- 它主要負責:
- 啟動守護進程
- 回收孤兒進程
- 將操作系統信號轉發給子進程
1. Docker 容器停止過程
對於容器來說,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
,內核會強行殺死應用,從而終止容器。
- 應用不處理 SIGTERM - 如果應用沒有監聽
2. 容器進程收不到 SIGTERM 信號?
如果容器中的進程沒有收到 SIGTERM
信號,很有可能是因為應用進程不是 PID 1
,PID 1 是 shell
,而應用進程只是 shell
的子進程。而 shell 不具備 init
系統的功能,也就不會將操作系統的信號轉發到子進程上,這也是容器中的應用沒有收到 SIGTERM
信號的常見原因。
問題的根源就來自 Dockerfile
,例如:
FROM alpine:3.7
COPY popcorn.sh .
RUN chmod +x popcorn.sh
ENTRYPOINT ./popcorn.sh
ENTRYPOINT
指令使用的是 shell 模式,這樣 Docker 就會把應用放到 shell
中運行,因此 shell
是 PID 1。
解決方案有以下幾種:
方案 1:使用 exec 模式的 ENTRYPOINT 指令
與其使用 shell 模式,不如使用 exec 模式,例如:
FROM alpine:3.7
COPY popcorn.sh .
RUN chmod +x popcorn.sh
ENTRYPOINT ["./popcorn.sh"]
這樣 PID 1 就是 ./popcorn.sh
,它將負責響應所有發送到容器的信號,至於 ./popcorn.sh
是否真的能捕捉到系統信號,那是另一回事。
舉個例子,假設使用上面的 Dockerfile 來構建鏡像,popcorn.sh
腳本每過一秒打印一次日期:
#!/bin/sh
while true
do
date
sleep 1
done
構建鏡像並創建容器:
🐳 → docker build -t truek8s/popcorn .
🐳 → docker run -it --name corny --rm truek8s/popcorn
打開另外一個終端執行停止容器的命令,並計時:
🐳 → time docker stop corny
因為 popcorn.sh
並沒有實現捕獲和處理 SIGTERM
信號的邏輯,所以需要 10s 左右才能停止容器。要想解決這個問題,就要往腳本中添加信號處理代碼,讓它捕獲到 SIGTERM
信號時就終止進程:
#!/bin/sh
# catch the TERM signal and then exit
trap "exit" TERM
while true
do
date
sleep 1
done
注意:下面這條指令與 shell 模式的 ENTRYPOINT 指令是等效的:
ENTRYPOINT ["/bin/sh", "./popcorn.sh"]
方案 2:直接使用 exec 命令
如果你就想使用 shell
模式的 ENTRYPOINT 指令,也不是不可以,只需將啟動命令追加到 exec
后面即可,例如:
FROM alpine:3.7
COPY popcorn.sh .
RUN chmod +x popcorn.sh
ENTRYPOINT exec ./popcorn.sh
這樣 exec
就會將 shell 進程替換為 ./popcorn.sh
進程,PID 1 仍然是 ./popcorn.sh
。
方案 3:使用 init 系統
如果容器中的應用默認無法處理 SIGTERM
信號,又不能修改代碼,這時候方案 1 和 2 都行不通了,只能在容器中添加一個 init
系統。init 系統有很多種,這里推薦使用 tini,它是專用於容器的輕量級 init 系統,使用方法也很簡單:
- 安裝
tini
- 將
tini
設為容器的默認應用 - 將
popcorn.sh
作為tini
的參數
具體的 Dockerfile 如下:
FROM alpine:3.7
COPY popcorn.sh .
RUN chmod +x popcorn.sh
RUN apk add --no-cache tini
ENTRYPOINT ["/sbin/tini", "--", "./popcorn.sh"]
現在 tini
就是 PID 1,它會將收到的系統信號轉發給子進程 popcorn.sh
。
如果你想直接通過 docker 命令來運行容器,可以直接通過參數
--init
來使用 tini,不需要在鏡像中安裝 tini。如果是Kubernetes
就不行了,還得老老實實安裝 tini。
3. 使用 tini 后應用還需要處理 SIGTERM 嗎?
最后一個問題:如果移除 popcorn.sh
中對 SIGTERM 信號的處理邏輯,容器會在我們執行停止命令后立即終止嗎?
答案是肯定的。在 Linux 系統中,PID 1
和其他進程不太一樣,准確地說應該是 init
進程和其他進程不一樣,它不會執行與接收到的信號相關的默認動作,必須在代碼中明確實現捕獲處理 SIGTERM
信號的邏輯,方案 1 和 2 干的就是這個事。
普通進程就簡單多了,只要它收到系統信號,就會執行與該信號相關的默認動作,不需要在代碼中顯示實現邏輯,因此可以優雅終止。
Kubernetes 1.18.2 1.17.5 1.16.9 1.15.12離線安裝包發布地址http://store.lameleg.com ,歡迎體驗。 使用了最新的sealos v3.3.6版本。 作了主機名解析配置優化,lvscare 掛載/lib/module解決開機啟動ipvs加載問題, 修復lvscare社區netlink與3.10內核不兼容問題,sealos生成百年證書等特性。更多特性 https://github.com/fanux/sealos 。歡迎掃描下方的二維碼加入釘釘群 ,釘釘群已經集成sealos的機器人實時可以看到sealos的動態。