利用 trap 在 docker 容器優雅關閉前執行環境清理

DrunkCat90發表於2021-12-17

當一個執行中的容器被終止時,如何能夠執行一些預定義的操作,比如在容器徹底退出之前清理環境。這是一種類似於 pre stop 的鉤子體驗。但 docker 本身無法提供這種能力,本文結合 Linux 內建命令 trap ,實現在容器優雅關閉之前,可以執行自定義的操作。

如何關閉容器

我瞭解有三種方式可以關閉一個正在執行中的容器,三者都是由 docker 命令列發起的。

  • 第一種是較為優雅的方式 docker stop ContainerID
  • 第二種看起來就比較武斷 docker rm -f ContainerID
  • 第三種用的人會少很多 docker kill --signal=KILL ContainerID

docker 的設計者自然不會平白無故的設計三種命令組合來做關閉容器這件事,三種方式都應該在什麼場景下被使用呢?

這三種終止容器的方式之間是略有不同的,在講解這些不同之前,需要提及一些看似和容器不相關的知識點——SIGNAL 。

程式與訊號

使用者是可以通過傳送訊號,來和程式通訊的。

基本上每一個運維工程師都執行過如下命令來殺死一個程式:

kill -9 PID

這個命令看起來恰如其分,我 "殺死" 了一個程式,但是,為什麼是 "-9" ?

9 是訊號 SIGKILL 的代號,上述命令實際上是向對應的程式傳送了一個訊號,一個可以殺死程式的訊號。

kill 命令的真正意義,是向程式傳送指定的訊號,除了SIGKILL(9) 之外,還可以傳送其他多種訊號:

root@ubuntuserver:~# kill --help
kill: kill [-s sigspec | -n signum | -sigspec] pid | jobspec ... or kill -l [sigspec]
    Send a signal to a job.
root@ubuntuserver:~# kill -l
 1) SIGHUP	 2) SIGINT	 3) SIGQUIT	 4) SIGILL	 5) SIGTRAP
 6) SIGABRT	 7) SIGBUS	 8) SIGFPE	 9) SIGKILL	10) SIGUSR1
11) SIGSEGV	12) SIGUSR2	13) SIGPIPE	14) SIGALRM	15) SIGTERM
16) SIGSTKFLT	17) SIGCHLD	18) SIGCONT	19) SIGSTOP	20) SIGTSTP
21) SIGTTIN	22) SIGTTOU	23) SIGURG	24) SIGXCPU	25) SIGXFSZ
26) SIGVTALRM	27) SIGPROF	28) SIGWINCH	29) SIGIO	30) SIGPWR
31) SIGSYS	34) SIGRTMIN	35) SIGRTMIN+1	36) SIGRTMIN+2	37) SIGRTMIN+3
38) SIGRTMIN+4	39) SIGRTMIN+5	40) SIGRTMIN+6	41) SIGRTMIN+7	42) SIGRTMIN+8
43) SIGRTMIN+9	44) SIGRTMIN+10	45) SIGRTMIN+11	46) SIGRTMIN+12	47) SIGRTMIN+13
48) SIGRTMIN+14	49) SIGRTMIN+15	50) SIGRTMAX-14	51) SIGRTMAX-13	52) SIGRTMAX-12
53) SIGRTMAX-11	54) SIGRTMAX-10	55) SIGRTMAX-9	56) SIGRTMAX-8	57) SIGRTMAX-7
58) SIGRTMAX-6	59) SIGRTMAX-5	60) SIGRTMAX-4	61) SIGRTMAX-3	62) SIGRTMAX-2
63) SIGRTMAX-1	64) SIGRTMAX

我無意去詳解每一個訊號的意義,我的功力還差得遠,在這裡只揀取和我們主題相關的知識來進行闡述。

有兩個訊號和我們的主題相關, SIGTERM. SIGKILL

訊號名稱 代號 可否被捕獲或忽略
SIGTERM 15 可以
SIGKILL 9 不可以

SIGTERMkill 命令預設傳送的訊號。當使用者請求終止程式時,會產生SIGTERM訊號。SIGTERM訊號可以被捕獲或無視。這允許該程式在結束前釋放掉所佔用的資源並儲存其狀態。

