限制容器的资源

默认情况下,容器没有资源限制,可以使用主机内核调度程序允许的尽可能多的给定资源。

Memory

内存风险

不允许容器消耗宿主机太多的内存是非常重要的。在 Linux 主机上,如果内核检测到没有足够的内存来执行重要的系统功能,它会抛出  OOME 或 Out of Memory 异常,并开始终止进程以释放内存。任何进程都会被杀死,包括 Docker 和其他重要的应用程序。如果杀错进程,可能导致整个系统瘫痪。

Docker 通过调整 Docker daemon 上的 OOM 优先级来降低这些风险,以便它比系统上的其他进程更不可能被杀死。容器上的 OOM 优先级未调整,这使得单个容器被杀死的可能性比 Docker daemon 或其他系统进程被杀死的可能性更大。你不应试图通过在 daemon 或容器上手动设置  --oom-score-adj 到极端负数,或通过在容器上设置  --oom-kill-disable 来绕过这些安全措施。

有关Linux内核的OOM管理的更多信息,查看 Out of Memory Management

可以通过以下方式降低 OOME 导致系统不稳定的风险:

  • 在应用程序发布到生产之前,执行相关测试以便了解应用程序的内存要求;
  • 确保应用程序仅在具有足够资源的主机上运行;
  • 限制容器可以使用的内存,如下所述;
  • 在 Docker 主机上配置 Swap 时要小心,Swap 比内存更慢且性能更低,但可以提供缓冲以防止系统内存耗尽;
  • 考虑将 Container 转换部署为 Service,并使用服务级别约束和节点标签来确保应用程序仅在具有足够内存的主机上运行。

限制容器内存

下述选项中的大多数采用正整数,后跟 b / k / m / g 的后缀,代表单位:字节 / 千字节 / 兆字节 / 千兆字节。

选项

描述

-m or --memory=

容器可使用的最大内存, 最小值是 4m

--memory-swap*

允许此容器交换到磁盘的内存量

--memory-swappiness

默认情况下,主机内核可以交换容器使用的匿名页面的百分比,可以设置 --memory-swappiness 的值区间为 0 - 100

--memory-reservation

指定小于 --memory 的软限制,当 Docker 检测到主机上的争用或内存不足时会激活该限制. 如果使用 --memory-reservation,则必须将其设置为低于 --memory 才能使其优先。因为它是软限制,所以不保证容器不超过限制。

--kernel-memory

容器可以使用的最大内核内存量, 最小值是 4m,因为内核内存无法换出,缺乏内核内存的容器可能会阻塞主机资源,这会对主机和其他容器产生副作用

--oom-kill-disable

默认情况, 如果发生内存不足(OOM)错误,内核会终止容器中的进程。 要改变这种行为,使用 --oom-kill-disable 选项。 仅在已设置 -m /  -memory 选项的容器上禁用 OOM killer,如果未设置 -m 标志,则主机可能会耗尽内存,内核可能需要终止主机系统的进程才能释放内存

有关 cgroup 和内存的更多信息,查看 Memory Resource Controller

关于 --memory-swap

--memory-swap 是一个修饰符标志,只有在设置了 --memory 时才有意义。使用 swap 允许容器在容器耗尽所有可用的 RAM 时,将多余的内存需求写入磁盘。对于经常将内存交换到磁盘的应用程序,性能会受到影响。

它的设置会产生复杂的影响:

  • 如果 --memory-swap 设置为正整数,则必须设置 --memory 和 --memory-swap。 --memory-swap 表示可以使用的 memory 和 swap 总量,  --memory 控制 no-swap 的用量。 所以,如果设置 --memory="300m" 和 --memory-swap="1g",  容器可以使用 300m memory 和 700m (1g - 300m) swap。
  • 如果 --memory-swap 设置为 0, 该设置被忽略,该值被视为未设置。
  • 如果 --memory-swap 的值等于 --memory 的值, 并且 --memory 设置为正整数, 则容器无权访问 swap。这是因为 --memory-swap 是可以使用组合的 Memory 和 Swap,而 --memory 只是可以使用的 Memory。
  • 如果 --memory-swap 不设置, 并且 --memory 设置了值, 容器可以使用 --memory 两倍的 Swap(如果主机容器配置了 Swap)。示例: 设置 --memory="300m" 并且不设置 --memory-swap,容器可以使用 300m memory 和 600m swap。
  • 如果 --memory-swap 设置为 -1,允许容器无限制使用 Swap。
  • 在容器内部,像 free 等工具报告的是主机的可用 Swap,而不是容器内可用的。不要依赖于 free 或类似工具的输出来确定是否存在 Swap。

