iT邦幫忙

2022 iThome 鐵人賽

DAY 21
0
DevOps

那些關於 docker 你知道與不知道的事系列 第 21

Day 21: container 裡誰是 PID 1 有差嗎?

  • 分享至 

  • xImage
  •  

昨天我們用 Dockerfile 做了兩個在 Ubuntu sleep 的 image,他們分別用了不同的執行命令的方式:

  1. exec mode
CMD ["sleep", "1000"]

用這種方式就是直接執行 CMD 後面的命令,因此我們會到看 sleep 1000 會是 container 中 PID 1。

  1. shell mode
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]),如果去翻一下 killman 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 是誰又有麼關係。

docker stop

當我們想要停止一個 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 秒。

我們來做幾個實驗吧!

  • 實驗1: 沒有處理 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 才會成功被關閉
  • 實驗2: 捕捉 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 很快地就關閉了。

  • 實驗 3: 捕捉 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

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 -f

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 還是蠻重要的對吧!


上一篇
Day 20: 不負責任的 PID 1
下一篇
Day 22: Ubuntu 的 /bin/sh 怎麼了?
系列文
那些關於 docker 你知道與不知道的事32
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言