天天看點

Docker 優雅終止方案

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

  1.容器中的程序沒有收到SIGTERM信号。

  2.容器中的程序收到了信号,但忽略了。

  3.容器中應用的關閉時間确實就是這麼長。

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

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

Docker 優雅終止方案

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

  init系統有以下幾個特點:

  1.它是系統的第一個程序,負責産生其他所有使用者程序。

  2.init 以守護程序方式存在,是所有其他程序的祖先。

  它主要負責:

  1.啟動守護程序

  2.回收孤兒程序

  3.将作業系統信号轉發給子程序

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,核心會強行殺死應用,進而終止容器。

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      

  這樣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幹的就是這個事。

  普通程序就簡單多了,隻要它收到系統信号,就會執行與該信号相關的預設動作,不需要在代碼中顯示實作邏輯,是以可以優雅終止。

作者:小家電維修

轉世燕還故榻,為你銜來二月的花。