docker stop 或者 docker kill 不能停止容器
原因
這幾天在生產環境發現有幾個容器一直不能正常的stop,或者rm 掉,而且查看docker daemon 日志里面會出現很多 msg="Container 5054f failed to exit within 10 seconds of signal 15 - using the force"
這樣的報錯,使用的命令為journalctl -xe -u docker
然后在短暫的時間內 docker ps查看到的容器還在運行中,過了一會沒有了我們在創建的時候會提示這個容器已經存在(如果建立同樣名稱的容器)
docker stop 主流程
1,docker 通過 containerd 向容器主進程發送
SIGTERM(終止進程)信號后等待一段時間后(默認是10s,可以通過-t 參數來修改),如果從containerd 收到了容器退出消息,那么容器退出成功。
2,如果超過等待的時間之后,還是沒收到容器退出的消息,那么docker 將使用docker kill方式試圖終止容器。
但是對於容器來說,init 系統進程並不是必須的,所以當我們停止容器的時候,docker 通過 containerd 向容器Pid 為 1 的進程發送 SIGTERM
信號並不一定會被采納。其實可以分為以下兩種情況來說明:
1,如果 PID==1 的進程是 init 進程:
那么 PID==1 會將 SIGTERM 信號轉發給子進程,然后子進程開始關閉,最后容器終止
2,如果PID==1 的進程不是 init 進程:
那么容器中的應用進程(Dockerfile 中的 ENTRYPOINT 或 CMD 指令指定的應用)的 PId 就是 1,應用進程直接負責響應 SIGTERM 信號。這個時候又分為兩種情況
1,應用不處理 SIGTERM 信號:
應用沒有監聽 SIGTERM 信號,或者應用中沒有事先處理 SIGTERM 信號的邏輯,應用就不會停止,容器也不會正常終止,會被 調用 docker kill 方式殺死(我們的程序目前就是這種)
2,容器停止時間很長:
運行命令 docker stop 之后,docker 會默認等待 10S(默認值,可以修改 docker stop -t 指令),如果 10s后容器還沒有終止,docker 就會繞過容器應用直接向內核發送 SIGKILL,內核強行殺死應用,從而終止容器。
docker kill主流程
1,docker 引擎通過containerd 使用 SIGKILL 發向容器主進程,等待一段時間后,如果從containerd收到容器退出消息,那么容器kill成功
2,在上一步中如果等待超時,Docker引擎將跳過 containerd 自己親自動手通過kill系統調用向容器主進程發送 SIGKILL 信號。如果此時 kill 系統調用返回主進程不存在,那么 Docker Kill 成功。否則引擎將一直死等到 containerd 通過引擎,容器退出.
docker 中 PID 進程不能處理 SIGTERM 信號的危害
上面我們講到如果容器內的 PID 進程不能處理 SIGTERM 信號的時候,docker 會等 10S(默認時間),然后調用 kill 去殺死容器的進程,其實這樣會造成下面兩個問題
1,進程不能正常終止
Linux 內核中其實會對 PID 1 進程發送特殊的信號量。一般情況下,當給一個進程發送信號時,內核會先檢查是否有用戶定義的處理函數,如果沒有,就會回退到默認行為。例如使用 SIGTERM 直接殺死進程。然而,如果進程的 PID 是 1,那么內核就會特殊對待它。如果沒有注冊用戶處理函數,內核不會回退到默認行為,什么也不做,換句話說,如果你的進程沒有處理信號的函數,給他發送 SIGTERM 會一點效果也沒有,這個我們在上面講過了。
常見的使用是 docker run my-container script. 給 docker run
進程發送SIGTERM
信號會殺掉 docker run
進程,但是容器還在后台運行。
2,孤兒僵屍進程不能正常回收
當進程退出時,它會變成僵屍進程,直到它的父進程調用 wait()
( 或其變種 ) 的系統調用。process table 里面會把它的標記為 defunct
狀態。一般情況下,父進程應該立即調用 wait()
, 以防僵屍進程時間過長。
如果父進程在子進程之前退出,子進程會變成孤兒進程, 它的父進程會變成 PID 1。因此,init 進程就要對這些進程負責,並在適當的時候調用 wait()
方法。
但是,通常情況下,大部分進程不會處理偶然依附在自己進程上的隨機子進程,所以在容器中,會出現許多僵屍進程。
解決容器進程收不到 SIGTERM 信號
通過上面的解釋應該能明白,我們不能正常退出,或者等 10s 才能退出的主要原因就是 PID 1 的進程不能處理/不處理 SIGTERM 信號造成的,知道問題所在了,那么久好辦了,有如下幾種解決方案:
1,讓你們公司的程序代碼支持處理 SIGTERM 信號。
當我們 pid 1 的進程(自己公司的代碼)能處理 SIGTERM 信號,那么這個問題不就解決了嗎?比較推薦這種方式,但是涉及到開發有一定的開發量,還是我們自己先用下面的方式解決。
2,構建 docker 包的時候使用 exec 模式的 ENTRYPOINT 指令
docker 官方文檔指出:
You can specify a plain string for the
ENTRYPOINT
and it will execute in/bin/sh -c
. This form will use shell processing to substitute shell environment variables, and will ignore anyCMD
ordocker run
command line arguments. To ensure thatdocker stop
will signal any long runningENTRYPOINT
executable correctly, you need to remember to start it withexec
:你可以為ENTRYPOINT指定一個普通字符串,它將在/bin/sh -c中執行。這個形式將使用shell處理來替代shell環境變量,並且會忽略任何CMD或docker運行命令行參數。為了確保docker stop會正確地提示任何長期運行的ENTRYPOINT可執行文件,你需要記得用exec啟動它。
使用方式很簡單,我們只需要按照如下格式編寫 Dockerfile 即可
ENTRYPOINT exec COMMAND param1 param2
以這種方式啟動,exec 就會將 shell 進程替換為 COMMAND 進程,
但是這種方式還是需要程序支持 SIGTERM,所以不推薦
3,在容器中使用 init 進程
當上面兩種情況我都不推薦的時候,那我們就只能用這種方式了。
在容器里面添加一個 init 系統,讓他去處理 SIGTERM 信號。
init 系統有很多,這推薦下面兩種
1,tini
FROM alpine:3.7
...
RUN apk add --no-cache tini
ENTRYPOINT ["/sbin/tini", "--", "COMMAND"]
現在 tini
就是 PID 1,它會將收到的系統信號轉發給子進程 COMMAND。
使用 tini 后應用還需要處理 SIGTERM 嗎?
答案是肯定不需要啊,如果需要那我們還大費周章的來講上面這么多廢話嗎?
當一個進程為普通進程,只要他收到系統信號,就會執行與該信號相關的默認動作,不需要再代碼中顯示實現邏輯,因此容器可以優雅的終止,而不需要強制 kill
他也是一個小型的 init 服務,他啟動一個子進程並轉發所有接收到的信號量給子進程。而且不需要修改應用代碼。
FROM alpine:3.7
...
RUN wget -O /usr/local/bin/dumb-init https://github.com/Yelp/dumb-init/releases/download/v1.2.0/dumb-init_1.2.0_amd64 &&\
chmod +x /usr/local/bin/dumb-init
# Runs "/usr/bin/dumb-init -- /my/script --with --args"
ENTRYPOINT ["/usr/bin/dumb-init", "--"]
CMD ["/my/script", "--with", "--args"]
需要注意的一點是:
雖然現在 PID 1 進程不是應用進程了,應用的行為和在沒有 init 進程時是一樣的。如果應用進程死掉,那么 init進程也會死掉,並會清理所有其他的子進程。
總結
開始說的那種情況就是應用進程沒有正常退出而造成的問題,
ENTRYPOINT的兩種模式
參考文檔:
docker init https://xcodest.me/docker-init-process.html
https://www.jianshu.com/p/813d8362d497
https://www.coder.work/article/41140
https://blog.csdn.net/shanzhizi/article/details/47320595
http://shareinto.github.io/2019/01/30/docker-init(1)/