Docker 容器優雅終止方案

米開朗基楊發表於2020-06-03

原文連結:Docker 容器優雅終止方案

作為一名系統重啟工程師(SRE),你可能經常需要重啟容器,畢竟 Kubernetes 的優勢就是快速彈性伸縮和故障恢復,遇到問題先重啟容器再說,幾秒鐘即可恢復,實在不行再重啟系統,這就是系統重啟工程師的殺手鐗。然而現實並沒有理論上那麼美好,某些容器需要花費 10s 左右才能停止,這是為啥?有以下幾種可能性:

  1. 容器中的程式沒有收到 SIGTERM 訊號。
  2. 容器中的程式收到了訊號,但忽略了。
  3. 容器中應用的關閉時間確實就是這麼長。

對於第 3 種可能性我們無能為力,本文主要解決 1 和 2。

如果要構建一個新的 Docker 映象,肯定希望映象越小越好,這樣它的下載和啟動速度都很快,一般我們都會選擇一個瘦了身的作業系統(例如 AlpineBusybox 等)作為基礎映象。

問題就在這裡,這些基礎映象的 init 系統也被抹掉了,這就是問題的根源!

init 系統有以下幾個特點:

  • 它是系統的第一個程式,負責產生其他所有使用者程式。
  • init 以守護程式方式存在,是所有其他程式的祖先。
  • 它主要負責:
    • 啟動守護程式
    • 回收孤兒程式
    • 將作業系統訊號轉發給子程式

1. Docker 容器停止過程

對於容器來說,init 系統不是必須的,當你通過命令 docker stop mycontainer 來停止容器時,docker CLI 會將 TERM 訊號傳送給 mycontainer 的 PID 為 1 的程式。

  • 如果 PID 1 是 init 程式 - 那麼 PID 1 會將 TERM 訊號轉發給子程式,然後子程式開始關閉,最後容器終止。
  • 如果沒有 init 程式 - 那麼容器中的應用程式(Dockerfile 中的 ENTRYPOINTCMD 指定的應用)就是 PID 1,應用程式直接負責響應 TERM 訊號。這時又分為兩種情況:
    • 應用不處理 SIGTERM - 如果應用沒有監聽 SIGTERM 訊號,或者應用中沒有實現處理 SIGTERM 訊號的邏輯,應用就不會停止,容器也不會終止。
    • 容器停止時間很長 - 執行命令 docker stop mycontainer 之後,Docker 會等待 10s,如果 10s 後容器還沒有終止,Docker 就會繞過容器應用直接向核心傳送 SIGKILL,核心會強行殺死應用,從而終止容器。

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 系統,使用方法也很簡單:

  1. 安裝 tini
  2. tini 設為容器的預設應用
  3. 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的動態。

相關文章