使用 docker 對容器資源進行限制

在使用 docker 運行容器時,一臺主機上可能會運行幾百個容器,這些容器雖然互相隔離,但是底層卻使用着相同的 CPU、內存和磁盤資源。如果不對容器使用的資源進行限制,那麼容器之間會互相影響,小的來說會導致容器資源使用不公平;大的來說,可能會導致主機和集羣資源耗盡,服務完全不可用。

docker 作爲容器的管理者,自然提供了控制容器資源的功能。正如使用內核的 namespace 來做容器之間的隔離,docker 也是通過內核的 cgroups 來做容器的資源限制。這篇文章就介紹如何使用 docker 來限制 CPU、內存和 IO,以及對應的 cgroups 文件。

NOTE: 如果想要了解 cgroups 的更多信息,可以參考 kernel 文檔 或者其他資源。

我本地測試的 docker 版本是 17.03.0 社區版:

➜  stress docker version
Client:
 Version:      17.03.0-ce
 API version:  1.26
 Go version:   go1.7.5
 Git commit:   60ccb22
 Built:        Thu Feb 23 11:02:43 2017
 OS/Arch:      linux/amd64

Server:
 Version:      17.03.0-ce
 API version:  1.26 (minimum version 1.12)
 Go version:   go1.7.5
 Git commit:   60ccb22
 Built:        Thu Feb 23 11:02:43 2017
 OS/Arch:      linux/amd64
 Experimental: false
使用的是 ubuntu 16.04 系統,內核版本是 4.10.0:
➜  ~ uname -a
Linux cizixs-ThinkPad-T450 4.10.0-28-generic #32~16.04.2-Ubuntu SMP Thu Jul 20 10:19:48 UTC 2017 x86_64 x86_64 x86_64 GNU/Linux

NOTE: 不同版本和系統的功能會有差異,具體的使用方法和功能解釋請以具體版本的 docker 官方文檔爲準。

我們使用 stress 容器來產生 CPU、內存和 IO 的壓力,具體的使用請參考它的幫助文檔。

  1. CPU 資源

主機上的進程會通過時間分片機制使用 CPU,CPU 的量化單位是頻率,也就是每秒鐘能執行的運算次數。爲容器限制 CPU 資源並不能改變 CPU 的運行頻率,而是改變每個容器能使用的 CPU 時間片。理想狀態下,CPU 應該一直處於運算狀態(並且進程需要的計算量不會超過 CPU 的處理能力)。

docker 限制 CPU Share

docker 允許用戶爲每個容器設置一個數字,代表容器的 CPU share,默認情況下每個容器的 share 是 1024。要注意,這個 share 是相對的,本身並不能代表任何確定的意義。當主機上有多個容器運行時,每個容器佔用的 CPU 時間比例爲它的 share 在總額中的比例。

舉個例子,如果主機上有兩個一直使用 CPU 的容器(爲了簡化理解,不考慮主機上其他進程),其 CPU share 都是 1024,那麼兩個容器 CPU 使用率都是 50%;如果把其中一個容器的 share 設置爲 512,那麼兩者 CPU 的使用率分別爲 67% 和 33%;如果刪除 share 爲 1024 的容器,剩下來容器的 CPU 使用率將會是 100%。

總結下來,這種情況下,docker 會根據主機上運行的容器和進程動態調整每個容器使用 CPU 的時間比例。這樣的好處是能保證 CPU 儘可能處於運行狀態,充分利用 CPU 資源,而且保證所有容器的相對公平;缺點是無法指定容器使用 CPU 的確定值。

docker 爲容器設置 CPU share 的參數是 -c --cpu-shares,它的值是一個整數。

我的機器是 4 核 CPU,因此使用 stress 啓動 4 個進程來產生計算壓力:

