把應用直接當 PID 1 的代價#
Container 的標準寫法常常是 CMD ["python", "app.py"] 或 CMD ["node", "server.js"],這會讓 python、node 直接成為 container 內的 PID 1。但這些 runtime 並不是為當 init 設計的,把它們塞到 PID 1 位置會出現一連串問題。
回顧上一章 init 的責任:
- Reap zombie
- 處理 SIGTERM、SIGINT、SIGHUP
- 轉發 signal 給子行程
普通應用幾乎都不做這些事,於是出現「不負責任的 PID 1」。
問題一:Zombie 累積#
應用 PID 1 通常沒寫 SIGCHLD handler。一旦它 fork 出 helper(例如 shell out 跑 convert、跑 git、跑某個 script),helper 結束後就變 zombie。
- 短期看不出問題,因為 zombie 不耗 CPU、記憶體
- 長期下來 PID 表被塞滿,新 fork 失敗
- 任何依賴 subprocess 的功能(例如 child_process.spawn)開始失靈
這個問題在 worker container(會跑 subprocess)上特別常見,而且潛伏期長,初次部署測不出來。
問題二:SIGTERM 被忽略#
Container runtime 走 graceful shutdown 時,預設先發 SIGTERM 給 PID 1,等一段時間(docker 預設 10 秒)才升級成 SIGKILL。
- 如果 PID 1 是個沒裝 SIGTERM handler 的程式,kernel 對 PID 1 有特殊保護(見後章),SIGTERM 會被丟掉
- 結果是 docker stop 看起來「卡住」,整整等到逾時被 SIGKILL
- 應用沒機會 flush log、close DB、釋放鎖
問題三:Signal 不轉發#
即使 PID 1 自己有處理 SIGTERM,子行程通常不會自動收到。Container runtime 只把 signal 送給 PID 1,PID 1 必須自己廣播下去。
- 一般應用不會做這件事
- 結果:PID 1 退出後,子嗣繼續跑,container 顯得遲遲不結束
- 或者反過來:PID 1 退出,子嗣被孤兒化,最終被 SIGKILL 集體清掉,沒有 graceful
問題四:exec mode vs shell mode#
Dockerfile 的 ENTRYPOINT 與 CMD 有兩種寫法,行為差很多。
Shell mode(不建議)#
CMD python app.py這會被 docker 翻成:
CMD ["/bin/sh", "-c", "python app.py"]於是 container PID 1 是 /bin/sh,python 是它的 child(PID 2)。
- SIGTERM 送給
/bin/sh,多數 shell 不會轉發給 child - python 收不到 SIGTERM,也就不能 graceful shutdown
- docker stop 必然走到 SIGKILL
Exec mode(建議)#
CMD ["python", "app.py"]這會直接 exec 成 python,container PID 1 就是 python。
- SIGTERM 直接送到 python
- python 若有 handler 就能 graceful shutdown
看
CMD跟ENTRYPOINT是否用 JSON 陣列形式([...]),就能區分 exec 還是 shell mode。陣列形式 = exec,純字串 = shell。
問題五:應用本身的 PID 1 行為差異#
某些 runtime 對「自己當 PID 1」這件事額外處理過:
- Java(OpenJDK 10+):對 SIGTERM 有預設行為(觸發 shutdown hook)
- Node.js:預設不裝 SIGTERM handler,作者必須自己
process.on('SIGTERM', ...) - Python:預設把 SIGTERM 翻成 KeyboardInterrupt?不,預設是直接 terminate;但若程式啟動了 thread / asyncio loop,行為不一定如預期
- Bash 腳本:必須顯式用
trap才會處理 SIGTERM
不要假設「我的 runtime 應該會處理 SIGTERM」。寫個 1 行的測試 container,docker stop 看一下實際行為才準。
解法:給 Container 一個正規 PID 1#
最普遍的解法:用一個小型 init 當 PID 1,再讓它去 exec 應用。
- tini:krallin/tini,docker 內建支援,
docker run --init - dumb-init:Yelp 出品,行為類似
- s6-overlay:偏向 multi-process container 的方案
下一章 signals 與後續 signal-and-container 會展開 signal 機制本身與 docker stop 的細節。
延伸閱讀#
- Tini README:https://github.com/krallin/tini ↗
- dumb-init README:https://github.com/Yelp/dumb-init ↗
- Docker docs:ENTRYPOINT exec form vs shell form