在上一章節,我們介紹了 Linux 的 cgroup(Control Groups)技術用於資源限制的概念。本章將聚焦於 Docker 容器,通過實驗探索資源限制的實際效果和特性。
開始之前
在該使之前,讓我們先準備好測試環境和工具。
Docker 資源限制指令
首先,通過以下指令查看 Docker 提供的資源限制參數:
1docker container run --help2#3[...]4 -c, --cpu-shares int CPU shares (relative weight)5 --cpus decimal Number of CPUs6 --cpuset-cpus string CPUs in which to allow execution (0-3, 0,1)7 --cpuset-mems string MEMs in which to allow execution (0-3, 0,1)8[...]9 -m, --memory bytes Memory limit10 --memory-reservation bytes Memory soft limit11 --memory-swap bytes Swap limit equal to memory plus swap: '-1' to enable12 unlimited swap13[...]
其中,--memory-reservation
的描述中提到它是一種「軟性限制」,怎麼記憶體也有「軟性限制」?
實際上,它更接近於「記憶體保留」的功能:
- 記憶體充足時,容器可以超過此限制使用更多記憶體。
- 記憶體緊張時,Docker 會嘗試回收記憶體,將容器使用量控制在此限制內。
- 適用於「基線需求」場景,確保容器擁有足夠的記憶體啟動或運行。
這與 Kubernetes 中的 request
和 limit
概念一致,可參考 Kubernetes Limit Range 官方文件。
壓力測試工具
stress-ng 是一款強大的 Linux 壓力測試工具,支持模擬多種資源負載(如 CPU、內存、I/O 和網絡),幫助用戶測試系統在高負載下的性能和穩定性。
Docker Image 構建
由於基礎鏡像 Alpine 不包含 stress-ng
,我們需要手動構建。
1FROM alpine:latest2
3# 安裝必要工具4RUN apk update && apk add --no-cache stress-ng5
6# 設置 ENTRYPOINT 使容器啟動時執行壓力測試命令7ENTRYPOINT ["stress-ng"]
執行以下指令構建鏡像:
1docker image build -f Dockerfile.stress -t stress:alpine .
記憶體測試 Image 建構
為了更好地測試記憶體限制,我們構建一個專用鏡像,包含自定義腳本。
腳本:run.sh
1#!/usr/bin/env sh2
3timeout 20 sh -c '4used=05for i in $(seq 1 10); do6 sleep 27 used=$((used + 100))8 stress-ng --vm 1 --vm-bytes 100M --vm-keep --quiet &9 echo "Used Memory: ${used}M"10done11wait12'
說明 每隔 2 秒新增一個
100M
記憶體佔用的stress-ng
工作,並累加輸出當前已用記憶體量。
範例輸出:
1Used Memory: 100M2Used Memory: 200M3Used Memory: 300M4...5Used Memory: 1000M
Dockerfile
1FROM alpine:latest2
3# 安裝必要工具4RUN apk update && apk add --no-cache stress-ng5
6# 複製腳本到容器7COPY run.sh /usr/local/bin/run.sh8
9# 確保腳本可執行10RUN chmod +x /usr/local/bin/run.sh11
12# 設置 ENTRYPOINT13ENTRYPOINT ["/usr/local/bin/run.sh"]
構建指令:
1docker build -f Dockerfile.stress.memory -t stress:memory .
觀察工具
在測試過程中,使用以下工具監控資源使用狀況:
htop
:交互式系統監控工具,用於查看 CPU、內存等系統資源使用情況。docker stats
:顯示運行中容器的資源使用情況。tail -f /var/log/syslog | grep -i "oom"
:監控系統日誌中有關 OOM(Out of Memory)的記錄。
實驗一:限制 CPU 資源
測試 1:限制至 0.5 個 CPU
1docker container run -it --rm --name stress --cpus=0.5 stress:alpine -c 1 -t 10
結果:
測試 2:限制至 1 個 CPU
1docker container run -it --rm --name stress --cpus=1 stress:alpine -c 1 -t 10
結果:
測試 3:限制至 1 個 CPU,負載增加至 2 個 CPU
1docker container run -it --rm --name stress --cpus=1 stress:alpine -c 2 -t 10
結果:
測試 4:限制至 1.5 個 CPU,負載增加至 2 個 CPU
1docker container run -it --rm --name stress --cpus=1.5 stress:alpine -c 2 -t 10
結果:
觀察結果:
即使壓力測試軟體將負載拉滿,基於 cgroup 的執行環境仍不能突破容器設置的資源限制。
實驗二:CPU 的軟性限制
在 CPU 資源限制中,容器執行時間由作業系統分配的運行時間決定。以下實驗旨在驗證這一點。
測試指令
1time sh -c 'for i in $(seq 1 3000000); do :; done'
上述指令執行一個簡單的空迴圈,重複 3,000,000 次,並測量執行時間。
範例輸出:
1real 0m0.882s2user 0m0.728s3sys 0m0.199s
real
:實際運行時間(包含所有等待時間)。user
:CPU 在用戶模式下執行的時間。sys
:CPU 在核心模式下執行的時間(如 I/O 操作)。
容器中執行測試
通過以下指令將測試整合至容器:
1docker container run -it --rm --name stress --cpus=1 --entrypoint ash stress:alpine -c "time sh -c 'for i in \$(seq 1 3000000); do :; done'"
對照組:--cpus=1
在限制為 1 個 CPU 的容器中執行上述指令:
實驗組:--cpus=0.5
在限制為 0.5 個 CPU 的容器中執行:
結論:
- 程式的 實際執行時間(user 時間) 不會因為 CPU 限制而改變。
- 當限制 CPU 資源時,程式會因 等待執行的時間增加,導致整體執行時間(real 時間)變長。
這與 CPU 使用權限的分配機制相吻合。
實驗三:CPU 的權重調度
cgroup
使用 cpu-share
來決定 CPU 資源的分配權重。通過 Docker 提供的 --cpu-shares
參數,我們可以調整容器的資源權重。
測試指令
1docker container run -it --rm --name stress --cpu-shares=256 stress:alpine
若未指定 --cpu-shares
,容器的預設值為 1024
。這是一個相對權重,與其他容器的 cpu-share
比例共同決定 CPU 分配。
資源分配公式
當系統資源不足時,cpu-share
決定了 CPU 分配的比例:
測試 1:資源充足情況下,權重對分配無影響
docker-compose.yml
1version: '2'2services:3 stress:4 image: stress:alpine5 container_name: stress6 command: ["-c", "1", "-t", "10"]7 cpu_shares: 2568
9 stress2:10 image: stress:alpine11 container_name: stress212 command: ["-c", "1", "-t", "10"]13 cpu_shares: 51214
15 stress3:10 collapsed lines
16 image: stress:alpine17 container_name: stress318 command: ["-c", "1", "-t", "10"]19 cpu_shares: 76820
21 stress4:22 image: stress:alpine23 container_name: stress424 command: ["-c", "1", "-t", "10"]25 cpu_shares: 1024
執行結果:
在資源充足時,權重對容器的 CPU 使用沒有影響。
測試 2:資源不足時的權重分配
設定 WSL 資源限制
在 Windows Host 的 %UserProfile%\.wslconfig
文件中添加以下配置:
1[wsl2]2memory=4GB3processors=2
重新啟動 WSL 後,資源限制生效。