➜  stress docker run --rm -it stress --cpu 4
stress: info: [1] dispatching hogs: 4 cpu, 0 io, 0 vm, 0 hdd
stress: dbug: [1] using backoff sleep of 12000us
stress: dbug: [1] --> hogcpu worker 4 [7] forked
stress: dbug: [1] using backoff sleep of 9000us
stress: dbug: [1] --> hogcpu worker 3 [8] forked
stress: dbug: [1] using backoff sleep of 6000us
stress: dbug: [1] --> hogcpu worker 2 [9] forked
stress: dbug: [1] using backoff sleep of 3000us
stress: dbug: [1] --> hogcpu worker 1 [10] forked

在另外一個 terminal 使用 htop 查看資源的使用情況:

從上圖中可以看到,CPU 四個覈資源都達到了 100%。四個 stress 進程 CPU 使用率沒有達到 100% 是因爲系統中還有其他機器在運行。

爲了比較,我另外啓動一個 share 爲 512 的容器:

➜  stress docker run --rm -it -c 512 stress --cpu 4 
stress: info: [1] dispatching hogs: 4 cpu, 0 io, 0 vm, 0 hdd
stress: dbug: [1] using backoff sleep of 12000us
stress: dbug: [1] --> hogcpu worker 4 [6] forked
stress: dbug: [1] using backoff sleep of 9000us
stress: dbug: [1] --> hogcpu worker 3 [7] forked
stress: dbug: [1] using backoff sleep of 6000us
stress: dbug: [1] --> hogcpu worker 2 [8] forked
stress: dbug: [1] using backoff sleep of 3000us
stress: dbug: [1] --> hogcpu worker 1 [9] forked

因爲默認情況下,容器的 CPU share 爲 1024,所以這兩個容器的 CPU 使用率應該大致爲 2:1,下面是啓動第二個容器之後的監控截圖:

兩個容器分別啓動了四個 stress 進程,第一個容器 stress 進程 CPU 使用率都在 54% 左右,第二個容器 stress 進程 CPU 使用率在 25% 左右,比例關係大致爲 2:1,符合之前的預期。

限制容器能使用的 CPU 核數

上面講述的 -c --cpu-shares 參數只能限制容器使用 CPU 的比例,或者說優先級,無法確定地限制容器使用 CPU 的具體核數;從 1.13 版本之後,docker 提供了 --cpus 參數可以限定容器能使用的 CPU 核數。這個功能可以讓我們更精確地設置容器 CPU 使用量,是一種更容易理解也因此更常用的手段。

--cpus 後面跟着一個浮點數,代表容器最多使用的核數,可以精確到小數點二位,也就是說容器最小可以使用 0.01 核 CPU。比如,我們可以限制容器只能使用 1.5 核數 CPU:

➜  ~ docker run --rm -it --cpus 1.5 stress --cpu 3
stress: info: [1] dispatching hogs: 3 cpu, 0 io, 0 vm, 0 hdd
stress: dbug: [1] using backoff sleep of 9000us
stress: dbug: [1] --> hogcpu worker 3 [7] forked
stress: dbug: [1] using backoff sleep of 6000us
stress: dbug: [1] --> hogcpu worker 2 [8] forked
stress: dbug: [1] using backoff sleep of 3000us
stress: dbug: [1] --> hogcpu worker 1 [9] forked

在容器裏啓動三個 stress 來跑 CPU 壓力,如果不加限制,這個容器會導致 CPU 的使用率爲 300% 左右(也就是說會佔用三個核的計算能力)。實際的監控如下圖:

可以看到,每個 stress 進程 CPU 使用率大約在 50%,總共的使用率爲 150%,符合 1.5 核的設置。

如果設置的 --cpus 值大於主機的 CPU 核數,docker 會直接報錯:

➜  ~ docker run --rm -it --cpus 8 stress --cpu 3
docker: Error response from daemon: Range of CPUs is from 0.01 to 4.00, as there are only 4 CPUs available.
See 'docker run --help'.

如果多個容器都設置了 --cpus ,並且它們之和超過主機的 CPU 核數,並不會導致容器失敗或者退出,這些容器之間會競爭使用 CPU,具體分配的 CPU 數量取決於主機運行情況和容器的 CPU share 值。也就是說 --cpus 只能保證在 CPU 資源充足的情況下容器最多能使用的 CPU 數,docker 並不能保證在任何情況下容器都能使用這麼多的 CPU(因爲這根本是不可能的)。

