限制容器的资源
默认情况下,容器没有资源限制,可以使用主机内核调度程序允许的尽可能多的给定资源。
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
的后缀,代表单位:字节 / 千字节 / 兆字节 / 千兆字节。
选项 | 描述 |
| 容器可使用的最大内存, 最小值是 |
| 允许此容器交换到磁盘的内存量 |
| 默认情况下,主机内核可以交换容器使用的匿名页面的百分比,可以设置 |
| 指定小于 |
| 容器可以使用的最大内核内存量, 最小值是 |
| 默认情况, 如果发生内存不足(OOM)错误,内核会终止容器中的进程。 要改变这种行为,使用 |
有关 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。
关于 --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并且,设置 |
--cpu-period=<value> | 指定 CPU CFS 调度程序周期,该周期与 |
--cpu-quota=<value> | 对容器施加 CPU CFS 配额,在受限制之前容器限制为每个 |
--cpuset-cpus | 限制容器可以使用的特定 CPU 或核心。如果主机有多个CPU,则容器可以使用的以 |
--cpu-shares | 将此值设置为大于或小于默认值 1024,以增加或减少容器的权重,并使其可以访问主机的 CPU 周期的占较大或较小比例。仅在 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 守护程序的实时调度程序周期内以实时优先级运行的最大微秒数,需要设置 |
--ulimit rtprio=<value> | 容器允许的最大实时优先级,需要 |
示例:
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的自动化运维开发提供解决思路和办法。