重溫 Linux ulimit 與 Docker Cgroups,為 AI Agent 打造物理級熔斷器

English Abstract: This article revisits Linux ulimit and Docker cgroups as hard resource limits for AI agent containers. It explains why per-UID nproc limits can cause cross-container outages, why --pids-limit is the container-level fork-bomb guard, and how to scope CPU-time limits to individual child processes so that a runaway script is terminated without killing the agent itself.

在先前的文章中,探討過 AI Agent 的本質其實就是 SRE 熟悉的控制迴圈(Loop Engineering)。近期的容器系列文也分別透過 MicroVM 沙盒 與 Egress Proxy,處理了核心隔離與金鑰保護的問題。要讓 AI Agent 在容器中安全運作,仍缺最後一塊拼圖:運算資源的防呆與熔斷(Resource Circuit Breaking)。

若 Agent 在自我修正的迴圈中寫出無窮迴圈腳本、意外觸發 Fork Bomb(無限衍生子行程),或開啟數萬個未關閉的檔案連線,單靠 Prompt 提示或應用層的 Timeout 無法攔截。此時就需要 Linux 與 Docker 中最經典的底層工具:ulimit 與 cgroups。

(1) 從「放寬限制」到「收緊牢籠」

過去撰寫 Docker 與 Linux ulimit 調校指南時,情境多半是為了應付高併發流量,例如 Nginx 或 Elasticsearch 出現 Too many open files 錯誤。當時的維運重點,是透過 --ulimit nofile=65535:65535 撐開系統限制,釋放硬體效能。

AI Agent 時代的思維則需要反過來:

  • 傳統微服務(行為已知):調高 ulimit,避免系統預設值成為流量瓶頸。
  • AI Agent(行為未知):收緊 ulimit 與 cgroups,避免失控的自主迴圈耗盡宿主機的檔案描述子(File Descriptors)、PID 與記憶體。

(2) 為什麼不能只依賴 ulimit nproc 限制行程數?

為防止 Agent 在容器內無限衍生子行程,直覺做法是設定 --ulimit nproc=128:128。但在共用 Kernel 的環境中,這是一個隱藏的架構誤區:

  1. ulimit nproc 綁定的是使用者 ID(UID),而非容器。Linux Kernel 統計 RLIMIT_NPROC 時,計算的是整台宿主機上同一 UID 的總行程數。若同一台主機上有 5 個容器都以非 root 的 UID 1000 執行,其中一個 Agent 容器衍生滿 128 個行程後,其餘 4 個正常的微服務容器也會因達到 UID 1000 的上限而無法 fork 新行程(出現 Resource temporarily unavailable),造成跨容器的連帶癱瘓。
  2. 正確作法是交給 cgroups 的 --pids-limit。它基於 cgroups 的 pids.max 機制,以單一容器為邊界,精準限制該容器的行程樹總量,不影響同主機上的其他服務。
控制機制 作用邊界 建議
--ulimit nofile 單一行程 建議調小(如 1024:2048)
--ulimit cpu 單一行程(包含 Agent 主程式) 不設在容器層級,改用子 shell 的 ulimit -t
--ulimit nproc 整台主機(同一 UID) 不建議單獨依賴
--pids-limit 單一容器 必備防線(如 100)

(3) AI Agent 容器的物理防呆配置

綜合上述原則,啟動具備程式碼執行能力的 AI Agent 容器時,可在 docker run(或 docker-compose.yml)加上以下參數,建立兩層式的物理熔斷機制。

第一層為 cgroups 容器級硬限制:最多 100 個 PID(防 Fork Bomb)、最多 2 顆 CPU、4GB 記憶體且禁用 Swap。第二層為 ulimit 行程級防呆:單一行程最多開啟 1024 個檔案(soft),硬上限為 2048。

docker run -d --name safe-ai-agent \
  --pids-limit 100 \
  --cpus="2.0" \
  --memory="4g" \
  --memory-swap="4g" \
  --ulimit nofile=1024:2048 \
  my-ai-agent:latest

如何讓 CPU 熔斷只作用於失控的子行程?

RLIMIT_CPU 是每個行程各自計時的限制,並且會被子行程繼承。若把 --ulimit cpu=60:60 直接設在容器啟動參數上,初始行程(即 Agent 主程式)同樣受限。主程式長時間運作、累計 CPU 時間超過 60 秒後,會被 Kernel 終止,導致整個容器退出,與「局部熔斷」的目的相反。

正確做法是把 CPU 限制下放到執行「不可信子任務」的那一層。Agent 呼叫外部腳本時,可在子 shell 中先設定限制再執行:

# 僅作用於此子 shell 及其子行程,不影響父行程(Agent 主程式)
( ulimit -t 60; python3 /workspace/generated_script.py )

當該腳本累計耗用 60 秒 CPU 時間,Kernel 會先發送 SIGXCPU,超過硬限制後以 SIGKILL 強制終止。父行程不受影響,因此 Agent 能在控制迴圈中收到「子任務因資源超限被終止」的回傳碼,進而修改程式碼並重新嘗試,實現「局部熔斷、整體自癒」。

注意:RLIMIT_CPU 計算的是實際 CPU 時間,而非牆上時鐘時間。若腳本因等待 I/O 而長時間卡住,需搭配 timeout 指令處理。

驗證方式

部署後,可用以下指令確認限制已生效(以 cgroups v2 為例):

# 1. 容器層級 PID 上限,預期輸出 100
docker exec safe-ai-agent cat /sys/fs/cgroup/pids.max

# 2. 行程層級檔案描述子上限,預期輸出 1024 與 2048
docker exec safe-ai-agent sh -c 'ulimit -Sn; ulimit -Hn'

# 3. 子任務 CPU 熔斷,預期回傳碼為 152(SIGXCPU)或 137(SIGKILL)
docker exec safe-ai-agent sh -c '( ulimit -t 2; while :; do :; done ); echo $?'

結語

在 AI 時代做 SRE 與基礎設施治理,並不總是需要追求全新的工具。把我們早就熟悉的 Linux ulimit 與 Docker cgroups 重新拿出來,看懂它們在 Kernel 底層的作用邊界,就能用零成本的方式,為不可預測的 AI 迴圈加上一道可靠的物理保險絲。若容器因資源超限而異常退出,可參考這篇 關於Docker Container異常排查的文章。

常見問題 FAQ

為什麼不直接用 ulimit nproc 限制容器行程數?

nproc 以 UID 計算,同一台主機上共用 UID 的其他容器會被連帶影響。應改用 cgroups 的 --pids-limit,以容器為單位限制。

--pids-limit 應設定多少?

應依 Agent 在正常負載下觀察到的最大行程數來決定,並預留緩衝(例如約 2 倍)。數值過低會讓合法的平行任務失敗,過高則失去防護效果。

ulimit 與 cgroups 會互相衝突嗎?

不會。ulimit 作用於單一行程,cgroups 作用於整個容器的行程群組,兩者同時生效,以較嚴格的限制為準。

是否可以只用 timeout 指令?

timeout 以牆上時鐘計時,RLIMIT_CPU 以實際 CPU 時間計算,兩者互補。CPU 密集的失控迴圈適合用 ulimit -t 攔截,等待 I/O 卡住的腳本則適合用 timeout。

留言

熱門文章

Docker 環境下的 Proxy 配置