SIGKILL 傳送SIGKILL訊號到一個程式可以使其立即終止(KILL)。與SIGTERM不同的是,這個訊號不能被捕獲或忽略,接收過程在接收到這個訊號時不能執行任何清理。但有時候 kill -9 並非一定可以殺死程式,釋放資源。還是有一些特殊情況:

  • 殭屍程式不能被殺死,因為它們已經死了,正在等待它們的父程式來收穫它們。
  • 處於阻塞狀態的程式不會死亡,直到它們再次醒來。
  • init 程式是特殊的: init不接收任何它不打算處理的訊號,因此它會忽略SIGKILL。這條規則有一個例外,Linux 上的 init 如果被 ptrace 了,那麼它是可以接收 SIGKILL 並被殺死的。
  • 處於不可中斷的睡眠的程式即使傳送了SIGKILL,也有可能不會終止(並釋放其資源)。這是少數 Unix 系統必須重新啟動才能解決臨時軟體問題的幾種情況之一。

容器與訊號

容器的本質,是一組被封裝起來的程式。所以通過開頭講到的三種命令列方式關閉一個執行中的容器,其本質也是在通過傳送訊號的方式與容器中的程式進行互動,使之被 "殺死" 的過程。

  • docker stop

執行 docker stop ContainerID ,會向容器中的主程式先傳送一個 SIGTERM 訊號,在一段時間的寬限期後,傳送 SIGKILL 訊號徹底殺死容器。

Docker 手冊原文如下:

The main process inside the container will receive SIGTERM, and after a grace period, SIGKILL

  • docker rm -f

執行 docker rm -f ContainerID ,會向容器中的主程式直接傳送SIGKILL 訊號,在容器殺死之後,也會把容器刪除掉。從刪除容器這個操作看來,這個命令是用來刪除一個已停止的容器,而非用於停止執行中的容器。

  • docker kill

執行 docker kill --signal=KILL ContainerID ,是專門向容器主程式傳送各種自定義訊號的方式。換言之,它就是面向容器的 kill 命令。當前命令是在向容器主程式傳送一個 SIGKILL 訊號。

通過比對,docker rm -f ContainerID 這種方式是不應該用於停止執行中容器的。而剩餘兩種方式之間, docker stop ContainerID 也明顯要優雅一些,它既可以保證容器會被最終殺死,也會提供 SIGTERM 供使用者後續捕獲處理。

接下來終於要進入正題了。

捕獲訊號並處理

訊號 SIGTERM 是一種可以被捕獲的訊號。當容器主程式捕獲到這個訊號之後,可以觸發事先設計好的邏輯,在徹底退出之前完成預定的任務。比如可以執行環境的清理、資料的儲存、關閉其他不受主程式控制的程式等等。在某些場景下,這種需求非常突出。

Linux 提供內建的 trap 命令,負責捕獲訊號,並確保在程式徹底退出前,執行某些任務。

root@ubuntuserver:~# trap --help
trap: trap [-lp] [[arg] signal_spec ...]
    Trap signals and other events.

其基本的使用方式如下:

trap do_some_things SIGSPEC

思路已經清晰了,我們需要在容器的啟動指令碼中,加入 trap 指令,來完成容器在退出前需要做的所有事情。

以下是一個指令碼示例,這個指令碼被作為容器的入口(ENTRYPOINT)執行。

#!/bin/bash

function clean_up_term {
  rm -rf /data/tmp
  echo "clean_up_term in execution"
}

trap clean_up_term SIGTERM

for ((i=1;i<=1000;i++))
  do
    echo "Wait for $i"
    sleep 1
  done 

容器啟動後,從其他終端執行了 docker stop ContainerID 命令,可以觀察到以下結果。

guox@MacBook-Pro-For-Guox: /Users/guox/GitHub/test-plugin git:(master) ✗ 
➜   docker run -ti --name=clean -v $(pwd)/data:/data clean 
Wait for 1
Wait for 2
Wait for 3
Wait for 4
Wait for 5
Wait for 6
Wait for 7
Wait for 8
Wait for 9
Wait for 10
Wait for 11
Wait for 12
Wait for 13
clean_up_term in execution
Wait for 14
Wait for 15
Wait for 16
Wait for 17
Wait for 18
Wait for 19
Wait for 20
Wait for 21
Wait for 22
Wait for 23
guox@MacBook-Pro-For-Guox: /Users/guox/GitHub/test-plugin git:(master) ✗ 

訊號 SIGTERM 的確被容器捕獲,並進行了相關的清理操作。 docker stop ContainerID 提供了一段寬限期,所以在執行了清理操作後,容器主程式還是繼續執行了一會才退出。

相關文章