再次執行 測試 1 中的 docker-compose.yml
,觀察結果:
測試 3:默認 cpu-share
值
根據 Docker 官方文檔,未設置 cpu-shares
的容器將採用默認值 1024
。我們來實驗看看。
稍微修改 docker-compose.yml
,移除 stress4
的 cpu-shares
配置:
1version: '2'2services:3 stress:4 image: stress:alpine5 container_name: stress6 command: ["-c", "1", "-t", "10"]7 cpu_shares: 2568
9 stress2:10 image: stress:alpine11 container_name: stress212 command: ["-c", "1", "-t", "10"]13 cpu_shares: 51214
15 stress3:9 collapsed lines
16 image: stress:alpine17 container_name: stress318 command: ["-c", "1", "-t", "10"]19 cpu_shares: 76820
21 stress4:22 image: stress:alpine23 container_name: stress424 command: ["-c", "1", "-t", "10"]
執行結果:
未設定 cpu-shares
的容器默認值為 1024
,其分配的 CPU 資源比例高於其他容器。
觀察結果
從實驗結果來看,沒有設定 cpu-shares
的容器,在資源搶奪上的優先度是高於有設定 cpu-shares
容器。
結論:
- 在資源不足的情況下,容器的 CPU 分配比例由
cpu-shares
決定。權重較高的容器將獲得更多的 CPU 資源。 - 未設定
cpu-shares
的容器,在資源搶奪上的優先度高於有設定cpu-shares
容器。
實驗四:限制記憶體資源
記憶體資源限制是容器資源控制的重要部分。以下實驗將展示如何使用 Docker 限制容器記憶體,並觀察系統的行為。
測試 1:不限制記憶體
當未設定記憶體限制時,容器預設可以使用宿主機的所有可用記憶體。
測試指令:
1docker container run -it --rm --name stress stress:memory
結果:
測試 2:限制記憶體使用量
將容器的記憶體限制為 300MB,觀察系統的行為。
測試指令:
1docker container run -it --rm --name stress --memory 300m stress:memory
結果:
觀察現象:
- 容器記憶體耗盡後,並未觸發 OOM(Out of Memory)Kill。
相反,容器的 Swap 使用量開始上升。
分析
Swap 是 Linux 系統的虛擬記憶體,使用磁盤空間模擬記憶體,因此在記憶體耗盡時,會切換到使用 Swap。 當未限制容器的 Swap 使用量時,容器可以使用的 Swap 等於設置的記憶體限制(此例為 300MB)。
至於為什麼 Swap 使用量也耗盡後,為什麼還是沒觸發 OOM Kill 呢? 其實它已經發生了,下面我們來驗證。
測試 3:限制記憶體與 Swap 使用量
進一步限制容器的總記憶體(物理記憶體 + Swap)為 300MB,模擬更嚴格的限制條件。
系統監控
在宿主機中開啟新終端,執行以下指令監控 OOM 現象:
1tail -f /var/log/syslog | grep -i "oom"
測試指令:
1docker container run -it --rm --name stress --memory 300m --memory-swap 300m stress:memory
--memory
限制物理記憶體。--memory-swap
限制總記憶體(物理記憶體 + Swap)。
結果:
觀察現象:
- 當容器記憶體與 Swap 使用達到限制時,觸發 OOM Kill。
- 容器內的某些進程被強制終止,但容器未停止運行。
分析:為何容器未被關閉?
這牽扯到兩個概念:
- 在 Docker 中,容器的 主進程(PID=1) 若未被 OOM Kill,則容器仍會保持運行狀態。詳情參考 Tracking Down “Invisible” OOM Kills in Kubernetes
- 使用
stress-ng
壓力測試工具時,它作為主進程(PID=1)啟動。壓力測試石的 Worker 則是stress-ng
產生的子進程。
也就是說,當我們使用 stress-ng
測試,發生 OOM Kill 的進程並不是主進程,而是其他子進程,難怪容器不會被殺掉。
為了驗證這個說法,我們進行下一個實驗。
測試 4:模擬主進程觸發 OOM
為驗證主進程 OOM 時容器的行為,我們使用 Python 編寫腳本進行實驗。
memory_stress_test.py
1import sys2import time3
4def trigger_oom():5 # 創建一個巨大的列表來快速消耗記憶體6 memory_list = []7 current_size = 08
9 print("開始觸發 OOM...")10
11 while True:12 try:13 # 每次分配約 100MB 的記憶體14 chunk = ['x'] * (25 * 1024 * 1024)15 memory_list.append(chunk)11 collapsed lines
16 current_size += 10017
18 print(f"當前已分配記憶體: {current_size} MB")19 time.sleep(0.5)20
21 except MemoryError:22 print("記憶體分配失敗")23 break24
25if __name__ == "__main__":26 trigger_oom()
Dockerfile
1FROM python:3.9-slim2
3WORKDIR /app4
5COPY memory_stress_test.py .6
7CMD ["python", "memory_stress_test.py"]
構建指令:
1docker build -f Dockerfile.stress.memory.python -t stress:memory-py .
測試指令
1docker run -it --memory=500m stress:memory-py
結果:
確認 OOM 狀態
檢查容器的狀態:
1docker container ls -a
輸出範例:
1CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES25f3679d1c217 stress:memory-py "python memory_stres…" 34 seconds ago Exited (137) 31 seconds ago heuristic_elbakyan
查看詳細資訊
使用 docker inspect
查看容器的結束狀態:
1docker container inspect 5f3679d1c217 --format '{{json .State}}' | jq
結果範例:
1{2 "Status": "exited",3 "Running": false,4 "Paused": false,5 "Restarting": false,6 "OOMKilled": true,7 "Dead": false,8 "Pid": 0,9 "ExitCode": 137,10 "Error": "",11 "StartedAt": "2024-12-10T11:20:50.956018772Z",12 "FinishedAt": "2024-12-10T11:20:52.491126925Z"13}
解讀:
- ExitCode 137 表示容器因 OOM 而退出。
OOMKilled
為true
,證明主進程觸發 OOM 導致容器被終止。
結論
通過上述實驗,我們證實了容器內的資源限制功能如何影響應用運行:
- CPU 資源限制:影響程式執行時間的等待部分,而非實際執行時間。
- 記憶體資源限制:觸發 OOM Kill 的條件與主進程和子進程的行為密切相關。
- Swap 的作用:在記憶體不足時,未限制的 Swap 會延緩 OOM 的發生。
希望這篇文章能幫助你更深入理解 Docker 的資源限制機制,並靈活應用於實際場景。
參考
- https://github.com/opencontainers/runc/blob/main/docs/cgroup-v2.md
- https://kubernetes.io/zh-cn/docs/concepts/architecture/cgroups/#cgroup-v2
- https://itplus.ithome.com.tw/webinar-page/239
- https://koding.work/how-to-use-docker-cpu-resource-limit/
- https://docs.docker.com/engine/containers/resource_constraints
- https://docs.redhat.com/en/documentation/red_hat_enterprise_linux/6/html/resource_management_guide/sec-cpu
- https://www.agileconnection.com/article/overview-linux-exit-codes