了解 Docker 如何啟動 process

了解 CMD 與 ENTRYPOINT 曾提到 container 即 process,那接下來就要了解 Docker 是如何啟動 process 的。

exec 模式與 shell 模式

build Laravel image 有提到 CMD 在執行指令的模式有分 exec 模式與 shell 模式,先回顧這兩個模式在寫法上的差異如下:

# exec 模式
CMD ["php", "artisan", "serve"]
# shell 模式
CMD php artisan serve

因為是執行指令,所以 RUN 與 ENTRYPOINT 也有一樣的模式。

雖然最終都會跑 php artisan serve 指令,但跑的方法不大一樣。以下使用 ubuntu image 來做說明

exec 模式

簡單來說,就是直接執行指令,因此會是 PID 1 process。使用 PID 1 process 的好處是,使用 docker stop 指令發出 SIGTERM 後,會是由 PID 1 process 收到,做 graceful shutdown 相容比較簡單。

FROM ubuntu
CMD ["ps", "-o", "ppid,pid,user,args"]

因 CMD 設定的指令是要執行 ps -o ppid,pid,user,args,所以 ps 出來的結果,ps 指令為 PID 1。

這是官方建議的方法。

shell 模式

Shell 模式是透過 /bin/sh -c 執行指令,因此會先有 /bin/sh -c 的 PID 1 process,然後在底下開子 process。在這個情況下,docker stop 指令發出的 SIGTERM 信號將會由 shell 收到。

FROM ubuntu
CMD ps -o ppid,pid,user,args

這次 ps 指令為 PID 7。

當然,使用 shell 也是有方便的地方,它可以直接在指令上使用環境變數,比方說:

FROM node_project
CMD npm run $NODE_ENV

這在 exec 是辦不到的。有些討論會提到 exec 如果要取環境變數,可以改成下面的寫法:

FROM ubuntu
CMD ["/bin/sh", "-c", "cd $HOME && ps -o ppid,pid,user,args"]

相信現在讀者應該知道了,其實改成 shell 的寫法就行了:

FROM ubuntu
CMD cd $HOME && ps -o ppid,pid,user,args

反過來說,exec mode 才能自行決定要用什麼 shell 來執行指令。

image 差異

上面使用 ubuntu image 做範例,說明了 exec 模式和 shell 模式的差異。但不同 image 在 shell 模式下又會不大一樣,原因是 /bin/sh 在大多數 image 都是用 link 的形式連結到其他 shell:

$ docker run --rm -it ubuntu ls -l /bin/sh
lrwxrwxrwx 1 root root 4 Jul 18 2019 /bin/sh -> dash

$ docker run --rm -it debian ls -l /bin/sh
lrwxrwxrwx 1 root root 4 Sep 8 07:00 /bin/sh -> dash

$ docker run --rm -it centos ls -l /bin/sh
lrwxrwxrwx 1 root root 4 Nov 8 2019 /bin/sh -> bash

$ docker run --rm -it alpine ls -l /bin/sh
lrwxrwxrwx 1 root root 12 May 29 14:20 /bin/sh -> /bin/busybox

從上面範例可以看到,四個 image 就有三種不同的 shell:bashdashash(BusyBox)

這些 shell 執行 /bin/sh -c 在 process 表現的行為又不大一樣,可以參考以下範例:

Debian 因沒有內建 ps,且與 Ubuntu 一樣是使用 dash,因此就不做為範例

docker run --rm -it ubuntu /bin/sh -c ps
docker run --rm -it ubuntu ps

docker run --rm -it centos /bin/sh -c ps
docker run --rm -it centos ps

docker run --rm -it alpine /bin/sh -c ps
docker run --rm -it alpine ps

這裡可以發現,只有 ubuntu 才會多卡一層 process,其他不會。就這個範例來看,其實只有 dash 才會有 PID 1 process 被 shell 佔走的問題。建議若要使用 shell 模式的話,還是拿 base image 實驗一下比較保險。

docker exec

除了 docker run 以外,還有 docker exec 也是透過 Docker 啟動 process 的,我們來看看它啟動會發生什麼事:

# Terminal 1
docker run --rm -it --name test alpine

# 使用 top 指令查看目前的 process
top

# Terminal 2
docker exec -it test sh

# 安裝 Vim
apk add --no-cache vim

# Terminal 1 離開的時候,Terminal 2 的 process 也會跟著結束
exit

首先 Terminal 1 啟動 container 進入 shell,然後啟動 top,可以看到目前 PID 1 為 /bin/sh--也就是啟動 container 那時的 shell。而 top 的 PPID 是 PID 1。

接著切換 Terminal 2 使用 docker exec 進入 container 並下安裝指令,這時 top 裡面看到多出兩個 process,一個是 docker execsh 指令 PID 7,它的 PPID 跟啟動 container 的 /bin/sh 一樣為 0,推測這應該代表都是從 Docker 直接啟動的 process。另一個 process 則是安裝指令的 process。

最後 Terminal 1 執行 exit 把 PID 1 結束後,沒有父子關係的 PID 7 還是一樣被結束掉了。推測 Docker 主要是因為要把 container 移除,所以會把裡面全部的 process 都淨空,只是使用什麼信號就不確定了,照範例的執行速度來看,很有可能是 SIGKILL

因對發信號實際運作不熟悉,因此這裡僅能做推測。

今日自我回顧

因為 container 即 process,所以了解 process 的生命週期非常重要,包括如何啟動,以及什麼時候要回收資源等。