題目與適用情境
一台使用 cgroup v2 的 Linux 主機上,容器 A 有以下練習約束:
| 約束 | 對應的 cgroup v2 值 | |---|---| | 記憶體 | memory.max = 536870912 位元組,即 512 MiB | | CPU | cpu.max = 50000 100000,即每 100ms 最多使用 50ms | | task 數 | pids.max = 128 |
容器內的應用程式把自己看到的 PID 顯示為 1,看到容器自己的 hostname、掛載表和網路介面。請說明 runtime 如何建立這個環境、隔離分別來自哪裡、觸及限制後如何失敗,並解釋共用核心的安全邊界。最後要用可觀察的核心狀態證明設定生效,不能只用一次成功的 docker run 當證據。
這道題適用於 Linux、平台工程、SRE、DevOps、基礎設施、雲端、安全和後端職位。題中數值只是練習條件,不是建議預設值。這裡的「容器」指 Linux 上的 OCI 風格行程容器;由虛擬機承載的容器產品還會多一層不同的隔離。
面試官考察重點
第一層是能否用準確的行程模型取代「輕量虛擬機」這個模糊說法。Linux 容器本質上是主機上的一棵行程樹。namespace 改變這組行程能看到的全域資源實例,包括 PID、掛載、網路物件、IPC 物件、主機識別、使用者 ID、cgroup 路徑,以及可選的時鐘。rootfs 和獨立掛載檢視提供使用者空間檔案。這些機制都沒有建立第二個核心。
第二層是區分可見性與資源控制。namespace 回答「行程能看到或修改哪個資源實例」,cgroup 回答「這組行程能消耗多少,以及如何計量」。PID namespace 不限制 task 數;cgroup 的 PID 限制也不會隱藏主機行程。mount namespace 改變掛載檢視,rootfs 提供檔案內容;單獨使用 chroot 既不是完整容器,也不是安全邊界。
第三層是根據具體 cgroup v2 檔案推導結果。memory.max 是記憶體硬邊界,回收失敗後可能觸發 cgroup 內的 OOM kill;cpu.max 是頻寬控制,50ms 配額在 100ms 週期內耗盡後會 throttle,行程不會因此被終止;pids.max 會讓違反限制的 fork() 或 clone() 以 EAGAIN 失敗。強回答還會給出區分這三種結果的事件計數器。
第四層是安全判斷。容器共用主機核心,所以 namespace 加 cgroup 不等於虛擬機邊界。使用者 ID 對應、最小 capability 集、nonewprivs、seccomp、AppArmor 或 SELinux 這類 LSM、唯讀或 masked mount,以及受限裝置會縮小攻擊面。privileged 容器、host PID 或 network namespace、容器引擎 socket、寬泛的主機目錄掛載,都可能主動拆掉重要邊界。
最後是驗證能力。回答應從邊界兩側比較 namespace inode 識別、UID/GID 對應、cgroup 歸屬與 controller 檔案、行程 capability、seccomp 狀態、掛載傳播、網路介面和故障計數,而不只複述設定參數。
回答前需要釐清的問題
- 目標 runtime 和容器模式是什麼? 本題假設 Linux 上的 OCI runtime。rootless、user namespace remap、privileged 和虛擬機沙箱會改變信任邊界。
- 「0.5 CPU」指 quota 還是相對權重? 本題指每 100,000 微秒最多執行 50,000 微秒的頻寬上限;
cpu.weight只在競爭時調整相對份額。 - 512 MiB 包含哪些記憶體? cgroup v2 會計量匿名記憶體、page cache、部分核心結構和 socket buffer 等主要項目,但單一檔案不可能控制所有主機資源。
- 128 指行程還是核心 task? pids controller 使用核心 task ID,執行緒也消耗預算;高執行緒數 runtime 必須考慮這點。
- 是否啟用 swap,並單獨限制了嗎?
memory.max與memory.swap.max是不同控制項。本題只固定記憶體上限,swap 政策必須實查,不能假設。 - 究竟設定了哪些 namespace? OCI 設定可以新建、依路徑加入既有 namespace,也可以省略某一類型並繼承 runtime 的 namespace。漏掉一個就會真實改變邊界。
- 威脅模型是什麼? 執行惡意程式碼的多租戶平台可能還需要 VM 或 microVM;可信任的內部工作負載可以接受另一種取捨。
30 秒回答架構
「Linux 容器是以 rootfs 啟動、放入指定 namespace 與 cgroup 的主機行程樹,主機核心仍然共用。namespace 隔離檢視,cgroup 計量並限制消耗。本題中,memory.max=536870912 在回收失敗後可能觸發 cgroup OOM;cpu.max=50000 100000 讓 fair-class 工作每 100ms 最多執行 50ms,之後 throttle;pids.max=128 讓違反限制的 fork 或 clone 回傳 EAGAIN。
我會在適用時加入 UID 對應、最小 capability、nonewprivs、seccomp、AppArmor 或 SELinux、安全 mount 與受限裝置。驗證時從邊界兩側比較 namespace 識別、UID map、mount、介面、有效 cgroup 檔案、capability 與安全狀態,再做有界故障測試,並要求看到對應的 memory.events、cpu.stat 與 pids.events 證據。」
分步深入解答
第一步:從主機行程模型出發
runtime 接收包含 rootfs 與設定的 OCI bundle。一條具代表性的建立流程是:
- 驗證 bundle、可執行檔、mount、namespace 選擇、憑證與資源設定;
- 建立或選擇 cgroup 子樹並寫入 controller 值;
- 新建 namespace,或加入設定指定的既有 namespace;
- 使用 user namespace 時建立 UID/GID 對應;
- 設定安全的 mount propagation,掛載 rootfs 與特殊檔案系統,再切換行程根目錄;
- 新建 network namespace 時建立或移動網路裝置並設定路由;
- 設定憑證、capability、
nonewprivs、seccomp 與 LSM label/profile; - 把行程加入 cgroup,並
exec設定的應用程式。
不同 runtime 的精確順序和輔助行程會不同,穩定不變的是可觀察的核心狀態:應用程式仍是由主機排程的行程,帶有 namespace 歸屬、憑證、mount、filter 與 cgroup 路徑。若 runtime 在設定中途崩潰,清理邏輯必須移除 mount、介面、namespace pin 和 cgroup;核心裡沒有一個名為「container」的物件自動包辦這些清理。
第二步:為每種 namespace 分配清楚職責
常用 namespace 互相補充:
| Namespace | 隔離的檢視 | 重要邊界 | |---|---|---| | PID | 行程 ID 空間與可見性 | 同一 task 同時有容器內 PID 和主機 PID;容器 PID 1 要正確回收子行程和處理訊號 | | Mount | 掛載點與傳播關係 | 改變的是掛載表,不會自動隔離底層儲存或核心 | | Network | 介面、路由、連接埠、socket 與網路堆疊 | 透過 veth、bridge、路由或其他 driver 有意恢復連線 | | UTS | hostname 與 NIS domain name | 只呈現識別,不提供驗證 | | IPC | System V IPC 與 POSIX message queue | 透過 mount 明確共用的檔案或 socket 仍可連接 workload | | User | UID/GID 對應與 namespace 範圍內的 capability | 容器內 UID 0 可對應到主機一般 UID | | Cgroup | cgroup 階層的檢視 | 資源執行來自 controller,不來自 cgroup namespace 的檢視 | | Time | boot 與 monotonic clock offset | 不等於擁有任意獨立的硬體牆鐘 |
OCI namespace 清單若省略某一類型,容器就繼承 runtime 的對應 namespace。--pid=host、host network 或加入另一容器的 namespace 可以是明確需求,但回答必須指出失去哪層隔離,不能繼續把它描述成全私有邊界。
檔案系統邊界同時需要 mount namespace 與 rootfs。private 或 slave propagation 能阻止容器 mount 事件意外傳播回主機;唯讀 mount、masked path、最小 /dev 和明確的 bind mount 繼續縮小存取。若把主機根目錄可寫掛入,或把容器引擎 socket 暴露進去,即使 hostname 私有,也已經形成高風險直達路徑。
第三步:推導三種資源限制的結果
記憶體。 memory.max=536870912 是該 cgroup 及後代的主要硬上限。接近限制時核心先嘗試回收;達到限制且無法降低使用量時,會進入 cgroup OOM 處理。預設模式可能選擇該 cgroup 內的一個 task,memory.oom.group=1 可要求把 workload 當作不可分割整體。瞬間讀值可能短暫高於限制。應檢查 memory.current、memory.peak,以及 memory.events 中的 max、oom、oomkill、oomgroup_kill。沒有這些計數和核心或 runtime 證據,不能把每次 SIGKILL 都診斷成 cgroup OOM。
CPU。 cpu.max=50000 100000 表示 fair-class task 在每個 100,000 微秒週期內最多消耗 50,000 微秒,即平均 0.5 CPU。多個執行緒可以並行用完這份 quota,於是更早進入本週期的 throttle;runnable task 會等下一份 quota,不會被終止。應查看 cpu.stat 的 usageusec、nrperiods、nrthrottled 與 throttledusec。因此,即使主機還有閒置 CPU,週期邊界附近的延遲尖峰也可能來自 quota throttle。
Task。 pids.max=128 是核心 task 數的階層硬限制,執行緒也計數。若建立新 task 會違反政策,fork() 或 clone() 會以 EAGAIN 失敗,既有 task 繼續執行。應查看 pids.current、pids.peak 與 pids.events 中的 max。移動既有 task 或把上限調到目前值以下,可以暫時出現 pids.current > pids.max;建立路徑仍不能繼續違反政策。
父 cgroup 同樣約束子 cgroup,子層無法獲得祖先沒有允許的 CPU、記憶體或 task 容量。反過來,這三項設定也不會自動限制全部資源;磁碟空間、I/O、檔案描述元、網路頻寬、裝置和全域核心物件還需要各自的控制與操作上限。
第四步:圍繞共用核心疊加安全機制
user namespace 允許行程在容器內為 UID 0,同時在外部對應為一般 UID。這樣可以減輕 namespace escape 或錯誤存取主機檔案的影響,但 ID mapping 與檔案擁有權必須一起設計。沒有 user namespace 時,容器 root 仍是主機 UID 0,只是被 capability 與可存取物件等機制約束。
capability 把傳統 root 權限拆分。應從最小集合開始,而不是授予全部 capability。移除 capability 還要防止行程透過 file capability、set-user-ID 程式或其他路徑重新取得;bounding set 配合 nonewprivs 才更容易稽核。
seccomp 依 syscall 與參數執行 allow、deny、trap、kill、log 或 notify,減少能觸及的核心攻擊面,但它不理解應用層授權。AppArmor、SELinux 等 LSM 對檔案、行程、socket 與其他物件套用政策。唯讀檔案系統、masked /proc 路徑與最小裝置集又提供彼此獨立的約束。
這些機制是疊加關係,不能互相取代。cgroup 主要負責計量與資源型阻斷服務,不能阻止容器讀取另一個容器的資料;namespace 主要隔離檢視,也不會修補共用核心漏洞。面對惡意租戶,核心逃逸風險或合規要求可能需要 VM、microVM 或沙箱核心邊界。
第五步:從邊界兩側驗證核心狀態
先在主機上找到容器 init 的 host PID 與 cgroup 路徑,再做通用檢查:
pid=<host-pid-of-container-init>
cg=/sys/fs/cgroup/<container-cgroup>
readlink /proc/$pid/ns/{pid,mnt,net,uts,ipc,user,cgroup}
cat /proc/$pid/uid_map
cat /proc/$pid/gid_map
cat /proc/$pid/cgroup
cat "$cg/memory.current" "$cg/memory.max" "$cg/memory.events"
cat "$cg/cpu.max" "$cg/cpu.stat"
cat "$cg/pids.current" "$cg/pids.max" "$cg/pids.events"
grep -E '^(CapPrm|CapEff|CapBnd|NoNewPrivs|Seccomp):' /proc/$pid/status把 namespace symlink 的裝置號與 inode 同主機、另一容器比較。識別不同證明是不同 namespace 實例,但不能單獨證明 mount、route 或 policy 安全;還要檢查真實 mount table 與 propagation、網路 link 與 route、有效 capability、seccomp mode、LSM label 和 device node。
容器內則記錄 /proc/1/status、/proc/self/cgroup、mount、hostname、可見行程、介面、路由、UID/GID 與 namespace link。主機上的 nsenter 只應由獲授權人員用於診斷;能以高權限進入 namespace 不是隔離失敗的證據。
最後在一次性環境做有界故障實驗:逐步分配記憶體並與 memory.events 對齊;執行 CPU workload 並與 nr_throttled 對齊;建立執行緒或行程,直到下一次建立回傳 EAGAIN 且 pids.events 增加。實驗要在形成主機層級壓力前停止,並確認 sibling cgroup 仍然健康。
第六步:把觀察結果寫成驗收標準
一份可辯護的驗收紀錄應包含數值、識別與結果:
- 需要隔離的 namespace ID 不同,只有明確共用的才相同;
- UID 0 mapping、有效 capability、
NoNewPrivs、seccomp mode 與 LSM label 符合威脅模型; - mount propagation、bind mount、masked/read-only path 與 device access 符合 OCI 設定;
- 有效 cgroup 的
memory.max、cpu.max、pids.max分別為 536870912、50000 100000、128; - 記憶體壓力改變對應 memory event,CPU 壓力增加 throttle 計數但不終止行程,已計入 128 個 task 時第 129 個建立要求被拒絕;
- 每次實驗中 parent 與 sibling cgroup 都維持自己的預算;
- restart 與強制失敗後,沒有意外遺留 mount、介面、namespace pin 或 populated cgroup。
「第 129 個」這個說法有前提:有效階層裡正好已有 128 個 task,且沒有同時退出。真實測試必須在建立前立即讀取 pids.current,不能假定應用程式只有一個 task。
高品質示範回答
「我先用主機行程模型回答。runtime 讀取 OCI rootfs 與設定,新建或加入 PID、mount、network、UTS、IPC、user、cgroup、time namespace,設定 mount 和網路,把行程樹加入 cgroup,套用憑證與安全政策,最後 exec 應用程式。容器內的 PID 1 仍是主機上的一個行程,只是在外部有另一個 PID;它擁有私有使用者空間檢視,但主機核心仍然共用。
namespace 與 cgroup 解決不同問題。PID namespace 改變行程可見性;mount namespace 配合 rootfs 改變可見檔案系統;network namespace 提供自己的介面、route、port 和 socket;user namespace 可把內部 UID 0 對應為主機一般 UID。cgroup 計量並控制行程樹,但不隱藏主機物件。
本題的 memory.max=536870912 是 512 MiB 硬邊界。核心先嘗試回收,無法降低使用量時可能進入 cgroup OOM;我會讀 memory.events 區分 max、oom 與 oomkill。cpu.max=50000 100000 讓 fair-class task 每 100ms 最多使用 50ms,quota 用完後 throttle,cpu.stat 的 nrthrottled 與 throttled_usec 會增加,不存在 CPU 限額造成的 kill。pids.max=128 統計包括執行緒在內的核心 task;違反上限的 fork 或 clone 回傳 EAGAIN,pids.events 記錄命中。
因為核心共用,我會在適用時疊加 user namespace、最小 capability、nonewprivs、seccomp、AppArmor 或 SELinux、唯讀或 masked mount 與最小裝置集。privileged、host namespace、寬泛主機掛載和引擎 socket 都必須是明確可信任需求;惡意多租戶還可能需要 VM 或 microVM。
驗證時,我會從主機和容器比較 /proc/{PID}/ns/* 的裝置號/inode、UID/GID map、mount、介面、cgroup 路徑與 controller 檔案、capability、seccomp mode 和 LSM label,再做有邊界的記憶體、CPU、task 實驗,並要求看到對應核心計數與失敗模式。容器能啟動,不等於隔離已被證明。」
常見錯誤與改善建議
- 把容器稱為小型 VM → 掩蓋共用核心,造成錯誤安全假設 → 描述主機行程及其 namespace、cgroup、filesystem、credential 和 policy 狀態。
- 說 namespace 限制 CPU 與記憶體 → namespace 隔離的是檢視,不是消耗量 → 把資源上限對應到 cgroup controller 與具體檔案。
- 說 cgroup 隔離檔案與行程 → cgroup 負責分組、計量與約束 → 把可見性對應到 PID 與 mount namespace。
- 把
chroot當容器 → 改變表面根目錄沒有增加 PID、network、user 或資源隔離 → 把 rootfs 與所需 namespace 和 policy 組合。 - 聲稱 CPU 限制會終止行程 → CPU quota 通常 throttle runnable fair-class task → 檢查
cpu.stat的 throttle 證據。 - 期待記憶體讀值嚴格停在 512 MiB → reclaim、計量時序與瞬間超界會影響某一刻讀值 → 用
memory.max和 event counter 判斷執行結果。 pids.max只數行程 → controller 數核心 task,執行緒也占額度 → 限制實驗前讀取pids.current。- 假定容器 root 天然無害 → 未使用 user namespace 時仍可能是主機 UID 0 → 檢查
uid_map與 capability。 - 把 namespace 分離等同安全租戶邊界 → 核心漏洞或危險主機掛載可能跨越行程邊界 → 先定義威脅模型,再疊加 policy 或 VM。
- 只檢查設定,不檢查有效狀態 → 參數可能被繼承、覆寫或部分失敗 → 讀取
/proc和 cgroup 檔案,並實際觸發每種限制。
延伸追問
追問一:為什麼容器裡的 PID 1 需要特殊處理?
PID namespace 的第一個行程對後代顯示為 PID 1,會接管孤兒行程並需要回收子行程,否則 zombie 會累積並消耗 pids 預算。PID 1 還有特殊的 signal 處理語義,不能轉發訊號的 wrapper 會讓優雅終止失敗。應驗證真實 init、child reaping、signal forwarding 與 shutdown deadline,而不是假定應用程式框架已經處理。
追問二:cpu.max 與 cpu.weight 有什麼差異?
cpu.max 給 fair-class 工作設定每週期最大頻寬;本週期 quota 用完後,即使主機還有閒置 CPU,也可能 throttle。cpu.weight 只在 runnable sibling cgroup 競爭時調整比例,本身沒有定義硬性的 0.5 CPU 上限。正式環境政策可能出於不同目的同時使用兩者。
追問三:容器 root 能否在主機上是非特權使用者?
可以。user namespace 可把容器內 UID 0 對應到外部一般 UID range,應透過 /proc/{PID}/uidmap 與 gidmap 驗證。它縮小主機權限,但 bind mount 擁有權、subordinate ID 分配、所屬 user namespace 內的 capability 與核心攻擊面仍需檢查。
追問四:為什麼 private mount namespace 仍不足以保證檔案安全?
它隔離 mount table,但 runtime 決定裡面出現哪些底層檔案系統和 bind mount。一個 private namespace 若包含主機 / 的可寫 bind mount,仍然暴露主機根目錄。要把 mount source、propagation、writable flag、masked/read-only path、device node、credential 與 LSM policy 一起檢查。
追問五:什麼情境應優先使用 VM 或 microVM?
互不信任的租戶執行任意程式碼、無法接受共用核心逃逸風險、合規要求獨立核心,或 workload 需要不同核心版本和 module 時,更強邊界通常更合適。代價是額外啟動時間、記憶體、映像檔和維運成本。應按威脅模型與平台實測約束決定,而不是只看容器或 VM 標籤。
追問六:容器很慢但沒有 OOM kill,如何定位?
把應用程式延遲與 cpu.stat throttle、memory.events 的 high/max 壓力、pressure stall information、I/O controller 統計、主機排程和網路錯誤對齊。nrthrottled 或 throttledusec 持續增加而 OOM 計數不變,支持 CPU quota 方向;reclaim 或 I/O 壓力也可能只造成 stall 不 kill,所以必須用統一時間線和有效 cgroup 祖先關係判斷。