限制容器運行在某些 CPU 核 現在的筆記本和服務器都會有多個 CPU,docker 也允許調度的時候限定容器運行在哪個 CPU 上。比如,我的主機上有 4 個核,可以通過 --cpuset 參數讓容器只運行在前兩個核上:

➜  ~ docker run --rm -it --cpuset-cpus=0,1 stress --cpu 2
stress: info: [1] dispatching hogs: 2 cpu, 0 io, 0 vm, 0 hdd
stress: dbug: [1] using backoff sleep of 6000us
stress: dbug: [1] --> hogcpu worker 2 [7] forked
stress: dbug: [1] using backoff sleep of 3000us
stress: dbug: [1] --> hogcpu worker 1 [8] forked

這樣,監控中可以看到只有前面兩個核 CPU 達到了 100% 使用率。

--cpuset-cpus 參數可以和 -c --cpu-shares 一起使用,限制容器只能運行在某些 CPU 核上,並且配置了使用率。

限制容器運行在哪些核上並不是一個很好的做法,因爲它需要實現知道主機上有多少 CPU 核,而且非常不靈活。除非有特別的需求,一般並不推薦在生產中這樣使用。

CPU 信息的 cgroup 文件

所有和容器 CPU share 有關的配置都在 /sys/fs/cgroup/cpu/docker/<docker_id>/ 目錄下面,其中 cpu.shares 保存了 CPU share 的值(其他文件的意義可以查看 cgroups 的官方文檔):

➜  ~ ls /sys/fs/cgroup/cpu/docker/d93c9a660f4a13789d995d56024f160e2267f2dc26ce676daa66ea6435473f6f/   
cgroup.clone_children  cpuacct.stat   cpuacct.usage_all     cpuacct.usage_percpu_sys   cpuacct.usage_sys   cpu.cfs_period_us  cpu.shares  notify_on_release
cgroup.procs           cpuacct.usage  cpuacct.usage_percpu  cpuacct.usage_percpu_user  cpuacct.usage_user  cpu.cfs_quota_us   cpu.stat    tasks
➜  ~ cat /sys/fs/cgroup/cpu/docker/d93c9a660f4a13789d995d56024f160e2267f2dc26ce676daa66ea6435473f6f/cpu.shares 
1024

和 cpuset(限制 CPU 核)有關的文件在 /sys/fs/cgroup/cpuset/docker/<docker_id> 目錄下,其中 cpuset.cpus 保存了當前容器能使用的 CPU 核:

➜  ~ ls /sys/fs/cgroup/cpuset/docker/d93c9a660f4a13789d995d56024f160e2267f2dc26ce676daa66ea6435473f6f/
cgroup.clone_children  cpuset.cpus            cpuset.mem_exclusive   cpuset.memory_pressure     cpuset.mems                      notify_on_release
cgroup.procs           cpuset.effective_cpus  cpuset.mem_hardwall    cpuset.memory_spread_page  cpuset.sched_load_balance        tasks
cpuset.cpu_exclusive   cpuset.effective_mems  cpuset.memory_migrate  cpuset.memory_spread_slab  cpuset.sched_relax_domain_level
➜  ~ cat /sys/fs/cgroup/cpuset/docker/d93c9a660f4a13789d995d56024f160e2267f2dc26ce676daa66ea6435473f6f/cpuset.cpus
0-1

--cpus 限制 CPU 核數並不像上面兩個參數一樣有對應的文件對應,它是由 cpu.cfs_period_uscpu.cfs_quota_us 兩個文件控制的。如果容器的 --cpus 設置爲 3,其對應的這兩個文件值爲:

➜  ~ cat /sys/fs/cgroup/cpu/docker/233a38cc641f2e4a1bec3434d88744517a2214aff9d8297e908fa13b9aa12e02/cpu.cfs_period_us 
100000
➜  ~ cat /sys/fs/cgroup/cpu/docker/233a38cc641f2e4a1bec3434d88744517a2214aff9d8297e908fa13b9aa12e02/cpu.cfs_quota_us 
300000

