一、背景
在使用 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只是用了其中一部分子系统,实现对资源配额和使用的控制。
Linux Cgroups 给用户暴露出来的操作接口是文件系统。他以文件和目录的方式组织在操作系统的 /sys/fs/cgroup
路径下。执行以下命令可以查看:
[root@server2 ~]# mount -t cgroup
三、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 时间。
CPU 集
我们可以设置容器可以在哪些 CPU 核上运行。
例如:
docker run -it --cpuset-cpus="1,3" ubuntu /bin/bash
表示容器中的进程可以在 cpu 1 和 cpu 3 上执行。
docker run -it --cpuset-cpus="0-2" ubuntu:14.04 /bin/bash
cat /sys/fs/cgroup/cpuset/docker/<容器的完整长ID>/cpuset.cpus
表示容器中的进程可以在 cpu 0、cpu 1 及 cpu 2 上执行。
CPU 资源的绝对限制
Linux 通过 CFS(Completely Fair Scheduler,完全公平调度器)来调度各个进程对 CPU 的使用。CFS 默认的调度周期是 100ms。
我们可以设置每个容器进程的调度周期,以及在这个周期内各个容器最多能使用多少 CPU 时间。使用--cpu-period
即可设置调度周期,使用--cpu-quota
即可设置在每个周期内容器能使用的 CPU 时间。两者一般配合使用。
例如:
运行一个容器并将其打入后台:
[root@server2 ~]# docker run --cpu-period 100000 --cpu-quota 20000 -it --name demo busyboxplus
将 CFS 调度的周期设为 100000,将容器在每个周期内的 CPU 配额设置为 20000,表示该容器每 100ms 可以得到 20% 的 CPU 运行时间,可以看出这两个选项的单位都是 us。
可以查看文件:
上图中cpu.cfs_quota_us文件中默认为-1,表示不做限制。
现在进行测试:
ctrl+pq使其后台运行,top命令查看cpu占用:
可以看出容器被限制在了20%。
注意:此实验最好给虚拟机分配1个cpu核心,可以更好的看出实验效果。
其他例子:
docker run -it --cpu-period=10000 --cpu-quota=20000 ubuntu:16.04 /bin/bash
将容器的 CPU 配额设置为 CFS 周期的两倍,CPU 使用时间怎么会比周期大呢?其实很好解释,给容器分配两个 vCPU 就可了。该配置表示容器可以在每个周期内使用两个 vCPU 的 100% 时间。
CFS 周期的有效范围是 1ms~1s,对应的–cpu-period的数值范围是 1000~1000000。而容器的 CPU 配额必须不小于 1ms,即–cpu-quota的值必须 >= 1000。
CPU资源优先级配置
注意前面我们用--cpu-quota
设置容器在一个调度周期内能使用的 CPU 时间时实际上设置的是一个上限。并不是说容器一定会使用这么长的 CPU 时间。比如,我们先启动一个容器,将其绑定到 cpu 1 上执行。给其–cpu-quota和–cpu-period都设置为 50000。
[root@server2 docker]# docker run -it --name test01 --cpu-quota=50000 --cpu-period=50000 busyboxplus
/ # dd if=/dev/zero of=/dev/null &
/ # ps ax
PID USER COMMAND
1 root /bin/sh
6 root dd if /dev/zero of /dev/null
7 root ps ax
/ # [root@server2 docker]#
调度周期为 50000,容器在每个周期内最多能使用 50000 cpu 时间。
再用top可以观察到该容器对 CPU 的使用率在100%左右。然后,我们再以同样的参数启动另一个容器。
[root@server2 docker]# docker run -it --name test02 --cpu-quota=50000 --cpu-period=50000 busyboxplus
/ # dd if=/dev/zero of=/dev/null &
/ # ps ax
PID USER COMMAND
1 root /bin/sh
6 root dd if /dev/zero of /dev/null
7 root ps ax
/ # [root@server2 docker]#
再用top命令可以观察到这两个容器,每个容器对 cpu 的使用率在 50% 左右。说明容器并没有在每个周期内使用 50000 的 cpu 时间。
此时两个容器是公平分配的,要想自定义分配就需要加优先级参数启动容器,当不指定优先级时,默认为1024,可以使用参数-c
指定优先级:
使用docker rm -f test02
命令结束第二个容器,再加一个参数-c 2048启动它:
[root@server2 docker]# docker run -it --name test02 --cpu-quota=50000 --cpu-period=50000 -c 2048 busyboxplus
/ # dd if=/dev/zero of=/dev/null &
/ # ps ax
PID USER COMMAND
1 root /bin/sh
6 root dd if /dev/zero of /dev/null
7 root ps ax
/ # [root@server2 docker]#
再用top命令:
可以观察到第一个容器的 CPU 使用率在 33% 左右,第二个容器的 CPU 使用率在 66% 左右。因为第二个容器的共享值是 2048,第一个容器的默认共享值是 1024,所以第二个容器在每个周期内能使用的 CPU 时间是第一个容器的两倍。
四、内存限制
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: 重启系统。
- 不设置
如果不设置-m,–memory和–memory-swap,容器默认可以用完宿主机的所有内存和 swap 分区。不过注意,如果容器占用宿主机的所有内存和 swap 分区超过一段时间后,会被宿主机系统杀死(如果没有设置–00m-kill-disable=true的话)。
- 设置-m,–memory,不设置–memory-swap
给-m或–memory设置一个不小于 4M 的值,假设为 a,不设置–memory-swap,或将–memory-swap设置为 0。这种情况下,容器能使用的内存大小为 a,能使用的交换分区大小也为 a。因为 Docker 默认容器交换分区的大小和内存相同。
如果在容器中运行一个一直不停申请内存的程序,你会观察到该程序最终能占用的内存大小为 2a。
比如 docker run -m 1G ubuntu
,该容器能使用的内存大小为 1G,能使用的 swap 分区大小也为 1G。容器内的进程能申请到的总内存大小为 2G。
- 设置-m,–memory=a,–memory-swap=b,且b >= a
给-m设置一个参数 a,给–memory-swap设置一个参数 b。a 时容器能使用的内存大小,b是容器能使用的 内存大小 + swap 分区大小。所以 b 必须大于等于 a。b -a 即为容器能使用的 swap 分区大小。
[root@server2 docker]# docker run -it --name test1 --memory 200M --memory-swap 200M busyboxplus
/ # free -m
total used free shared buffers
Mem: 487 400 86 0 0
-/+ buffers: 399 87
Swap: 2047 2 2045
/ # [root@server2 docker]#
容器内的内存显示有问题是因为容器隔离的不彻底,在文件中查看:
[root@server2 docker]# pwd
/sys/fs/cgroup/memory/docker
[root@server2 docker]# cat 0b526ba6a02507fe879a1fb9edf045a9a6350eda6575744a05b643fb7a4175f8/memory.limit_in_bytes
209715200
[root@server2 docker]# cat 0b526ba6a02507fe879a1fb9edf045a9a6350eda6575744a05b643fb7a4175f8/memory.memsw.limit_in_bytes
209715200
可以看出设置成功。
比如:docker run -m 1G --memory-swap 3G ubuntu
,该容器能使用的内存大小为 1G,能使用的 swap 分区大小为 2G。容器内的进程能申请到的总内存大小为 3G。
- 设置-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限制的内存大小。**
例如:
docker run -it -m 500M --memory-reservation 200M ubuntu /bin/bash
如果容器使用了大于 200M 但小于 500M 内存时,下次系统的内存回收会尝试将容器的内存锁紧到 200M 以下。
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:
docker run -it -m 100M --oom-kill-disable ubuntu /bin/bash
是正确的使用方法。
而下面这个容器没设置内存限制,却禁用了 OOM killer 是非常危险的:
docker run -it --oom-kill-disable ubuntu:16.04 /bin/bash
容器没用内存限制,可能或导致系统无内存可用,并尝试时杀死系统进程来获取更多可用内存。
一般一个容器只有一个进程,这个唯一进程被杀死,容器也就被杀死了。我们可以通过–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,因为核心内存的变化也会导致用户计数器的变化,容器核心内存和用户内存都会触发回收行为。这种配置可以让管理员以一种统一的视图看待内存。对想跟踪核心内存使用情况的用户也是有用的。
例如:
docker run -it -m 500M --kernel-memory 50M ubuntu:16.04 /bin/bash
容器中的进程最多能使用 500M 内存,在这 500M 中,最多只有 50M 核心内存。
docker run -it --kernel-memory 50M ubuntu:16.04 /bin/bash
没用设置用户内存限制,所以容器中的进程可以使用尽可能多的内存,但是最多能使用 50M 核心内存。
Swappiness
默认情况下,容器的内核可以交换出一定比例的匿名页。–memory-swappiness就是用来设置这个比例的。–memory-swappiness可以设置为从 0 到 100。0 表示关闭匿名页面交换。100 表示所有的匿名页都可以交换。默认情况下,如果不适用–memory-swappiness,则该值从父进程继承而来。
例如:
docker run -it --memory-swappiness=0 ubuntu:16.04 /bin/bash
将–memory-swappiness设置为 0 可以保持容器的工作集,避免交换代理的性能损失。
五、磁盘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 |
注意:目前的block IO仅对direct IO(不使用缓存)有效。
磁盘IO配额控制示例
device-write-bps
使用下面的命令创建容器,并执行命令验证写速度的限制。
通过dd来验证写速度,输出如下所示:
[root@server2 docker]# docker run --device-write-bps /dev/sda:10M -it ubuntu
root@cf6c1f3977ae:/# dd if=/dev/zero of=bigfile bs=1M count=100 oflag=direct
100+0 records in
100+0 records out
104857600 bytes (105 MB, 100 MiB) copied, 9.95123 s, 10.5 MB/s
可以看到容器的写磁盘速度被成功地限制到了10MB/s。device-read-bps等其他磁盘IO限制参数可以使用类似的方式进行验证。
六、容器空间大小限制
在docker使用devicemapper作为存储驱动时,默认每个容器和镜像的最大大小为10G。如果需要调整,可以在daemon启动参数中,使用dm.basesize来指定,但需要注意的是,修改这个值,不仅仅需要重启docker daemon服务,还会导致宿主机上的所有本地镜像和容器都被清理掉。
使用aufs或者overlay等其他存储驱动时,没有这个限制。
七、docker 安全加固
内存显示修复
问题的提出
如图在容器内查看内存时显示的是宿主机的内存信息,显示不准确
要修复这个问题需要安装以下软件:
[root@server2 ~]# yum install lxcfs-2.0.5-3.el7.centos.x86_64.rpm -y
默认的数据目录:/var/lib/lxcfs/
运行软件:
[root@server2 ~]# lxcfs /var/lib/lxcfs/ &
[1] 5751
[root@server2 ~]# hierarchies:
0: fd: 5: blkio
1: fd: 6: devices
2: fd: 7: cpuset
3: fd: 8: memory
4: fd: 9: hugetlb
5: fd: 10: net_prio,net_cls
6: fd: 11: perf_event
7: fd: 12: freezer
8: fd: 13: pids
9: fd: 14: cpuacct,cpu
10: fd: 15: name=systemd
此时运行容器,运行时将lxcfs数据目录下的内容挂载到容器内:
[root@server2 proc]# docker run -it -m 256m \
> -v /var/lib/lxcfs/proc/cpuinfo :/proc/cpuinfo:rw \
> -v /var/lib/lxcfs/proc/diskstats:^C
[root@server2 proc]# docker run -it -m 256m \
> -v /var/lib/lxcfs/proc/cpuinfo:/proc/cpuinfo:rw \
> -v /var/lib/lxcfs/proc/diskstats:/proc/diskstats:rw \
> -v /var/lib/lxcfs/proc/meminfo:/proc/meminfo:rw \
> -v /var/lib/lxcfs/proc/swaps:/proc/swaps:rw \
> -v /var/lib/lxcfs/proc/uptime:/proc/uptime:rw \
> ubuntu
此时查看内存吸纳是准确:
容器内用户权限提升
默认在运行容器时容器内的用户并不是真正的root用户,比如不能进行网络设备,磁盘设备的管理,可以在运行容器时加 --privileged=true
参数以提升用户权限:
[root@server2 ~]# docker run --privileged=true -it --rm ubuntu
这个权限比较大,接近于宿主机的root用户,为了防止用户权限的滥用,需要增加权限,只提供给容器必须的权限。此时Docker提供了权限白名单的机制,使用–cap-add提娜加必要的权限。
可以查看capabilities手册地址:http://man7.org/linux/man-pages/man7/capabilities.7.html
示例:
只添加网络权限:
[root@server2 ~]# docker run -it --cap-add=NET_ADMIN ubuntu
root@8f22f328d90b:/# fdisk -l
root@8f22f328d90b:/# exit
可以看出上面的容器没有磁盘管理的权限。
[root@server2 ~]# docker run -it --cap-add=NET_ADMIN busyboxplus
上图中可以看出容器具有网络管理的权限。