0%

為什麼 Go 程式在 Docker 裡當 PID 1 會導致 prefork 問題?

前言

這篇是我在 containerize Go 專案時踩到的一個坑,主要是 prefork 模式無法啟動,最後才發現是因為我直接用 CMD ["./build/app"],導致這個程式變成 PID 1。後來試了 sh -c--pid=host 之後才搞定,順便把這整個過程紀錄一下。


問題起因

一開始我用最常見的寫法:

1
CMD ["./build/app"]

這樣在容器裡 build/app 會變成 PID 1,而 Linux 下 PID 1 是非常特別的存在,有一些額外的行為限制。結果導致我使用的 Go prefork(應該是類似 fiber 那種)完全無法正常跑。

解法一:用 shell 包一層

後來試著用這種方式:

1
CMD ["sh", "-c", "./build/app"]

這樣的好處是:

  • 容器裡 PID 1 是 sh
  • build/app 變成子程序,可以自由 fork
  • Go 的 prefork 終於正常執行了

解法二:加上 –pid=host

我也試了下面這個參數:

1
docker run --pid=host my-image

這是直接把容器的 PID namespace 切換成 host 的,讓容器的程式不會再被限制只能是自己的 PID 空間。這樣 CMD ["./build/app"] 也可以成功啟動 prefork。


那 PID 1 到底哪裡有問題?

Linux 的 PID 1 有幾個特性:

  1. Signal 處理特別:PID 1 預設會忽略 SIGTERM / SIGINT,必須自己寫 handler
  2. Zombie process 不會自動回收:如果有 fork 出子程序,要自己 wait(),不然會累積成 zombie
  3. 某些 runtime(像 Go)根本沒考慮 PID 1 情境:導致 prefork 無法用

為什麼 –pid=host 也能解?

這是因為 Go 的 prefork 能否正常運作,關鍵在於程式能否 fork 子程序並共用 socket,而不單純是 PID 1 的問題。

當你加上 --pid=host,雖然容器內的 Go 程式還是 PID 1,但在 host 上看起來不是,因此 prefork 可以正常啟動。

不過這樣做會讓容器看到 host 上所有的 process,帶來安全性疑慮。雖然預設下(非 privileged container)你沒有權限 kill 其他 process,但如果加上 --privileged 或額外 capability,就有可能影響 host 上的服務,因此不建議這樣做。


小結

寫法Prefork OK安全性
CMD ["./build/app"]
CMD ["sh", "-c", "./build/app"]
CMD ["./build/app"] + --pid=host

簡而言之,Go 程式在 Docker 裡當 PID 1 會讓 prefork 失效,建議用 shell 包一層或用 tini 等 init 解決。