其實在 1.12 以及之前的版本,都是通過 --cpu-period--cpu-quota 這兩個參數控制容器能使用的 CPU 核數的。前者表示 CPU 的週期數,默認是 100000,單位是微秒,也就是 1s,一般不需要修改;後者表示容器的在上述 CPU 週期裏能使用的 quota,真正能使用的 CPU 核數就是 cpu-quota / cpu-period,因此對於 3 核的容器,對應的 cpu-quota 值爲 300000。

  1. 內存資源

默認情況下,docker 並沒有對容器內存進行限制,也就是說容器可以使用主機提供的所有內存。這當然是非常危險的事情,如果某個容器運行了惡意的內存消耗軟件,或者代碼有內存泄露,很可能會導致主機內存耗盡,因此導致服務不可用。對於這種情況,docker 會設置 docker daemonOOM(out of memory) 值,使其在內存不足的時候被殺死的優先級降低。另外,就是你可以爲每個容器設置內存使用的上限,一旦超過這個上限,容器會被殺死,而不是耗盡主機的內存。

限制內存上限雖然能保護主機,但是也可能會傷害到容器裏的服務。如果爲服務設置的內存上限太小,會導致服務還在正常工作的時候就被 OOM 殺死;如果設置的過大,會因爲調度器算法浪費內存。因此,合理的做法包括:

爲應用做內存壓力測試,理解正常業務需求下使用的內存情況,然後才能進入生產環境使用

一定要限制容器的內存使用上限

儘量保證主機的資源充足,一旦通過監控發現資源不足,就進行擴容或者對容器進行遷移 如果可以(內存資源充足的情況),儘量不要使用 swap,swap 的使用會導致內存計算複雜,對調度器非常不友好

docker 限制容器內存使用量

在 docker 啓動參數中,和內存限制有關的包括(參數的值一般是內存大小,也就是一個正數,後面跟着內存單位 b、k、m、g,分別對應 bytes、KB、MB、和 GB):

如果 --memory-swap 的值大於 --memory,那麼容器能使用的總內存(內存 + swap)爲 --memory-swap 的值,能使用的 swap 值爲 --memory-swap 減去 --memory 的值 如果 --memory-swap 爲 0,或者和 --memory 的值相同,那麼容器能使用兩倍於內存的 swap 大小,如果 --memory 對應的值是 200M,那麼容器可以使用 400M swap 如果 --memory-swap 的值爲 -1,那麼不限制 swap 的使用,也就是說主機有多少 swap,容器都可以使用 如果限制容器的內存使用爲 64M,在申請 64M 資源的情況下,容器運行正常(如果主機上內存非常緊張,並不一定能保證這一點):

➜  docker run --rm -it -m 64m stress --vm 1 --vm-bytes 64M --vm-hang 0
WARNING: Your kernel does not support swap limit capabilities or the cgroup is not mounted. Memory limited without swap.
stress: info: [1] dispatching hogs: 0 cpu, 0 io, 1 vm, 0 hdd
stress: dbug: [1] using backoff sleep of 3000us
stress: dbug: [1] --> hogvm worker 1 [7] forked
stress: dbug: [7] allocating 67108864 bytes ...
stress: dbug: [7] touching bytes in strides of 4096 bytes ...
stress: dbug: [7] sleeping forever with allocated memory
.....

而如果申請 100M 內存,會發現容器裏的進程被 kill 掉了(worker 7 got signal 9,signal 9 就是 kill 信號)

➜  docker run --rm -it -m 64m stress --vm 1 --vm-bytes 100M --vm-hang 0
WARNING: Your kernel does not support swap limit capabilities or the cgroup is not mounted. Memory limited without swap.
stress: info: [1] dispatching hogs: 0 cpu, 0 io, 1 vm, 0 hdd
stress: dbug: [1] using backoff sleep of 3000us
stress: dbug: [1] --> hogvm worker 1 [7] forked
stress: dbug: [7] allocating 104857600 bytes ...
stress: dbug: [7] touching bytes in strides of 4096 bytes ...
stress: FAIL: [1] (415) <-- worker 7 got signal 9
stress: WARN: [1] (417) now reaping child worker processes
stress: FAIL: [1] (421) kill error: No such process
stress: FAIL: [1] (451) failed run completed in 0s

