背景
在使用 docker 运行容器时,默认的情况下,docker 没有对容器进行硬件资源的限制,当一台主机上运行几百个容器,这些容器虽然互相隔离,但是底层却使用着相同的 CPU、内存和磁盘资源。如果不对容器使用的资源进行限制,那么容器之间会互相影响,小的来说会导致容器资源使用不公平;大的来说,可能会导致主机和集群资源耗尽,服务完全不可用。
docker 作为容器的管理者,自然提供了控制容器资源的功能。正如使用内核的 namespace 来做容器之间的隔离,docker 也是通过内核的 cgroups 来做容器的资源限制;包括CPU、内存、磁盘三大方面,基本覆盖了常见的资源配额和使用量控制。
Docker 内存控制 OOME 在 linxu 系统上,如果内核探测到当前宿主机已经没有可用内存使用,那么会抛出一个 OOME(Out Of Memory Exception:内存异常 ),并且会开启 killing 去杀掉一些进程。
一旦发生 OOME,任何进程都有可能被杀死,包括docker daemon 在内,为此,docker特地调整了docker daemon 的 OOM_Odj 优先级,以免他被杀掉,但容器的优先级并未被调整。经过系统内部复制的计算后,每个系统进程都会有一个 OOM_Score 得分,OOM_Odj 越高,得分越高,(在 docker run 的时候可以调整 OOM_Odj)得分最高的优先被 kill 掉,当然,也可以指定一些特定的重要的容器禁止被 OMM 杀掉,在启动容器时使用 –oom-kill-disable=true 指定。
cgroup 简介
cgroup 是 Control Groups 的缩写,是 Linux 内核提供的一种可以限制、记录、隔离进程组所使用的物理资源(如 cpu、memory、磁盘 IO 等等) 的机制,被 LXC、docker 等很多项目用于实现进程资源控制。cgroup 将任意进程进行分组化管理的 Linux 内核功能。cgroup 本身是提供将进程进行分组化管理的功能和接口的基础结构,I/O 或内存的分配控制等具体的资源管理功能是通过这个功能来实现的。这些具体的资源管理功能称为 cgroup 子系统,有以下几大子系统实现:
- blkio:设置限制每个块设备的输入输出控制。例如:磁盘,光盘以及 usb 等等。
- cpu:使用调度程序为 cgroup 任务提供 cpu 的访问。
- cpuacct:产生 cgroup 任务的 cpu 资源报告。
- cpuset:如果是多核心的 cpu,这个子系统会为 cgroup 任务分配单独的 cpu 和内存。
- devices:允许或拒绝 cgroup 任务对设备的访问。
- freezer:暂停和恢复 cgroup 任务。
- memory:设置每个 cgroup 的内存限制以及产生内存资源报告。
- net_cls:标记每个网络包以供 cgroup 方便使用。
- ns:命名空间子系统。
- perf_event:增加了对每 group 的监测跟踪的能力,即可以监测属于某个特定的 group 的所有线程以及运行在特定CPU上的线程。
目前 docker 只是用了其中一部分子系统,实现对资源配额和使用的控制。
可以使用 stress 工具来测试CPU和内存。使用下面的 Dockerfile 来创建一个基于 Ubuntu 的 stress 工具镜像。
FROM ubuntu:14.04
RUN apt-get update &&apt-get install stress
资源监控的关键目录:cat 读出
已使用内存:
/sys/fs/cgroup/memory/docker/应用 ID/memory.usage_in_bytes
分配的总内存:
/sys/fs/cgroup/memory/docker/应用 ID/memory.limit_in_bytes
已使用的 cpu:单位纳秒
/sys/fs/cgroup/cpuacct/docker/应用 ID/cpuacct.usage
系统当前 cpu:
<code class="has-numbering hljs shell"><span class="hljs-meta">$</span><span class="bash"> cat /proc/<span class="hljs-built_in">stat</span> | grep <span class="hljs-string">'cpu '</span>(周期/时间片/jiffies)</span> <span class="hljs-meta">#</span><span class="bash">得到的数字相加/HZ(cat /boot/config-`uname -r` | grep <span class="hljs-string">'^CONFIG_HZ='</span></span> ubuntu 14.04 为 250)就是系统时间(秒) <span class="hljs-meta">#</span><span class="bash">再乘以 10*9 就是系统时间(纳秒)</span></code>
例子
<code class="has-numbering hljs perl">[~]$ cat /proc/<span class="hljs-keyword">stat</span> cpu <span class="hljs-number">432661</span> <span class="hljs-number">13295</span> <span class="hljs-number">86656</span> <span class="hljs-number">422145968</span> <span class="hljs-number">171474</span> <span class="hljs-number">233</span> <span class="hljs-number">5346</span> cpu<span class="hljs-number">0</span> <span class="hljs-number">123075</span> <span class="hljs-number">2462</span> <span class="hljs-number">23494</span> <span class="hljs-number">105543694</span> <span class="hljs-number">16586</span> <span class="hljs-number">0</span> <span class="hljs-number">4615</span> cpu1 <span class="hljs-number">111917</span> <span class="hljs-number">4124</span> <span class="hljs-number">23858</span> <span class="hljs-number">105503820</span> <span class="hljs-number">69697</span> <span class="hljs-number">123</span> <span class="hljs-number">371</span> cpu2 <span class="hljs-number">103164</span> <span class="hljs-number">3554</span> <span class="hljs-number">21530</span> <span class="hljs-number">105521167</span> <span class="hljs-number">64032</span> <span class="hljs-number">106</span> <span class="hljs-number">334</span> cpu3 <span class="hljs-number">94504</span> <span class="hljs-number">3153</span> <span class="hljs-number">17772</span> <span class="hljs-number">105577285</span> <span class="hljs-number">21158</span> <span class="hljs-number">4</span> <span class="hljs-number">24</span> intr <span class="hljs-number">1065711094</span> <span class="hljs-number">1057275779</span> <span class="hljs-number">92</span> <span class="hljs-number">0</span> <span class="hljs-number">6</span> <span class="hljs-number">6</span> <span class="hljs-number">0</span> <span class="hljs-number">4</span> <span class="hljs-number">0</span> <span class="hljs-number">3527</span> <span class="hljs-number">0</span> <span class="hljs-number">0</span> <span class="hljs-number">0</span> <span class="hljs-number">70</span> <span class="hljs-number">0</span> <span class="hljs-number">20</span> <span class="hljs-number">0</span> <span class="hljs-number">0</span> <span class="hljs-number">0</span> <span class="hljs-number">0</span> <span class="hljs-number">0</span> <span class="hljs-number">0</span> <span class="hljs-number">0</span> <span class="hljs-number">0</span> <span class="hljs-number">0</span> <span class="hljs-number">0</span> <span class="hljs-number">0</span> <span class="hljs-number">0</span> <span class="hljs-number">0</span> <span class="hljs-number">0</span> <span class="hljs-number">0</span> <span class="hljs-number">0</span> <span class="hljs-number">0</span> <span class="hljs-number">0</span> <span class="hljs-number">0</span> <span class="hljs-number">0</span> <span class="hljs-number">0</span> <span class="hljs-number">0</span> <span class="hljs-number">0</span> <span class="hljs-number">0</span> <span class="hljs-number">0</span> <span class="hljs-number">0</span> <span class="hljs-number">0</span> <span class="hljs-number">0</span> <span class="hljs-number">0</span> <span class="hljs-number">0</span> <span class="hljs-number">0</span> <span class="hljs-number">0</span> <span class="hljs-number">0</span> <span class="hljs-number">0</span> <span class="hljs-number">0</span> <span class="hljs-number">0</span> <span class="hljs-number">0</span> <span class="hljs-number">0</span> <span class="hljs-number">0</span> <span class="hljs-number">0</span> <span class="hljs-number">0</span> <span class="hljs-number">0</span> <span class="hljs-number">0</span> <span class="hljs-number">0</span> <span class="hljs-number">0</span> <span class="hljs-number">0</span> <span class="hljs-number">0</span> <span class="hljs-number">0</span> <span class="hljs-number">0</span> <span class="hljs-number">0</span> <span class="hljs-number">0</span> <span class="hljs-number">0</span> ctxt <span class="hljs-number">19067887</span> btime <span class="hljs-number">1139187531</span> processes <span class="hljs-number">270014</span> procs_running <span class="hljs-number">1</span> procs_blocked <span class="hljs-number">0</span> 输出解释 <a href="https://www.lushuiwan.com/tag/cpu" title="查看更多关于 CPU 的文章" target="_blank">CPU</a> 以及<a href="https://www.lushuiwan.com/tag/cpu" title="查看更多关于 CPU 的文章" target="_blank">CPU</a><span class="hljs-number">0</span>、<a href="https://www.lushuiwan.com/tag/cpu" title="查看更多关于 CPU 的文章" target="_blank">CPU</a>1、CPU2、CPU3 每行的每个参数意思(以第一行为例)为: 参数 解释 user (<span class="hljs-number">432661</span>) 从系统启动开始累计到当前时刻,用户态的 CPU 时间(单位:jiffies) ,不包含 nice 值为负进程。 nice (<span class="hljs-number">13295</span>) 从系统启动开始累计到当前时刻,nice 值为负的进程所占用的 CPU 时间(单位:jiffies) <span class="hljs-keyword">system</span> (<span class="hljs-number">86656</span>) 从系统启动开始累计到当前时刻,核心时间(单位:jiffies) idle (<span class="hljs-number">422145968</span>) 从系统启动开始累计到当前时刻,除硬盘 IO 等待时间以外其它等待时间(单位:jiffies) iowait (<span class="hljs-number">171474</span>) 从系统启动开始累计到当前时刻,硬盘 IO 等待时间(单位:jiffies) , ir<span class="hljs-string">q (233)</span> 从系统启动开始累计到当前时刻,硬中断时间(单位:jiffies) softir<span class="hljs-string">q (5346)</span> 从系统启动开始累计到当前时刻,软中断时间(单位:jiffies) </code>
cpu 使用率:
(已使用 2-已使用 1)/(系统当前 2-系统当前 1)*100%
内存限制
Docker 提供的内存限制功能有以下几点:
- 容器能使用的内存和交换分区大小。
- 容器的核心内存大小。
- 容器虚拟内存的交换行为。
- 容器内存的软性限制。
- 是否杀死占用过多内存的容器。
- 容器被杀死的优先级
一般情况下,达到内存限制的容器过段时间后就会被系统杀死。
内存限制相关的参数
执行docker run
命令时能使用的和内存限制相关的所有选项如下。
选项 | 描述 |
---|---|
-m ,--memory |
内存限制,格式是数字加单位,单位可以为 b,k,m,g。最小为 4M |
--memory-swap |
内存+交换分区大小总限制。格式同上。必须必-m 设置的大 |
--memory-reservation |
内存的软性限制。格式同上 |
--oom-kill-disable |
是否阻止 OOM killer 杀死容器,默认没设置 |
--oom-score-adj |
容器被 OOM killer 杀死的优先级,范围是[-1000, 1000],默认为 0 |
--memory-swappiness |
用于设置容器的虚拟内存控制行为。值为 0~100 之间的整数 |
--kernel-memory |
核心内存限制。格式同上,最小为 4M |
用户内存限制
用户内存限制就是对容器能使用的内存和交换分区的大小作出限制。使用时要遵循两条直观的规则:-m,--memory
选项的参数最小为 4 M。--memory-swap
不是交换分区,而是内存加交换分区的总大小,所以--memory-swap
必须比-m,--memory
大。在这两条规则下,一般有四种设置方式。
你可能在进行内存限制的实验时发现
docker run
命令报错:WARNING: Your kernel does not support swap limit capabilities, memory limited without swap.这是因为宿主机内核的相关功能没有打开。按照下面的设置就行。
step 1:编辑
/etc/default/grub
文件,将GRUB_CMDLINE_LINUX
一行改为GRUB_CMDLINE_LINUX="cgroup_enable=memory swapaccount=1"
step 2:更新 GRUB,即执行
$ sudo update-grub
step 3: 重启系统。
1. 不设置
如果不设置-m,--memory
和--memory-swap
,容器默认可以用完宿舍机的所有内存和 swap 分区。不过注意,如果容器占用宿主机的所有内存和 swap 分区超过一段时间后,会被宿主机系统杀死(如果没有设置--00m-kill-disable=true
的话)。
2. 设置-m,--memory
,不设置--memory-swap
给-m
或--memory
设置一个不小于 4M 的值,假设为 a,不设置--memory-swap
,或将--memory-swap
设置为 0。这种情况下,容器能使用的内存大小为 a,能使用的交换分区大小也为 a。因为 Docker 默认容器交换分区的大小和内存相同。
如果在容器中运行一个一直不停申请内存的程序,你会观察到该程序最终能占用的内存大小为 2a。
比如$ docker run -m 1G ubuntu:16.04
,该容器能使用的内存大小为 1G,能使用的 swap 分区大小也为 1G。容器内的进程能申请到的总内存大小为 2G。
3. 设置-m,--memory=a
,--memory-swap=b
,且 b > a
给-m
设置一个参数 a,给--memory-swap
设置一个参数 b。a 时容器能使用的内存大小,b 是容器能使用的 内存大小 + swap 分区大小。所以 b 必须大于 a。b -a 即为容器能使用的 swap 分区大小。
比如$ docker run -m 1G --memory-swap 3G ubuntu:16.04
,该容器能使用的内存大小为 1G,能使用的 swap 分区大小为 2G。容器内的进程能申请到的总内存大小为 3G。
4. 设置-m,--memory=a
,--memory-swap=-1
给-m
参数设置一个正常值,而给--memory-swap
设置成 -1。这种情况表示限制容器能使用的内存大小为 a,而不限制容器能使用的 swap 分区大小。
这时候,容器内进程能申请到的内存大小为 a + 宿主机的 swap 大小。
Memory reservation
这种 memory reservation 机制不知道怎么翻译比较形象。Memory reservation 是一种软性限制,用于节制容器内存使用。给--memory-reservation
设置一个比-m
小的值后,虽然容器最多可以使用-m
使用的内存大小,但在宿主机内存资源紧张时,在系统的下次内存回收时,系统会回收容器的部分内存页,强迫容器的内存占用回到--memory-reservation
设置的值大小。
没有设置时(默认情况下)--memory-reservation
的值和-m
的限定的值相同。将它设置为 0 会设置的比-m
的参数大 等同于没有设置。
Memory reservation 是一种软性机制,它不保证任何时刻容器使用的内存不会超过--memory-reservation
限定的值,它只是确保容器不会长时间占用超过--memory-reservation
限制的内存大小。
例如:
<code class="language-shell hljs applescript has-numbering"><span class="hljs-meta">$</span><span class="bash"> docker </span><span class="hljs-command"><span class="bash">run -</span><span class="hljs-keyword"><span class="bash">it -m </span><span class="hljs-number"><span class="bash">500M </span><span class="hljs-comment"><span class="bash">--memory-reservation 200M ubuntu:16.04 /bin/bash</span></span></span></span></span></code>
如果容器使用了大于 200M 但小于 500M 内存时,下次系统的内存回收会尝试将容器的内存锁紧到 200M 以下。
例如:
<code class="language-shell hljs applescript has-numbering"><span class="hljs-meta">$</span><span class="bash"> docker </span><span class="hljs-command"><span class="bash">run -</span><span class="hljs-keyword"><span class="bash">it </span><span class="hljs-comment"><span class="bash">--memory-reservation 1G ubuntu:16.04 /bin/bash</span></span></span></span></code>
容器可以使用尽可能多的内存。--memory-reservation
确保容器不会长时间占用太多内存。
OOM killer
默认情况下,在出现 out-of-memory(OOM) 错误时,系统会杀死容器内的进程来获取更多空闲内存。这个杀死进程来节省内存的进程,我们姑且叫它 OOM killer。我们可以通过设置--oom-kill-disable
选项来禁止 OOM killer 杀死容器内进程。但请确保只有在使用了-m/--memory
选项时才使用--oom-kill-disable
禁用 OOM killer。如果没有设置-m
选项,却禁用了 OOM-killer,可能会造成出现 out-of-memory 错误时,系统通过杀死宿主机进程或获取更改内存。
下面的例子限制了容器的内存为 100M 并禁止了 OOM killer:
<code class="language-shell hljs lasso has-numbering"><span class="hljs-meta">$</span><span class="bash"> docker run </span><span class="hljs-attribute"><span class="bash">-it </span><span class="bash">-m </span><span class="hljs-number"><span class="bash">100M </span><span class="hljs-subst"><span class="bash">--oom</span><span class="bash">-kill</span><span class="bash">-disable ubuntu:</span><span class="bash">16.04 /bin/bash</span></span></span></span></code>
是正确的使用方法。
而下面这个容器没设置内存限制,却禁用了 OOM killer 是非常危险的:
<code class="language-shell hljs applescript has-numbering"><span class="hljs-meta">$</span><span class="bash"> docker </span><span class="hljs-command"><span class="bash">run -</span><span class="hljs-keyword"><span class="bash">it </span><span class="hljs-comment"><span class="bash">--oom-kill-disable ubuntu:16.04 /bin/bash</span></span></span></span></code>
容器没用内存限制,可能或导致系统无内存可用,并尝试时杀死系统进程来获取更多可用内存。
一般一个容器只有一个进程,这个唯一进程被杀死,容器也就被杀死了。我们可以通过--oom-score-adj
选项来设置在系统内存不够时,容器被杀死的优先级。负值更教不可能被杀死,而正值更有可能被杀死。
核心内存
核心内存和用户内存不同的地方在于核心内存不能被交换出。不能交换出去的特性使得容器可以通过消耗太多内存来堵塞一些系统服务。核心内存包括:
- stack pages(栈页面)
- slab pages
- socket memory pressure
- tcp memory pressure
可以通过设置核心内存限制来约束这些内存。例如,每个进程都要消耗一些栈页面,通过限制核心内存,可以在核心内存使用过多时阻止新进程被创建。
核心内存和用户内存并不是独立的,必须在用户内存限制的上下文中限制核心内存。
假设用户内存的限制值为 U,核心内存的限制值为 K。有三种可能地限制核心内存的方式:
- U != 0,不限制核心内存。这是默认的标准设置方式
- K < U,核心内存时用户内存的子集。这种设置在部署时,每个 cgroup 的内存总量被过度使用。过度使用核心内存限制是绝不推荐的,因为系统还是会用完不能回收的内存。在这种情况下,你可以设置 K,这样 groups 的总数就不会超过总内存了。然后,根据系统服务的质量自有地设置 U。
- K > U,因为核心内存的变化也会导致用户计数器的变化,容器核心内存和用户内存都会触发回收行为。这种配置可以让管理员以一种统一的视图看待内存。对想跟踪核心内存使用情况的用户也是有用的。
例如:
<code class="language-shell hljs applescript has-numbering"><span class="hljs-meta">$</span><span class="bash"> docker </span><span class="hljs-command"><span class="bash">run -</span><span class="hljs-keyword"><span class="bash">it -m </span><span class="hljs-number"><span class="bash">500M </span><span class="hljs-comment"><span class="bash">--kernel-memory 50M ubuntu:16.04 /bin/bash</span></span></span></span></span></code>
容器中的进程最多能使用 500M 内存,在这 500M 中,最多只有 50M 核心内存。
<code class="language-shell hljs applescript has-numbering"><span class="hljs-meta">$</span><span class="bash"> docker </span><span class="hljs-command"><span class="bash">run -</span><span class="hljs-keyword"><span class="bash">it </span><span class="hljs-comment"><span class="bash">--kernel-memory 50M ubuntu:16.04 /bin/bash</span></span></span></span></code>
没用设置用户内存限制,所以容器中的进程可以使用尽可能多的内存,但是最多能使用 50M 核心内存。
Swappiness
默认情况下,容器的内核可以交换出一定比例的匿名页。--memory-swappiness
就是用来设置这个比例的。--memory-swappiness
可以设置为从 0 到 100。0 表示关闭匿名页面交换。100 表示所有的匿名页都可以交换。默认情况下,如果不适用--memory-swappiness
,则该值从父进程继承而来。
例如:
<code class="language-shell hljs applescript has-numbering"><span class="hljs-meta">$</span><span class="bash"> docker </span><span class="hljs-command"><span class="bash">run -</span><span class="hljs-keyword"><span class="bash">it </span><span class="hljs-comment"><span class="bash">--memory-swappiness=0 ubuntu:16.04 /bin/bash</span> </span></span></span></code>
将--memory-swappiness
设置为 0 可以保持容器的工作集,避免交换代理的性能损失。
<code class="language-shell hljs applescript has-numbering"><span class="hljs-meta">$</span><span class="bash"> docker run -tid —name mem1 —memory 128m ubuntu:16.04 /bin/bash</span> <span class="bash">$ cat /sys/fs/cgroup/memory/docker/<容器的完整 ID>/memory.limit_in_bytes</span> <span class="bash">$ cat /sys/fs/cgroup/memory/docker/<容器的完整 ID>/memory.memsw.limit_in_bytes</span></code>
CPU 限制
概述
Docker 的资源限制和隔离完全基于 Linux cgroups。对 CPU 资源的限制方式也和 cgroups 相同。Docker 提供的 CPU 资源限制选项可以在多核系统上限制容器能利用哪些 vCPU。而对容器最多能使用的 CPU 时间有两种限制方式:一是有多个 CPU 密集型的容器竞争 CPU 时,设置各个容器能使用的 CPU 时间相对比例。二是以绝对的方式设置容器在每个调度周期内最多能使用的 CPU 时间。
CPU 限制相关参数
docker run
命令和 CPU 限制相关的所有选项如下:
选项 | 描述 |
---|---|
--cpuset-cpus="" |
允许使用的 CPU 集,值可以为 0-3,0,1 |
-c ,--cpu-shares=0 |
CPU 共享权值(相对权重) |
cpu-period=0 |
限制 CPU CFS 的周期,范围从 100ms~1s,即[1000, 1000000] |
--cpu-quota=0 |
限制 CPU CFS 配额,必须不小于 1ms,即 >= 1000 |
--cpuset-mems="" |
允许在上执行的内存节点(MEMs),只对 NUMA 系统有效 |
其中--cpuset-cpus
用于设置容器可以使用的 vCPU 核。-c
,--cpu-shares
用于设置多个容器竞争 CPU 时,各个容器相对能分配到的 CPU 时间比例。--cpu-period
和--cpu-quata
用于绝对设置容器能使用 CPU 时间。
--cpuset-mems
暂用不上,这里不谈。
CPU 集
我们可以设置容器可以在哪些 CPU 核上运行。
例如:
<code class="language-shell hljs applescript has-numbering"><span class="hljs-meta">$</span><span class="bash"> docker </span><span class="hljs-command"><span class="bash">run -</span><span class="hljs-keyword"><span class="bash">it </span><span class="hljs-comment"><span class="bash">--cpuset-cpus=<span class="hljs-string">"1,3"</span> ubuntu:14.04 /bin/bash</span></span></span></span></code>
表示容器中的进程可以在 cpu 1 和 cpu 3 上执行。
<code class="language-shell hljs applescript has-numbering"><span class="hljs-meta">$</span><span class="bash"> docker </span><span class="hljs-command"><span class="bash">run -</span><span class="hljs-keyword"><span class="bash">it </span><span class="hljs-comment"><span class="bash">--cpuset-cpus=<span class="hljs-string">"0-2"</span> ubuntu:14.04 /bin/bash</span> <span class="bash">$ cat /sys/fs/cgroup/cpuset/docker/<容器的完整长 ID>/cpuset.cpus</span> </span></span></span></code>
表示容器中的进程可以在 cpu 0、cpu 1 及 cpu 3 上执行。
在 NUMA 系统上,我们可以设置容器可以使用的内存节点。
例如:
<code class="language-shell hljs applescript has-numbering"><span class="hljs-meta">$</span><span class="bash"> docker </span><span class="hljs-command"><span class="bash">run -</span><span class="hljs-keyword"><span class="bash">it </span><span class="hljs-comment"><span class="bash">--cpuset-mems=<span class="hljs-string">"1,3"</span> ubuntu:14.04 /bin/bash</span></span></span></span></code>
表示容器中的进程只能使用内存节点 1 和 3 上的内存。
<code class="language-shell hljs applescript has-numbering"><span class="hljs-meta">$</span><span class="bash"> docker </span><span class="hljs-command"><span class="bash">run -</span><span class="hljs-keyword"><span class="bash">it </span><span class="hljs-comment"><span class="bash">--cpuset-mems=<span class="hljs-string">"0-2"</span> ubuntu:14.04 /bin/bash</span></span></span></span></code>
表示容器中的进程只能使用内存节点 0、1、2 上的内存。
CPU 资源的相对限制
默认情况下,所有的容器得到同等比例的 CPU 周期。在有多个容器竞争 CPU 时我们可以设置每个容器能使用的 CPU 时间比例。这个比例叫作共享权值,通过-c
或--cpu-shares
设置。Docker 默认每个容器的权值为 1024。不设置或将其设置为 0,都将使用这个默认值。系统会根据每个容器的共享权值和所有容器共享权值和比例来给容器分配 CPU 时间。
假设有三个正在运行的容器,这三个容器中的任务都是 CPU 密集型的。第一个容器的 cpu 共享权值是 1024,其它两个容器的 cpu 共享权值是 512。第一个容器将得到 50% 的 CPU 时间,而其它两个容器就只能各得到 25% 的 CPU 时间了。如果再添加第四个 cpu 共享值为 1024 的容器,每个容器得到的 CPU 时间将重新计算。第一个容器的 CPU 时间变为 33%,其它容器分得的 CPU 时间分别为 16.5%、16.5%、33%。
必须注意的是,这个比例只有在 CPU 密集型的任务执行时才有用。在四核的系统上,假设有四个单进程的容器,它们都能各自使用一个核的 100% CPU 时间,不管它们的 cpu 共享权值是多少。
在多核系统上,CPU 时间权值是在所有 CPU 核上计算的。即使某个容器的 CPU 时间限制少于 100%,它也能使用各个 CPU 核的 100% 时间。
例如,假设有一个不止三核的系统。用-c=512
的选项启动容器{C0}
,并且该容器只有一个进程,用-c=1024
的启动选项为启动容器C2
,并且该容器有两个进程。CPU 权值的分布可能是这样的:
<code class="language-shell hljs matlab has-numbering">PID container CPU CPU share <span class="hljs-number">100 <span class="hljs-cell">{C0} 0 100<span class="hljs-comment">% of CPU0 101 {C1} 1 100% of CPU1 102 {C1} 2 100% of CPU2 $ docker run -it --cpu-shares=100 ubuntu:14.04 /bin/bash $ cat /sys/fs/cgroup/cpu/docker/<容器的完整长 ID>/cpu.shares </span></span></span></code>
表示容器中的进程 CPU 份额值为 100。
CPU 资源的绝对限制
Linux 通过 CFS(Completely Fair Scheduler,完全公平调度器)来调度各个进程对 CPU 的使用。CFS 默认的调度周期是 100ms。
关于 CFS 的更多信息,参考CFS documentation on bandwidth limiting。
我们可以设置每个容器进程的调度周期,以及在这个周期内各个容器最多能使用多少 CPU 时间。使用--cpu-period
即可设置调度周期,使用--cpu-quota
即可设置在每个周期内容器能使用的 CPU 时间。两者一般配合使用。
例如:
<code class="language-shell hljs applescript has-numbering"><span class="hljs-meta">$</span><span class="bash"> docker </span><span class="hljs-command"><span class="bash">run -</span><span class="hljs-keyword"><span class="bash">it </span><span class="hljs-comment"><span class="bash">--cpu-period=50000 --cpu-quota=25000 ubuntu:16.04 /bin/bash</span></span></span></span></code>
将 CFS 调度的周期设为 50000,将容器在每个周期内的 CPU 配额设置为 25000,表示该容器每 50ms 可以得到 50% 的 CPU 运行时间。
<code class="language-shell hljs applescript has-numbering"><span class="hljs-meta">$</span><span class="bash"> docker </span><span class="hljs-command"><span class="bash">run -</span><span class="hljs-keyword"><span class="bash">it </span><span class="hljs-comment"><span class="bash">--cpu-period=10000 --cpu-quota=20000 ubuntu:16.04 /bin/bash</span> <span class="bash">$ cat /sys/fs/cgroup/cpu/docker/<容器的完整长 ID>/cpu.cfs_period_us</span> <span class="bash">$ cat /sys/fs/cgroup/cpu/docker/<容器的完整长 ID>/cpu.cfs_quota_us</span> </span></span></span></code>
将容器的 CPU 配额设置为 CFS 周期的两倍,CPU 使用时间怎么会比周期大呢?其实很好解释,给容器分配两个 vCPU 就可以了。该配置表示容器可以在每个周期内使用两个 vCPU 的 100% 时间。
CFS 周期的有效范围是 1ms~1s,对应的--cpu-period
的数值范围是 1000~1000000。而容器的 CPU 配额必须不小于 1ms,即--cpu-quota
的值必须 >= 1000。可以看出这两个选项的单位都是 us。
正确的理解“绝对”
注意前面我们用--cpu-quota
设置容器在一个调度周期内能使用的 CPU 时间时实际上设置的是一个上限。并不是说容器一定会使用这么长的 CPU 时间。比如,我们先启动一个容器,将其绑定到 cpu 1 上执行。给其--cpu-quota
和--cpu-period
都设置为 50000。
<code class="language-shell hljs brainfuck has-numbering"><span class="hljs-comment"><span class="hljs-meta">$</span> <span class="bash">docker </span><span class="bash">run </span><span class="hljs-literal"><span class="bash">-</span><span class="bash">-</span><span class="bash">rm </span><span class="bash">-</span><span class="bash">-</span><span class="bash">name </span><span class="bash">test01 </span><span class="bash">-</span><span class="bash">-</span><span class="bash">cpu</span><span class="bash">-</span><span class="bash">cpus </span><span class="bash">1 </span><span class="bash">-</span><span class="bash">-</span><span class="bash">cpu</span><span class="bash">-</span><span class="bash">quota=50000 </span><span class="bash">-</span><span class="bash">-</span><span class="bash">cpu</span><span class="bash">-</span><span class="bash">period=50000 </span><span class="bash">deadloop:busybox</span><span class="bash">-</span><span class="bash">1</span><span class="hljs-string"><span class="bash">.</span><span class="bash">25</span><span class="bash">.</span><span class="bash">1</span><span class="bash">-</span><span class="bash">glibc</span></span></span></span></code>
调度周期为 50000,容器在每个周期内最多能使用 50000 cpu 时间。
再用docker stats test01
可以观察到该容器对 CPU 的使用率在 100%左右。然后,我们再以同样的参数启动另一个容器。
<code class="language-shell hljs brainfuck has-numbering"><span class="hljs-comment"><span class="hljs-meta">$</span> <span class="bash">docker </span><span class="bash">run </span><span class="hljs-literal"><span class="bash">-</span><span class="bash">-</span><span class="bash">rm </span><span class="bash">-</span><span class="bash">-</span><span class="bash">name </span><span class="bash">test02 </span><span class="bash">-</span><span class="bash">-</span><span class="bash">cpu</span><span class="bash">-</span><span class="bash">cpus </span><span class="bash">1 </span><span class="bash">-</span><span class="bash">-</span><span class="bash">cpu</span><span class="bash">-</span><span class="bash">quota=50000 </span><span class="bash">-</span><span class="bash">-</span><span class="bash">cpu</span><span class="bash">-</span><span class="bash">period=50000 </span><span class="bash">deadloop:busybox</span><span class="bash">-</span><span class="bash">1</span><span class="hljs-string"><span class="bash">.</span><span class="bash">25</span><span class="bash">.</span><span class="bash">1</span><span class="bash">-</span><span class="bash">glibc</span></span></span></span></code>
再用docker stats test01 test02
可以观察到这两个容器,每个容器对 cpu 的使用率在 50% 左右。说明容器并没有在每个周期内使用 50000 的 cpu 时间。
使用docker stop test02
命令结束第二个容器,再加一个参数-c 2048
启动它:
<code class="language-shell hljs brainfuck has-numbering"><span class="hljs-comment"><span class="hljs-meta">$</span> <span class="bash">docker </span><span class="bash">run </span><span class="hljs-literal"><span class="bash">-</span><span class="bash">-</span><span class="bash">rm </span><span class="bash">-</span><span class="bash">-</span><span class="bash">name </span><span class="bash">test02 </span><span class="bash">-</span><span class="bash">-</span><span class="bash">cpu</span><span class="bash">-</span><span class="bash">cpus </span><span class="bash">1 </span><span class="bash">-</span><span class="bash">-</span><span class="bash">cpu</span><span class="bash">-</span><span class="bash">quota=50000 </span><span class="bash">-</span><span class="bash">-</span><span class="bash">cpu</span><span class="bash">-</span><span class="bash">period=50000 </span><span class="bash">-</span><span class="bash">c </span><span class="bash">2048 </span><span class="bash">deadloop:busybox</span><span class="bash">-</span><span class="bash">1</span><span class="hljs-string"><span class="bash">.</span><span class="bash">25</span><span class="bash">.</span><span class="bash">1</span><span class="bash">-</span><span class="bash">glibc</span></span></span></span></code>
再用docker stats test01
命令可以观察到第一个容器的 CPU 使用率在 33% 左右,第二个容器的 CPU 使用率在 66% 左右。因为第二个容器的共享值是 2048,第一个容器的默认共享值是 1024,所以第二个容器在每个周期内能使用的 CPU 时间是第一个容器的两倍。
磁盘 IO 配额控制
相对于 CPU 和内存的配额控制,docker 对磁盘 IO 的控制相对不成熟,大多数都必须在有宿主机设备的情况下使用。主要包括以下参数:
- –device-read-bps:限制此设备上的读速度(bytes per second),单位可以是 kb、mb 或者 gb。
- –device-read-iops:通过每秒读 IO 次数来限制指定设备的读速度。
- –device-write-bps :限制此设备上的写速度(bytes per second),单位可以是 kb、mb 或者 gb。
- –device-write-iops:通过每秒写 IO 次数来限制指定设备的写速度。
- –blkio-weight:容器默认磁盘 IO 的加权值,有效值范围为 10-100。
- –blkio-weight-device: 针对特定设备的 IO 加权控制。其格式为 DEVICE_NAME:WEIGHT
存储配额控制的相关参数,可以参考Red Hat 文档中 blkio 这一章,了解它们的详细作用。
磁盘 IO 配额控制示例
blkio-weight
要使–blkio-weight 生效,需要保证 IO 的调度算法为 CFQ。可以使用下面的方式查看:
root@ubuntu:~# cat /sys/block/sda/queue/scheduler
noop [deadline] cfq
使用下面的命令创建两个–blkio-weight 值不同的容器:
docker run -ti –rm –blkio-weight 100 ubuntu:stress
docker run -ti –rm –blkio-weight 1000 ubuntu:stress
在容器中同时执行下面的 dd 命令,进行测试:
time dd if=/dev/zero of=test.out bs=1M count=1024 oflag=direct
最终输出如下图所示:
在我的测试环境上没有达到理想的测试效果,通过 docker 官方的blkio-weight doesn’t take effect in docker Docker version 1.8.1 #16173,可以发现这个问题在一些环境上存在,但 docker 官方也没有给出解决办法。
device-write-bps
使用下面的命令创建容器,并执行命令验证写速度的限制。
docker run -tid –name disk1 –device-write-bps /dev/sda:1mb ubuntu:stress
通过 dd 来验证写速度,输出如下图示:
可以看到容器的写磁盘速度被成功地限制到了 1MB/s。device-read-bps 等其他磁盘 IO 限制参数可以使用类似的方式进行验证。
容器空间大小限制
在 docker 使用 devicemapper 作为存储驱动时,默认每个容器和镜像的最大大小为 10G。如果需要调整,可以在 daemon 启动参数中,使用 dm.basesize 来指定,但需要注意的是,修改这个值,不仅仅需要重启 docker daemon 服务,还会导致宿主机上的所有本地镜像和容器都被清理掉。
使用 aufs 或者 overlay 等其他存储驱动时,没有这个限制。