這幾天在生產環境發現有幾個容器一直不能正常的stop,或者rm 掉,並且查看docker daemon 日誌裏面會出現不少 msg="Container 5054f failed to exit within 10 seconds of<br/>signal 15 - using the force"
這樣的報錯,使用的命令爲journalctl -xe -u docker
而後在短暫的時間內 docker ps查看到的容器還在運行中,過了一會沒有了咱們在建立的時候會提示這個容器已經存在(若是創建一樣名稱的容器)html
1,docker 經過 containerd 向容器主進程發送
SIGTERM(終止進程)信號後等待一段時間後(默認是10s,能夠經過-t 參數來修改),若是從containerd 收到了容器退出消息,那麼容器退出成功。
2,若是超過等待的時間以後,仍是沒收到容器退出的消息,那麼docker 將使用docker kill方式試圖終止容器。git
可是對於容器來講,init 系統進程並非必須的,因此當咱們中止容器的時候,docker 經過 containerd 向容器Pid 爲 1 的進程發送 SIGTERM
信號並不必定會被採納。其實能夠分爲如下兩種狀況來講明:github
1,若是 PID==1 的進程是 init 進程:docker
那麼 PID==1 會將 SIGTERM 信號轉發給子進程,而後子進程開始關閉,最後容器終止shell
2,若是PID==1 的進程不是 init 進程:xcode
那麼容器中的應用進程(Dockerfile 中的 ENTRYPOINT 或 CMD 指令指定的應用)的 PId 就是 1,應用進程直接負責響應 SIGTERM 信號。這個時候又分爲兩種狀況ide
1,應用不處理 SIGTERM 信號:函數
應用沒有監聽 SIGTERM 信號,或者應用中沒有事先處理 SIGTERM 信號的邏輯,應用就不會中止,容器也不會正常終止,會被 調用 docker kill 方式殺死(咱們的程序目前就是這種).net
2,容器中止時間很長:命令行
運行命令 docker stop 以後,docker 會默認等待 10S(默認值,能夠修改 docker stop -t 指令),若是 10s後容器尚未終止,docker 就會繞過容器應用直接向內核發送 SIGKILL,內核強行殺死應用,從而終止容器。
1,docker 引擎經過containerd 使用 SIGKILL 發向容器主進程,等待一段時間後,若是從containerd收到容器退出消息,那麼容器kill成功
2,在上一步中若是等待超時,Docker引擎將跳過 containerd 本身親自動手經過kill系統調用向容器主進程發送 SIGKILL 信號。若是此時 kill 系統調用返回主進程不存在,那麼 Docker Kill 成功。不然引擎將一直死等到 containerd 經過引擎,容器退出.
上面咱們講到若是容器內的 PID 進程不能處理 SIGTERM 信號的時候,docker 會等 10S(默認時間),而後調用 kill 去殺死容器的進程,其實這樣會形成下面兩個問題
Linux 內核中其實會對 PID 1 進程發送特殊的信號量。通常狀況下,當給一個進程發送信號時,內核會先檢查是否有用戶定義的處理函數,若是沒有,就會回退到默認行爲。例如使用 SIGTERM 直接殺死進程。然而,若是進程的 PID 是 1,那麼內核就會特殊對待它。若是沒有註冊用戶處理函數,內核不會回退到默認行爲,什麼也不作,換句話說,若是你的進程沒有處理信號的函數,給他發送 SIGTERM 會一點效果也沒有,這個咱們在上面講過了。
常見的使用是 docker run my-container script. 給 docker run
進程發送SIGTERM
信號會殺掉 docker run
進程,可是容器還在後臺運行。
當進程退出時,它會變成殭屍進程,直到它的父進程調用 wait()
( 或其變種 ) 的系統調用。process table 裏面會把它的標記爲 defunct
狀態。通常狀況下,父進程應該當即調用 wait()
, 以防殭屍進程時間過長。
若是父進程在子進程以前退出,子進程會變成孤兒進程, 它的父進程會變成 PID 1。所以,init 進程就要對這些進程負責,並在適當的時候調用 wait()
方法。
可是,一般狀況下,大部分進程不會處理偶然依附在本身進程上的隨機子進程,因此在容器中,會出現許多殭屍進程。
經過上面的解釋應該能明白,咱們不能正常退出,或者等 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進程也會死掉,並會清理全部其餘的子進程。
開始說的那種狀況就是應用進程沒有正常退出而形成的問題,
參考文檔:
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)/