参考文章: Docker容器CPU、memory资源限制
背景
在使用 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(Control Group)
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和内存。参考 监控 Docker 资源的占用情况
内存限制
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
选项的参数最小为 4M;--memory-swap
不是交换分区,而是内存加交换分区的总大小,所以--memory-swap
必须比-m,--memory
大;
注:如果在进行内存限制时发现
docker run
命令报错:WARNING: Your kernel does not support swap limit capabilities, memory limited without swap
。这是因为宿主机内核的相关功能没有打开。按照下面的设置就行。
- 编辑
/etc/default/grub
文件,将GRUB_CMDLINE_LINUX
一行改为GRUB_CMDLINE_LINUX="cgroup_enable=memory swapaccount=1"
;- 执行
sudo update-grub
更新 GRUB;- 重启系统。
参数设置分析
- 如果不设置
-m,--memory
和--memory-swap
,容器默认可以用完宿主机的所有内存和 swap 分区。不过注意,如果容器占用宿主机的所有内存和 swap 分区超过一段时间后,会被宿主机系统杀死(如果没有设置--oom-kill-disable=true
的话)。 - 设置
-m,--memory
,不设置--memory-swap
给-m
或--memory
设置一个不小于 4M 的值,假设为 a,不设置--memory-swap
,或将--memory-swap
设置为 0。这种情况下,容器能使用的内存大小为 a,能使用的交换分区大小也为 a。因为 Docker 默认容器交换分区的大小和内存相同。 - 设置
-m,--memory=a
,--memory-swap=b
,且 b > a
给-m
设置一个参数 a,给--memory-swap
设置一个参数 b。a 时容器能使用的内存大小,b是容器能使用的 内存大小 + swap 分区大小。所以 b 必须大于 a。b-a 即为容器能使用的 swap 分区大小。 - 设置
-m,--memory=a
,--memory-swap=-1
,给-m
参数设置一个正常值,而给--memory-swap
设置成 -1。这种情况表示限制容器能使用的内存大小为 a,而不限制容器能使用的 swap 分区大小。这时候,容器内进程能申请到的内存大小为 a + 宿主机的 swap 大小。
Memory reservation
Memory reservation 是一种软性限制,用于节制容器内存使用。给 --memory-reservation
设置一个比 -m
小的值后,虽然容器最多可以使用 -m
使用的内存大小,但在宿主机内存资源紧张时,在系统的下次内存回收时,系统会回收容器的部分内存页,强迫容器的内存占用回到 --memory-reservation
设置的值大小。
没有设置时(默认情况下)--memory-reservation
的值和 -m
的限定的值相同。将它设置为 0 或设置的比 -m
的参数大时等同于没有设置。
Memory reservation 是一种软性机制,它不保证任何时刻容器使用的内存不会超过 --memory-reservation
限定的值,它只是确保容器不会长时间占用超过--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 错误时,系统通过杀死宿主机进程或获取更改内存。
核心内存
核心内存和用户内存不同的地方在于核心内存不能被交换出。不能交换出去的特性使得容器可以通过消耗太多内存来堵塞一些系统服务。核心内存包括:
- stack pages(栈页面)
- slab pages
- socket memory pressure
- tcp memory pressure
可以通过设置核心内存限制来约束这些内存。例如,每个进程都要消耗一些栈页面,通过限制核心内存,可以在核心内存使用过多时阻止新进程被创建。
核心内存和用户内存并不是独立的,必须在用户内存限制的上下文中限制核心内存。
Swappiness
默认情况下,容器的内核可以交换出一定比例的匿名页。--memory-swappiness
就是用来设置这个比例的。--memory-swappiness
可以设置为从 0 到 100。0 表示关闭匿名页面交换。100 表示所有的匿名页都可以交换。默认情况下,如果不使用 --memory-swappiness
,则该值从父进程继承而来。
限制内存使用示例
1 | docker run \ |
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 核上运行。
以下示例表示容器中的进程可以在 cpu 1 和 cpu 3 上执行。
1
docker run -it --cpuset-cpus="1,3" ubuntu:14.04 /bin/bash
以下示例表示容器中的进程可以在 cpu 0、cpu 1 及 cpu 2 上执行。
1
2
3docker run -it --cpuset-cpus="0-2" ubuntu:14.04 /bin/bash
$ cat /sys/fs/cgroup/cpuset/docker/<容器的完整长ID>/cpuset.cpus在 NUMA 系统上,我们可以设置容器可以使用的内存节点。以下示例表示容器中的进程只能使用内存节点 1 和 3 上的内存。
1
docker run -it --cpuset-mems="1,3" ubuntu:14.04 /bin/bash
以下示例表示容器中的进程只能使用内存节点 0、1、2 上的内存。
1
docker run -it --cpuset-mems="0-2" ubuntu:14.04 /bin/bash
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% 时间。
CPU 资源的绝对限制
Linux 通过 CFS(Completely Fair Scheduler,完全公平调度器)来调度各个进程对 CPU 的使用。CFS 默认的调度周期是 100ms。
我们可以设置每个容器进程的调度周期,以及在这个周期内各个容器最多能使用多少 CPU 时间。使用 --cpu-period
即可设置调度周期,使用 --cpu-quota
即可设置在每个周期内容器能使用的 CPU 时间。两者一般配合使用。
例如,将 CFS 调度的周期设为 50000,将容器在每个周期内的 CPU 配额设置为 25000,表示该容器每 50ms 可以得到 50% 的 CPU 运行时间。
1
docker run -it --cpu-period=50000 --cpu-quota=25000 ubuntu:16.04 /bin/bash
如果将容器的 CPU 配额设置为 CFS 周期的两倍,CPU 使用时间怎么会比周期大呢?其实很好解释,给容器分配两个 vCPU 就可以了。以下配置表示容器可以在每个周期内使用两个 vCPU 的 100% 时间。
1
2
3
4docker run -it --cpu-period=10000 --cpu-quota=20000 ubuntu:16.04 /bin/bash
$ cat /sys/fs/cgroup/cpu/docker/<容器的完整长ID>/cpu.cfs_period_us
$ cat /sys/fs/cgroup/cpu/docker/<容器的完整长ID>/cpu.cfs_quota_us
CFS 周期的有效范围是 1ms1s,对应的 1000000。而容器的 CPU 配额必须不小于 1ms,即 --cpu-period
的数值范围是 1000--cpu-quota
的值必须 >= 1000。可以看出这两个选项的单位都是 us。
注意前面我们用 --cpu-quota
置容器在一个调度周期内能使用的 CPU 时间时实际上设置的是一个上限。并不是说容器一定会使用这么长的 CPU 时间。
磁盘 I/O 限制
相对于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
磁盘IO配额控制示例
blkio-weight
要使
-–blkio-weight
生效,需要保证 I/O 的调度算法为 CFQ。可以使用下面的方式查看:1
cat /sys/block/sda/queue/scheduler
使用下面的命令创建两个
-–blkio-weight
值不同的容器:1
2docker run -ti –rm –blkio-weight 100 ubuntu:stress
docker run -ti –rm –blkio-weight 1000 ubuntu:stress在容器中同时执行下面的dd命令,进行测试:
1
time dd if=/dev/zero of=test.out bs=1M count=1024 oflag=direct
device-write-bps
使用下面的命令创建容器,并执行命令验证写速度的限制。
1
docker run -tid –name disk1 –device-write-bps /dev/sda:1mb ubuntu:stress
通过dd来验证写速度
1
time dd if=/dev/zero of=test.out bs=1M count=1024 oflag=direct
容器空间大小限制
在 docker 使用 devicemapper
作为存储驱动时,默认每个容器和镜像的最大大小为10G。如果需要调整,可以在 daemon 启动参数中,使用 dm.basesize
来指定,但需要注意的是,修改这个值,不仅仅需要重启 docker daemon 服务,还会导致宿主机上的所有本地镜像和容器都被清理掉。
使用 aufs
或者 overlay
等其他存储驱动时,没有这个限制。