使用 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_us 和 cpu.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 。
2. 內存資源
默認情況下,docker 并沒有對容器內存進行限制,也就是說容器可以使用主機提供的所有內存。這當然是非常危險的事情,如果某個容器運行了惡意的內存消耗軟件,或者代碼有內存泄露,很可能會導致主機內存耗盡,因此導致服務不可用。對于這種情況,docker 會設置 docker daemon 的 OOM(out of memory) 值,使其在內存不足的時候被殺死的優先級降低。另外,就是你可以為每個容器設置內存使用的上限,一旦超過這個上限,容器會被殺死,而不是耗盡主機的內存。
限制內存上限雖然能保護主機,但是也可能會傷害到容器里的服務。如果為服務設置的內存上限太小,會導致服務還在正常工作的時候就被 OOM 殺死;如果設置的過大,會因為調度器算法浪費內存。因此,合理的做法包括:
- 為應用做內存壓力測試,理解正常業務需求下使用的內存情況,然后才能進入生產環境使用
- 一定要限制容器的內存使用上限
- 盡量保證主機的資源充足,一旦通過監控發現資源不足,就進行擴容或者對容器進行遷移
- 如果可以(內存資源充足的情況),盡量不要使用 swap,swap 的使用會導致內存計算復雜,對調度器非常不友好
docker 限制容器內存使用量
在 docker 啟動參數中,和內存限制有關的包括(參數的值一般是內存大小,也就是一個正數,后面跟著內存單位 b 、 k 、 m 、 g ,分別對應 bytes、KB、MB、和 GB):
- -m --memory :容器能使用的最大內存大小,最小值為 4m
- --memory-swap :容器能夠使用的 swap 大小
- --memory-swappiness :默認情況下,主機可以把容器使用的匿名頁(anonymous page)swap 出來,你可以設置一個 0-100 之間的值,代表允許 swap 出來的比例
- --memory-reservation :設置一個內存使用的 soft limit,如果 docker 發現主機內存不足,會執行 OOM 操作。這個值必須小于 --memory 設置的值
- --kernel-memory :容器能夠使用的 kernel memory 大小,最小值為 4m。
- --oom-kill-disable :是否運行 OOM 的時候殺死容器。只有設置了 -m ,才可以把這個選項設置為 false,否則容器會耗盡主機內存,而且導致主機應用被殺死
關于 --memory-swap 的設置必須解釋一下, --memory-swap 必須在 --memory 也配置的情況下才能有用。
- 如果 --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
關于 swap 和 kernel 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
3. 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 還允許你直接限制磁盤的讀寫速率,對應的參數有:
- --device-read-bps :磁盤每秒最多可以讀多少比特(bytes)
- --device-write-bps :磁盤每秒最多可以寫多少比特(bytes)
上面兩個參數的值都是磁盤以及對應的速率,格式為 <device-path>:<limit>[unit] , device-path 表示磁盤所在的位置,限制 limit 為正整數,單位可以是 kb 、 mb 和 gb 。
比如可以把設備的度速率限制在 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 左右,說明磁盤速率限制起了作用。
另外兩個參數可以限制磁盤讀寫頻率(每秒能執行多少次讀寫操作):
- --device-read-iops :磁盤每秒最多可以執行多少 IO 讀操作
- --device-write-iops :磁盤每秒最多可以執行多少 IO 寫操作
上面兩個參數的值都是磁盤以及對應的 IO 上限,格式為 <device-path>:<limit> ,limit 為正整數,表示磁盤 IO 上限數。
比如,我們可以讓磁盤每秒最多讀 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 是關于網絡流量限制的: issue 26767 、 issue 37 、 issue 4763 。
資源限制一方面可以讓我們為容器(應用)設置合理的 CPU、內存等資源,方便管理;另外一方面也能有效地預防惡意的攻擊和異常,對容器來說是非常重要的功能。如果你需要在生產環境使用容器,請務必要花時間去做這件事情。
參考資料
- docker docs: Limit a container’s resources
- Resource management in Docker
- Docker資源管理探秘:Docker背后的內核Cgroups機制
來自:http://cizixs.com/2017/08/04/docker-resources-limit