關於 swapkernel memory 的限制就不在這裏過多解釋了,感興趣的可以查看官方的文檔。

內存信息的 cgroups 文件

對於 docker 來說,它的內存限制也是存放在 cgroups 文件系統的。對於某個容器,你可以在 sys/fs/cgroup/memory/docker/<container_id> 目錄下看到容器內存相關的文件:

➜  ls /sys/fs/cgroup/memory/docker/b067fa0c58dcdd4fa856177fac0112655b605fcc9a0fe07e36950f0086f62f46 
cgroup.clone_children  memory.kmem.failcnt             memory.kmem.tcp.limit_in_bytes      memory.max_usage_in_bytes        memory.soft_limit_in_bytes  notify_on_release
cgroup.event_control   memory.kmem.limit_in_bytes      memory.kmem.tcp.max_usage_in_bytes  memory.move_charge_at_immigrate  memory.stat                 tasks
cgroup.procs           memory.kmem.max_usage_in_bytes  memory.kmem.tcp.usage_in_bytes      memory.numa_stat                 memory.swappiness
memory.failcnt         memory.kmem.slabinfo            memory.kmem.usage_in_bytes          memory.oom_control               memory.usage_in_bytes
memory.force_empty     memory.kmem.tcp.failcnt         memory.limit_in_bytes               memory.pressure_level            memory.use_hierarchy

而上面的內存限制對應的文件是 memory.limit_in_bytes

➜  cat /sys/fs/cgroup/memory/docker/b067fa0c58dcdd4fa856177fac0112655b605fcc9a0fe07e36950f0086f62f46/memory.limit_in_bytes
67108864
  1. IO 資源(磁盤)

對於磁盤來說,考量的參數是容量和讀寫速度,因此對容器的磁盤限制也應該從這兩個維度出發。目前 docker 支持對磁盤的讀寫速度進行限制,但是並沒有方法能限制容器能使用的磁盤容量(一旦磁盤 mount 到容器裏,容器就能夠使用磁盤的所有容量)。

➜  ~ docker run -it --rm ubuntu:16.04 bash

root@5229f756523c:/# time $(dd if=/dev/zero of=/tmp/test.data bs=10M count=100 && sync)
100+0 records in
100+0 records out
1048576000 bytes (1.0 GB) copied, 3.82859 s, 274 MB/s

real    0m4.124s
user    0m0.000s
sys    0m1.812s

限制磁盤的權重

通過 --blkio-weight 參數可以設置 block 的權重,這個權重和 --cpu-shares 類似,它是一個相對值,取值範圍是 10-1000,當多個 block 去寫磁盤的時候,其讀寫速度和權重成反比。

不過在我的環境中,--blkio-weight 參數雖然設置了對應的 cgroups 值,但是並沒有作用,不同 weight 容器的讀寫速度還是一樣的。github 上有一個對應的 issue,但是沒有詳細的解答。

--blkio-weight-device 可以設置某個設備的權重值,測試下來雖然兩個容器同時讀的速度不同,但是並沒有按照對應的比例來限制。

限制磁盤的讀寫速率

除了權重之外,docker 還允許你直接限制磁盤的讀寫速率,對應的參數有:

比如可以把設備的讀速率限制在 1mb:

$ docker run -it --device /dev/sda:/dev/sda --device-read-bps /dev/sda:1mb ubuntu:16.04 bash
root@6c048edef769:/# cat /sys/fs/cgroup/blkio/blkio.throttle.read_bps_device 
8:0 1048576
root@6c048edef769:/# dd iflag=direct,nonblock if=/dev/sda of=/dev/null bs=5M count=10
10+0 records in
10+0 records out
52428800 bytes (52 MB) copied, 50.0154 s, 1.0 MB/s

