昨天我們用 Dockerfile 做了兩個在 Ubuntu sleep
的 image,他們分別用了不同的執行命令的方式:
CMD ["sleep", "1000"]
用這種方式就是直接執行 CMD
後面的命令,因此我們會到看 sleep 1000
會是 container 中 PID 1。
CMD sleep 1000
用這種方式則是會透過 /bin/sh -c
去執行,當我們透過 docker ps
查看時,會看到 "/bin/sh -c 'sleep 1…"
,container 中的 PID 1 會是 /bin/sh -c ...
,不會是 sleep
。
根據昨天實驗的,當 container 內的其他 process 變成孤兒時,雖然不管是 sleep
或是 /bin/sh
是 PID 1,他們都會擔任起 init process 的責任,接收這些孤兒,但 sleep
並不會好好地回收 zombie
。
除了昨天實驗的這個差異外,另外一個更常見的討論則是在要關閉 container 時,但在討論怎麼關閉 container 前,我們先來簡單了解一下 Linux 中的 signals。
根據 wiki,signals (訊號)是 Linux 中用來做 processes 之間的溝通的,是一種非同步的通知機制,通知 process 有一個事件已經發生,通常是 interrup、terminate、kill 或要 suspend 一個 process(這邊有點難翻譯,保留英文),每個訊號會有預設的處理方式,當 process 收到訊號時,可能會執行預設的處理方式、可能會被忽略,也有可能會被捕捉(catch
)起來、由程式中定義的函式來處理。
聽起來好像很玄乎,但如果有在使用 Linux 的話應該都有用過,例如我們經常用 ctrl+c 來中斷一個程式的執行,其實 ctrl+c 就是對這個執行中的 process 送一個 SIGINT
的訊號。我們也會用 kill [pid]
來終止一個程式的執行,有時候光是 kill
來不夠,我們還會加上一個 -9
的參數(kill -9 [pid]
),如果去翻一下 kill
的 man page,他的描述並不是說「我是一個負責殺死 process 的指令」,相反地,他的描述是:
kill - send a signal to a process
也就是說,kill
指令的自我定義是對 procss 送出一個信號,預設是送出 SIGTERM
,如果加上 -9
的話,則是送出 SIGKILL
。那這些訊號就是什麼呢?來翻一下 man page:
Signal Standard Action Comment
--------------------------------------------------
SIGINT P1990 Term Interrupt from keyboard
SIGKILL P1990 Term Kill signal
SIGTERM P1990 Term Termination signal
SIGSTOP P1990 Stop Stop process
...略
我們這邊就放幾個可能相關的,其中有一個欄位 Action
,就是這個訊號預設的行為,其中的 Term
就是去終止(terminate
) 這個 process。除此之外,可以再看一下文件中 Signal numbering for standard signals
這邊列出的表格,可以看到每個訊號對應的數字,例如:
Signal numberic value
--------------------------------------------------
SIGINT 2
SIGKILL 9
SIGTERM 15
SIGSTOP 19
...略
透過這個表格,就可以知道為什麼 kill -9
是送出 SIGKILL
了。
現在讓我們來寫個簡單的 script 測試看看:
#!/bin/bash
# no-trap.sh
i=1
while true;
do
echo "running for $i times"
((i++))
sleep 10
done
以上只是一個簡單的無窮迴圈,當我們執行這個 script 時,可以用 ctrl+c 停止它,也可以用 kill [pid]
或是 kill -9 [pid]
來終止這個 script 的執行。
接著讓我們自己捕捉訊號試試看:
#!/bin/bash
# trap.sh
trap "echo Hello SIGTERM" SIGTERM
trap "echo Hello SIGINT" SIGINT
trap "echo Hello SIGKILL" SIGKILL
i=1
while true;
do
echo "running for $i times"
((i++))
sleep 10
done
trap
這個指令就是用來捕捉信號的,根據文件 其用法為 trap [action condition...]
,action 指的是動作,condition 就是指要被捕捉的訊號,大家可以試著動手執行看看上述的 script,如果嘗試用 crtl+c 去終止他的話,terminal 應該會印出 Hello SIGINT
但程式卻沒有被終止,用 kill [pid]
也類似,會印出 Hello SIGTERM
,但也不會停止執行。但如果是用 kill -9 [pid]
的話,會印出 Killed
,而不是我們 script 中寫的 Hello SIGKILL
字串,這又是為什麼呢?去翻一下文件,文件中有寫到這句話:
The signals SIGKILL and SIGSTOP cannot be caught, blocked, or ignored.
也就是 SIGKILL 與 SIGSTOP 這兩種訊號,是不能被捕捉、也不能被忽略的。
以上就是對 Linux 中的訊號做的一點點小補充,現在來讓我們看看要怎麼樣才可以關閉 container,跟 PID 1 是誰又有麼關係。
當我們想要停止一個 container 時,第一個會到的指令就是 docker stop [container id]
,這個指令會對 container 中 PID 1 的 process 送出一個 SIGTERM
的訊號。如果這個 process 有處理這個訊號,並且停止執行,那隨著這個 PID 1 process 的終止,也就是這個 PID namespace 的 init process 終止,kernel 會透過 SIGKILL
去終止掉這個 namespace 中的其他 processes。這裡也反映出了,PID 1 這個 process 的特殊性,他是這個 namespace 正確運作不可或缺的一部分(參考文件)。
那如果這個 PID 1 的 process 忽略 SIGTERM
、這個 process 沒有停止,那過了 10 秒後,docker 會再送一個 SIGKILL
訊號給這個 PID 1 的 process,由於 SIGKILL
不能被忽略,PID 1 的 process 會被終止,這個 container 也就隨之關閉。
ps. 等待 10 秒這個是可以修改的,例如 docker stop --time=5 [container id]
,這樣就只會等待 5 秒。
我們來做幾個實驗吧!
SIGTERM
訊號,並且以 exec mode 方式執行把前面段落提到的 no-trap.sh
給放進 Dockerfile 裡,並且以 exec 模式執行,看看會發生什麼事:
Dockerfile-notrap-exec
From ubuntu:20.04
WORKDIR /app
COPY no-trap.sh .
CMD ["/app/no-trap.sh"]
ubuntu@ip-xxx:~/signal$ docker build -t notrap:exec -f Dockerfile-notrap-exec .
ubuntu@ip-xxx:~/signal$ docker run -d notrap:exec
dae01c5eb9bf3f7dde53e5579794e0c4cac2be4c0b7aad21d30c08809b4da87f
ubuntu@ip-xxx:~/signal$ docker stop dae01c5eb9
# 這邊可以觀察到,會等待十秒鐘,container 才會成功被關閉
SIGTERM
訊號自行處理,並且以 exec mode 方式執行調整一下上面提供的 trap.sh,家上一個 cleanup 函式,模擬一下關閉前想處理一些事情,處理完畢後才透過 exit
離開。
#!/bin/bash
# trap.sh
cleanup() {
echo "do something for graceful stop..."
exit
}
trap cleanup SIGTERM
i=1
while true;
do
echo "running for $i times"
((i++))
sleep 1
done
Dockerfile-trap-exec
From ubuntu:20.04
WORKDIR /app
COPY trap.sh .
CMD ["/app/trap.sh"]
一樣 build 成 trap:exec
image 後執行:
ubuntu@ip-xxx:~/signal$ docker run -d trap:exec
495e9c3367357394dce10cf6f6f079709fd08e65e31242c81eb31dcef1439fa0
ubuntu@ip-xxx:~/signal$ docker stop 495e9c33673
495e9c33673
ubuntu@ip-xxx:~/signal$ docker logs 495e9c33673
running for 1 times
running for 2 times
running for 3 times
running for 4 times
do something for graceful stop...
可以觀察到,我們可以捕捉到 SIGTERM
訊號,並且先處理一些事情,例如通知別人我們要終止了、把一些該寫的檔案寫完等等(優雅地離開 graceful stop),當我們處理完事情後,就會立刻離開,以我們這邊為例,因為只有印出一行字,所以 container 很快地就關閉了。
SIGTERM
訊號自行處理,並且以 shell mode 方式執行這邊 Dockerfile 跟實驗 2 的差別就只是最後的執行改成用 shell mode 的方式執行 CMD /app/trap.sh
,我們把這個 image 叫做 trap:shell
。當這個 image 啟動成 container 後,一樣用 docker stop
去關閉,同樣有捕捉 SIGTERM
訊號並且 exit
離開的 script,這次卻得等上 10 秒鐘...這是為什麼呢?
ubuntu@ip-xxx:~/signal$ docker run -d trap:shell
9d22ec6ff3d9e00ae4ace394ad67da7d4f5f722180904d8b05c29c0fa9bae268
ubuntu@ip-xxx:~/signal$ docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
9d22ec6ff3d9 trap:shell "/bin/sh -c /app/tra…" ...略
ubuntu@ip-xxx:~/signal$ docker top 9d22ec6ff3d9 -o pid,ppid,args
PID PPID COMMAND
105087 105063 /bin/sh -c /app/trap.sh
105115 105087 /bin/bash /app/trap.sh
105161 105115 sleep 1
ubuntu@ip-xxx:~/signal$ docker stop 9d22ec6ff3d9
透過上述觀察,我們可以知道透過 shell mode 啟動 container,在 base image 是 Ubuntu 的情況下,他 PID 1 的 process 並不是我們的這個 script,而是 /bin/sh -c
,這也導致了收到訊號的其實是 /bin/sh
,而不是我們的 trap.sh,看來 /bin/sh
並沒有好好地處理 SIGTERM
,所以要等 10 秒過後,docker 送出 SIGKILL
才能被關閉。
docker kill
看名稱可能可以猜到,他會直接送一個 SIGKILL
給 container 中的 PID 1 process,蠻像是 kill -9
的,而送過去的訊號是可以透過 --signal
調整的。
一樣來做個實驗,就拿上面實驗 3 的 image 來試:
ubuntu@ip-xxx:~/signal$ docker run -d trap:shell
36221b14788920d7fdcda694813cec7863043008fe0aa5b49396556478858dc5
ubuntu@ip-xxx:~/signal$ docker kill 36221b147889
36221b147889
這邊很難用文字表達,可以動手做做看,應該可以觀察到,這個 container 很快地就關閉了,不需要等上 10 秒。
docker rm
原本是用來移除一個已經停止的 container 的,如果是對一個正在執行中的 container 下這個命令,那會拿到錯誤訊息 You cannot remove a running container
,加上 -f
就是要強制離開(force)的意思,會對這個 container 送一個 SIGKILL
訊號,container 被停止後就可以移除了。
ubuntu@ip-xxx:~/signal$ docker run -d trap:shell
23befd6ec23c18ff0f083d50ba83971c724b665659b75e75d4dc277ce8ab7c72
ubuntu@ip-xxx:~/signal$ docker rm 23befd6ec
Error response from daemon: You cannot remove a running container 23befd6ec23c18ff0f083d50ba83971c724b665659b75e75d4dc277ce8ab7c72. Stop the container before attempting removal or force remove
ubuntu@ip-xxx:~/signal$ docker rm -f 23befd6ec
23befd6ec
回到我們今天的題目,container 裡誰是 PID 1 很重要嗎?至少到目前為止,我們可以整理出以下結論:
作為 init process (PID 1),需要成為孤兒的爸爸,並且要有回收殭屍的能力。此外,也要能捕捉與處理 SIGTERM
訊號來做 graceful stop,更進一步,如果 container 中還有其他 child processes,這個 init porcess 也要有能力將 SIGTERM
訊號送給其他 child processes,等這些 child processes 都結束後再退出。
看來誰是 PID 1 還是蠻重要的對吧!