docker的零宕机部署 docker oom_docker的零宕机部署

关于 --memory-swappiness

  • 值为 0 时,关闭匿名页交换。
  • 值为 100 时,将所有匿名页设置为可交换。
  • 默认情况下,如果不设置 --memory-swappiness, 该值从主机继承。

关于 --kernel-memory

内核内存限制是就分配给容器的总内存而言的,考虑一下方案:

  • 无限内存,无限内核内存:这是默认行为。
  • 无限内存,有限内核内存:当所有 cgroup 所需的内存量大于主机上实际存在的内存量时,它是合适的。可以将内核内存配置为永远不会超过主机上可用的内存,而需求更多内存的容器需要等待它。
  • 有限内存,无限内核内存:整体内存有限,但内核内存不是。
  • 有限内存,有限内核内存:限制用户和内核内存对于调试与内存相关的问题非常有用,如果容器使用意外数量的任意类型的内存,则内存不足不会影响其他容器或主机。在此设置中,如果内核内存限制低于用户内存限制,则内核内存不足会导致容器遇到 OOM 错误。如果内核内存限制高于用户内存限制,则内核限制不会导致容器遇到 OOM。

当你打开任何内核内存限制时,主机会根据每个进程跟踪 “高水位线” 统计信息,因此您可以跟踪哪些进程(在本例中为容器)正在使用多余的内存。通过查看主机上的 /proc/<PID>/status,可以在每个进程中看到这一点。

CPU

默认情况下,每个容器对主机 CPU 周期的访问权限是不受限制的,您可以设置各种约束来限制给定容器访问主机的 CPU 周期。大多数用户使用和配置 默认 CFS 调度程序。在 Docker 1.13 及更高版本中,还可以配置实时调度程序。

配置默认 CFS 调度程序

CFS 是用于普通 Linux 进程的 Linux 内核 CPU 调度程序。通过以下设置,可以控制容器的 CPU 资源访问量,使用这些设置时,Docker 会修改主机上容器的 cgroup 的设置。

选项

描述

--cpus=<value>

指定容器可以使用的 CPU 资源量。例如,如果主机有两个CPU并且,设置 --cpus="1.5",则容器最多可以使用 1.5 个 CPU,这相当于设置 --cpu-period="100000" 和 --cpu-quota="150000"。可在Docker 1.13及更高版本中使用。

--cpu-period=<value>

指定 CPU CFS 调度程序周期,该周期与 --cpu-quota 一起使用,默认为100微秒。大多数用户不会更改默认设置,如果使用Docker 1.13 或更高版本,请改用 --cpus

--cpu-quota=<value>

对容器施加 CPU CFS 配额,在受限制之前容器限制为每个 --cpu-period 的微秒数,作为有效上限。如果使用Docker 1.13 或更高版本,请改用 --cpus

--cpuset-cpus

限制容器可以使用的特定 CPU 或核心。如果主机有多个CPU,则容器可以使用的以, 分 隔的列表或 - 分隔的 CPU 范围。第一个CPU 编号为 0,有效值可能是 0-3(使用第一个、第二个、第三个和第四个CPU)或 1,3(使用第二个和第四个CPU)。

--cpu-shares

将此值设置为大于或小于默认值 1024,以增加或减少容器的权重,并使其可以访问主机的 CPU 周期的占较大或较小比例。仅在 CPU 周期受限时才会强制执行此操作。当有足够的 CPU 周期时,所有容器都会根据需要使用尽可能多的 CPU。这是一个软限制,--cpu-shares 不会阻止在群集模式下的容器调度。它为可用的 CPU 周期优先考虑容器 CPU 资源。它不保证或保留任何特定的 CPU 访问权限。