從磁盤中讀取 50m 花費了 50s 左右,說明磁盤速率限制起了作用。

另外兩個參數可以限制磁盤讀寫頻率(每秒能執行多少次讀寫操作):

比如,我們可以讓磁盤每秒最多讀 100 次:

➜  ~ docker run -it --device /dev/sda:/dev/sda --device-read-iops /dev/sda:100 ubuntu:16.04 bash
root@2e3026e9ccd2:/# dd iflag=direct,nonblock if=/dev/sda of=/dev/null bs=1k count=1000
1000+0 records in
1000+0 records out
1024000 bytes (1.0 MB) copied, 9.9159 s, 103 kB/s

從測試中可以看出,容器設置了讀操作的 iops 爲 100,在容器內部從 block 中讀取 1m 數據(每次 1k,一共要讀 1000 次),共計耗時約 10s,換算起來就是 100 iops/s,符合預期結果。

寫操作 bps 和 iops 與讀類似,這裏就不再重複了,感興趣的可以自己實驗。

磁盤信息的 cgroups 文件

容器中磁盤限制的 cgroups 文件位於 /sys/fs/cgroup/blkio/docker/<docker_id> 目錄:

➜  ~ ls /sys/fs/cgroup/blkio/docker/1402c1682cba743b4d80f638da3d4272b2ebdb6dc6c2111acfe9c7f7aeb72917/                               
blkio.io_merged                   blkio.io_serviced                blkio.leaf_weight                blkio.throttle.io_serviced        blkio.time_recursive   tasks
blkio.io_merged_recursive         blkio.io_serviced_recursive      blkio.leaf_weight_device         blkio.throttle.read_bps_device    blkio.weight
blkio.io_queued                   blkio.io_service_time            blkio.reset_stats                blkio.throttle.read_iops_device   blkio.weight_device
blkio.io_queued_recursive         blkio.io_service_time_recursive  blkio.sectors                    blkio.throttle.write_bps_device   cgroup.clone_children
blkio.io_service_bytes            blkio.io_wait_time               blkio.sectors_recursive          blkio.throttle.write_iops_device  cgroup.procs
blkio.io_service_bytes_recursive  blkio.io_wait_time_recursive     blkio.throttle.io_service_bytes  blkio.time                        notify_on_release

其中 blkio.throttle.read_iops_device 對應了設備的讀 IOPS,前面一列是設備的編號,可以通過 cat /proc/partitions 查看設備和分區的設備號;後面是 IOPS 上限值:

➜  ~ cat /sys/fs/cgroup/blkio/docker/1402c1682cba743b4d80f638da3d4272b2ebdb6dc6c2111acfe9c7f7aeb72917/blkio.throttle.read_iops_device 
8:0 100
blkio.throttle.read_bps_device 對應了設備的讀速率,格式和 IOPS 類似,只是第二列的值爲 bps:

➜  ~ cat /sys/fs/cgroup/blkio/docker/9de94493f1ab4437d9c2c42fab818f12c7e82dddc576f356c555a2db7bc61e21/blkio.throttle.read_bps_device 
8:0 1048576

總結

從上面的實驗可以看出來,CPU 和內存的資源限制已經是比較成熟和易用,能夠滿足大部分用戶的需求。磁盤限制也是不錯的,雖然現在無法動態地限制容量,但是限制磁盤讀寫速度也能應對很多場景。

至於網絡,docker 現在並沒有給出網絡限制的方案,也不會在可見的未來做這件事情,因爲目前網絡是通過插件來實現的,和容器本身的功能相對獨立,不是很容易實現,擴展性也很差。docker 社區已經有很多呼聲,也有 issue 是關於網絡流量限制的。

資源限制一方面可以讓我們爲容器(應用)設置合理的 CPU、內存等資源,方便管理;另外一方面也能有效地預防惡意的攻擊和異常,對容器來說是非常重要的功能。如果你需要在生產環境使用容器,請務必要花時間去做這件事情。

轉自:cizixs

cizixs.com/2017/08/04/docker-resources-limit

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/C4OPhsQlW9HHwxyHSuzGQQ