示例:如果你有 1 个 CPU,则以下每个命令都会保证容器每秒最多占 CPU 的 50%。

Docker 1.13 或更高版本:

docker run -it --cpus=".5" ubuntu /bin/bash

Docker 1.12 或更低版本:

docker run -it --cpu-period=100000 --cpu-quota=50000 ubuntu /bin/bash

配置实时调度程序

在 Docker 1.13 或更高版本,你可以配置容器使用实时调度程序。在配置 Docker daemon 或配置容器之前,需要确保正确配置主机的内核。

警告:CPU 调度和优先级是高级内核级功能,大多数用户不需要从默认值更改这些值,错误地设置这些值可能会导致主机系统变得不稳定或无法使用。

配置主机机器的内核

通过运行 zcat /proc/config.gz | grep CONFIG_RT_GROUP_SCHED 验证是否在 Linux 内核中启用了 CONFIG_RT_GROUP_SCHED,或者检查是否存在文件 /sys/fs/cgroup/cpu.rt_runtime_us。有关配置内核实时调度程序的教程,请参阅操作系统的文档。

配置DOCKER DAEMON

要使用实时调度程序运行容器,请运行 Docker daemon,并将 --cpu-rt-runtime 设置为每个运行时间段为实时任务保留的最大微秒数。例如,默认周期为 1000000 微秒(1秒),设置 --cpu-rt-runtime=950000 可确保使用实时调度程序的容器每 1000000 微秒可运行 950000 微秒,并保留至少 50000 微秒用于非实时任务。要在使用 systemd 的系统上永久保留此配置,请参阅 Control and configure Docker with systemd

配置个别容器

使用 docker run 启动容器时,可以传递多个参数来控制容器的 CPU 优先级。有关适当值的信息,请参阅操作系统的文档或 ulimit 命令。

选项

描述

--cap-add=sys_nice

授予容器 CAP_SYS_NICE 功能,该功能允许容器引发进程良好值,设置实时调度策略,设置 CPU 亲和性以及其他操作。

--cpu-rt-runtime=<value>

容器可以在 Docker 守护程序的实时调度程序周期内以实时优先级运行的最大微秒数,需要设置 --cap-add=sys_nice 。

--ulimit rtprio=<value>

容器允许的最大实时优先级,需要 --cap-add=sys_nice 标志。

示例:

docker run --it --cpu-rt-runtime=950000 \
                  --ulimit rtprio=99 \
                  --cap-add=sys_nice \
                  debian:jessie

如果未正确配置内核或 Docker Daemon,则会发生错误。

演示效果

[root@node1 ~]# lscpu 
 Architecture:          x86_64
 CPU op-mode(s):        32-bit, 64-bit
 Byte Order:            Little Endian
CPU(s):                1
 On-line CPU(s) list:   0
 Thread(s) per core:    1
 Core(s) per socket:    1
 座:                 1
 NUMA 节点:         1
 厂商 ID:           GenuineIntel
 CPU 系列:          6
 型号:              94
 型号名称:        Intel(R) Core(TM) i5-6300HQ CPU @ 2.30GHz
 步进:              3
 CPU MHz:             2304.002
 BogoMIPS:            4608.00
 超管理器厂商:  VMware
 虚拟化类型:     完全
 L1d 缓存:          32K
 L1i 缓存:          32K
 L2 缓存:           256K
 L3 缓存:           6144K
 NUMA 节点0 CPU:    0


拖压测工具:

https://hub.docker.com/r/lorel/docker-stress-ng/

[root@node1 ~]# docker pull lorel/docker-stress-ng
 查看选项:[root@node1 ~]# docker run --name stress -it --rm lorel/docker-stress-ng stress --help
设置选项
[root@node1 ~]# docker run --name stress -it --rm  -m 256m lorel/docker-stress-ng stress --vm 2 
//给docker传选项,该进程最多占用256M内存    --vm 2 则需要512M,内存溢出

结果

[root@node1 ~]# docker run --name stress -it --rm  -m 256m lorel/docker-stress-ng stress --vm 2
 stress-ng: info: [1] defaulting to a 86400 second run per stressor
 stress-ng: info: [1] dispatching hogs: 2 vm
 ^[[^Cstress-ng: info: [1] successful run completed in 27.36s
 [root@node1 ~]# docker run --name stress -it --rm  -m 256m lorel/docker-stress-ng stress --vm 2
 stress-ng: info: [1] defaulting to a 86400 second run per stressor
 stress-ng: info: [1] dispatching hogs: 2 vm

查看进程

[root@node1 ~]# docker top stress
 UID                 PID                 PPID                C                   STIME               TTY                 TIME                CMD
 root                2749                2735                0                   21:35               pts/0               00:00:00            /usr/bin/stress-ng stress --vm 2
root                2779                2749                0                   21:35               pts/0               00:00:00            /usr/bin/stress-ng stress --vm 2
 root                2780                2749                0                   21:35               pts/0               00:00:00            /usr/bin/stress-ng stress --vm 2
 root                2800                2779                20                  21:36               pts/0               00:00:01            /usr/bin/stress-ng stress --vm 2
 root                2807                2780                0                   21:36               pts/0               00:00:00            /usr/bin/stress-ng stress --vm 2显示内存变量信息
[root@node1 ~]# docker stats
CONTAINER ID        NAME                CPU %               MEM USAGE / LIMIT   MEM %               NET I/O             BLOCK I/O           PIDS
 52fdb41f90fd        stress              23.12%              256MiB / 256MiB

演示限制CPU核心数

[root@node1 ~]# docker run --name stress -it --rm  --cpus 1 lorel/docker-stress-ng:latest stress  --cpu 8 
//也可以不加,默认会把CPU核数全吞下去
 stress-ng: info: [1] defaulting to a 86400 second run per stressor
 stress-ng: info: [1] dispatching hogs: 8 cpu
 [root@node1 ~]# docker top stress
 UID                 PID                 PPID                C                   STIME               TTY                 TIME                CMD
 root                3466                3452                0                   21:53               pts/0               00:00:00            /usr/bin/stress-ng stress --cpu 8
 root                3496                3466                11                  21:53               pts/0               00:00:04            /usr/bin/stress-ng stress --cpu 8
 root                3497                3466                11                  21:53               pts/0               00:00:04            /usr/bin/stress-ng stress --cpu 8
 root                3498                3466                11                  21:53               pts/0               00:00:04            /usr/bin/stress-ng stress --cpu 8
 root                3499                3466                11                  21:53               pts/0               00:00:04            /usr/bin/stress-ng stress --cpu 8
 root                3500                3466                11                  21:53               pts/0               00:00:04            /usr/bin/stress-ng stress --cpu 8
 root                3501                3466                11                  21:53               pts/0               00:00:04            /usr/bin/stress-ng stress --cpu 8
 root                3502                3466                11                  21:53               pts/0               00:00:04            /usr/bin/stress-ng stress --cpu 8
 root                3503                3466                11                  21:53               pts/0               00:00:04            /usr/bin/stress-ng stress --cpu 8[root@node1 ~]# docker stats
CONTAINER ID        NAME                CPU %               MEM USAGE / LIMIT     MEM %               NET I/O             BLOCK I/O           PIDS
 ff567312dc6e        stress              90.73%              15.81MiB / 974.6MiB   1.62%               648B / 0B           0B / 0B             9

docker容器如何进行资源控制和验证?

Docker同LXC一样,其对资源的隔离和管控是以Linux内核的namespaces和cgroup为基础。Docker的资源隔离使用了Linux内核 Kernel中的Namespaces功能来实现,隔离的对象包括:主机名与域名、进程编号、网络设备、文件系统的挂载点等,namespace中的IPC隔离docker并未使用,docker中使用TCP替代IPC。

在使用Namespaces隔离资源的同时,Docker使用了Linux内核Kernel提供的cgroup来对Container使用的CPU、内存、磁盘IO资源进行配额管控。换句话说,在docker的容器启动参数中,像—cpu*、--memory*和—blkio*的设置,实际上就是设置cgroup的相对应cpu子系统、内存子系统、磁盘IO子系统的配额控制文件,只不过这个修改配额控制文件的过程是docker实例代替我们做掉罢了。因此,我们完全可以直接修改docker容器所对应的cgroup子系统中的配额控制文件来达到控制docker容器资源配额的同样目的,接下来将以IO控制为例,介绍一下具体实现过程。

【物理部署架构图】

实验测试环境物理部署图如图1

整体的部署思路如下:
1、首先将物理磁盘进行分区操作(此步骤可选)
2、将分区创建为物理卷(PV)并添加到卷组(VG)中
3、从卷组(VG)中创建出逻辑卷(LV),并将该卷格式化后挂载供docker使用

4、将步骤3中逻辑卷挂载点再挂载到docker镜像容器中。

【具体操作实现过程】

1、将磁盘设备/dev/sdb进行分区,分别为sdb1和sdb2

2、创建PV

pvcreate /dev/sdb2 #使用sdb2创建物理卷(PV)

3、创建VG

vgcreate datavg /dev/sdb2 #vg名称为datavg

4、创建LV,并格式化

lvcreate -n datalv datavg && mkfs.ext4 /dev/mapper/datavg-datalv

5、创建目录并挂载

mkdir /data && mount /dev/mapper/datavg-datalv /data

【测试过程】

1、 使用docker镜像docker.io/learn/tutorial :jiangzt作为测试容器


2、 不加配额参数的情况下启动容器
启动容器时不配额参数,并观察其所在的cgroup子系统的配置情况
docker run -dit -h inner --name test -v /data:/data a36927dbb31f /bin/bash

根据容器ID观察cgroup下blkio子系统的配置情况,结果是文件blkio.throttle.write_bps_device中没有内容

进入容器内,执行dd命令,测试观察此时默认的写入IO能力

docker exec -it test /bin/bash
 time dd if=/dev/zero of=/data/test.out bs=1M count=1024 oflag=direct


可以得知写入的平均速度是 2.1GB/s

3、使用参数device-write-bps启动容器

启动容器增加配额参数device-write-bps,写入目标速度1mb/s,并观察其所在的cgroup子系统的配置情况

docker run -dit -m 100m -h inner --name test --device-write-bps /dev/dm-6:1mb -v/data:/data a36927dbb31f /bin/bash

说明:lv的设备名称/dev/dm-6可以通过dmsetup做查询

容器启动后根据容器id观察cgroup子系统中的配置情况

在cgroup的blkio.throttle.write_bps_device文件中观察到了253:6 1048576 就是我们在启动docker时指定的--device-write-bps参数值,设备为253:6(/dev/dm-6),io能力为1048576byte(1mb)

进入容器内,执行dd命令,测试观察此时默认的写入IO能力

可以观察到此时的写入速度为1.0MB/s
4、 直接修改cgroup中的参数配置
前面在原理介绍时提过,docker其实也是修改cgroup中的参数来控制资源的使用,那么我们直接修改blkio.throttle.write_bps_device,然后观察是否能直接作用于docker容器,还是使用上面的容器id进行操作,将blkio.throttle.write_bps_device中的内容修改为10MB/secho 253:6 10485760 > blkio.throttle.write_bps_device进入容器内,执行dd命令,测试观察此时默认的写入IO能力

可以看到通过直接修改cgroup中的blkio.throttle.write_bps_device的速度值起到了作用
5、 对于其他参数的控制分析
细心的你可能已经发现在docker镜像时,还有个参数-m 100m,该参数是用来控制容器使用内存的配额,我们可以通过docker stats进行验证

同样我们也是可以在cgroup的内存子系统中看到相应的参数配置。

【总结】

通过以上的分析测试,进一步熟悉了解了docker运行时的行为和所依赖的底层的技术原理,当我们希望在不停止docker容器运行的前提下,能够对cpu、内存和磁盘IO等资源进行动态配额调整时,便可以通过直接修改cgroup配置来达到目的,希望能够为docker的自动化运维开发提供解决思路和办法。