在之前的 docker run 介绍过程中,并没有对使用的资源做任何的限制,这是有风险的。docker本身就提供了一些参数进行对资源的限制,其原理是通过cgroups来做限制的。Linux Cgroups的全称是Linux Control Group。它最主要的作⽤,就是限制⼀个进程组能够使⽤的资源上限,包括CPU、内存、磁盘、⽹络带宽等等。Linux Cgroups就是Linux内核中⽤来为进程设置资源限制的⼀个重要功能。
CPU
前戏
在Linux中,Cgroups给⽤户暴露出来的操作接⼝是⽂件系统,即它以⽂件和⽬录的⽅式组织在操作系统的/sys/fs/cgroup路径下1
2
3
4
5
6
7
8
9
10
11
12[root@master ~]# mount -t cgroup
cgroup on /sys/fs/cgroup/systemd type cgroup (rw,nosuid,nodev,noexec,relatime,xattr,release_agent=/usr/lib/systemd/systemd-cgroups-agent,name=systemd)
cgroup on /sys/fs/cgroup/pids type cgroup (rw,nosuid,nodev,noexec,relatime,pids)
cgroup on /sys/fs/cgroup/freezer type cgroup (rw,nosuid,nodev,noexec,relatime,freezer)
cgroup on /sys/fs/cgroup/devices type cgroup (rw,nosuid,nodev,noexec,relatime,devices)
cgroup on /sys/fs/cgroup/cpu,cpuacct type cgroup (rw,nosuid,nodev,noexec,relatime,cpu,cpuacct)
cgroup on /sys/fs/cgroup/net_cls,net_prio type cgroup (rw,nosuid,nodev,noexec,relatime,net_cls,net_prio)
cgroup on /sys/fs/cgroup/perf_event type cgroup (rw,nosuid,nodev,noexec,relatime,perf_event)
cgroup on /sys/fs/cgroup/cpuset type cgroup (rw,nosuid,nodev,noexec,relatime,cpuset)
cgroup on /sys/fs/cgroup/hugetlb type cgroup (rw,nosuid,nodev,noexec,relatime,hugetlb)
cgroup on /sys/fs/cgroup/memory type cgroup (rw,nosuid,nodev,noexec,relatime,memory)
cgroup on /sys/fs/cgroup/blkio type cgroup (rw,nosuid,nodev,noexec,relatime,blkio)
在/sys/fs/cgroup下⾯有很多诸如cpuset、cpu、 memory这样的⼦⽬录,也叫⼦系统。这些都是可以被Cgroups进⾏限制的资源种类。⽽在⼦系统对应的资源种类下,你就可以看到该类资源具体可以被限制的⽅法。
如果熟悉Linux CPU管理的话,你就会在它的输出⾥注意到cfs_period和cfs_quota这样的关键词。这两个参数需要组合使⽤,可以⽤来限制进程在⻓度为cfs_period的⼀段时间内,只能被分配到总量为cfs_quota的CPU时间。
我们现在进⼊/sys/fs/cgroup/cpu⽬录下,mkdir container目录之后,⾃动⽣成该⼦系统对应的资源限制⽂件。
1 | [root@master cpu]# cd /sys/fs/cgroup/cpu |
然后while : ; do : ; done &运行这个死循环脚本。可以把计算机的CPU吃到100%。如下CPU的占用:
1 | [root@master test]# while : ; do : ; done & |
向container组⾥的cfs_quota⽂件写⼊50 ms(50000 us)echo 50000 >/sys/fs/cgroup/cpu/container/cpu.cfs_quota_us,它意味着在每100 ms的时间⾥,被该控制组限制的进程只能使⽤20 ms的CPU时间,也就是说这个进程只能使⽤到50%的CPU带宽。
1 | [root@master container]# echo 100000 >cpu.cfs_period_us |
把被限制的进程的PID写⼊container组⾥的tasks⽂件/sys/fs/cgroup/cpu/container/tasks,上⾯的设置就会对该进程⽣效了。可以看到 %CPU 被限制在40%。
限制可用的 CPU 个数
以上使用 cpu.cfs_period_us cpu.cfs_quota_us 来设置比较麻烦,在 docker 1.13 及更高的版本上,能够很容易的限制容器可以使用的主机 CPU 个数。只需要通过 --cpus 选项指定容器可以使用的 CPU 个数就可以了,并且还可以指定如 1.5 之类的小数。运行 docker run -it --cpus=1 --name stress --rm progrium/stress --cpu 2 表示限制容器最多使用1个CPU资源,但使用stress压测时使用2个CPU进行压测。
使用 docker stats 是可以看到CPU占用100%,但是使用top去查看的话,会发现会均分在2个CPU核上。
1 | [root@master ~]# docker stats stress |
所以这个参数是限制不了的,还是会多使用CPU核数的。
固定使用CPU核数
使用 --cpuset-cpus 是可以固定将容器运行在哪个CPU上面。所以此方法非常实用。
1 | # 在第一个shell窗口上运行以下命令: |
可以看到2个stress进程都被控制到CPU 0上面了。
设置CPU权重
所谓CPU权重,指的是在CPU出现资源紧张的情况下,设置CPU权重让不同的容器获取到不同的CPU使用率。--cpu-shares 选项用来设置 CPU 权重,它的默认值为 1024。我们可以把它设置为 2 表示很低的权重,但是设置为 0 表示使用默认值 1024。值越小权重越小。
启动2个容器,stress01绑定到CPU1上,权重为10;stress02也绑定到CPU1上,权重为100,这样出现竞争时,stress02占用了90%的CPU,而stress01占用了10%的CPU,合情合理。
1 | [root@master ~]# docker run -itd --cpuset-cpus 1 --name stress01 --cpu-shares 10 progrium/stress --cpu 1 |
内存
默认情况下,docker 并没有对容器内存进行限制,也就是说容器可以使用主机提供的所有内存。这当然是非常危险的事情,如果某个容器运行了恶意的内存消耗软件,或者代码有内存泄露,很可能会导致主机内存耗尽,因此导致服务不可用。对于这种情况,docker 会设置 docker daemon 的 OOM(out of memory) 值,使其在内存不足的时候被杀死的优先级降低。另外,就是你可以为每个容器设置内存使用的上限,一旦超过这个上限,容器会被杀死,而不是耗尽主机的内存。
内存相关参数
在 docker 启动参数中,和内存限制有关的包括(参数的值一般是内存大小,也就是一个正数,后面跟着内存单位 b、k、m、g,分别对应 bytes、KB、MB、和 GB):
-m --memory:容器能使用的最大内存大小,最小值为 4m--memory-swap:容器能够使用的 swap 大小--memory-swappiness:默认情况下,主机可以把容器使用的匿名页(anonymous page)swap 出来,你可以设置一个 0-100 之间的值,代表允许 swap 出来的比例--memory-reservation:设置一个内存使用的 soft limit,如果 docker 发现主机内存不足,会执行 OOM 操作。这个值必须小于--memory设置的值--kernel-memory:容器能够使用的 kernel memory 大小,最小值为 4m。--oom-kill-disable:是否运行 OOM 的时候杀死容器。只有设置了-m,才可以把这个选项设置为 false,否则容器会耗尽主机内存,而且导致主机应用被杀死
关于 --memory-swap 的设置必须解释一下,--memory-swap 必须在 --memory 也配置的情况下才能有用。
- 如果
--memory-swap的值大于--memory,那么容器能使用的总内存(内存 + swap)为--memory-swap的值。能使用的 swap 值为--memory-swap减去--memory的值 - 如果
--memory-swap为 0,那么容器能使用两倍于内存的 swap 大小,如果--memory对应的值是200M,那么容器可以使用400Mswap - 如果
--memory-swap的值为 -1,那么不限制 swap 的使用,也就是说主机有多少 swap,容器都可以使用
内存实验测试
测试一:不限制swap内存
思路是使用stress工具来做测试,直接运行以下命令:
1 | [fdm@localhost ~]$ docker run -it -m 300M --memory-swap -1 --name stress --rm --entrypoint /bin/bash progrium/stress |
上面的 docker run 命令中通过 -m 选项限制容器使用的内存上限为 300M。同时设置 memory-swap 值为 -1,表示可以使用的 swap 空间使用不受限制(宿主机有多少 swap 容器就可以使用多少)。
stress --vm 1 --vm-bytes 500M 表示运行运行一个进程,申请500M的内存。
我们有2种方法进行内存的查看。一是使用docker stats 查看:
1 | [fdm@localhost ~]$ docker stats stress |
可以看到内存成功限制在300M了。
另外还可以在宿主机上面查看。先查出stress运行的PID,再使用top查看。
1 | [fdm@localhost ~]$ ps auxwf |grep stress |grep -v grep |
由top的输出可以看到,VIRT是500M,就是虚拟内存申请了500M,但实际分配了300M左右的内存。
测试二:swap设置为0
由上参数介绍所知,--memory-swap 0表示2倍的memory大小,即申请300M内存,合计不能超过600M,如下实验,申请了600M内存,直接被系统内核OOM kill了。
1 | [fdm@localhost ~]$ docker run -it -m 300M --memory-swap 0 --name stress --rm --entrypoint /bin/bash progrium/stress |
messages日志就记录了oom_kill_process:
1 | [root@localhost ~]# cat /var/log/messages |grep kill |
测试三:限制swap内存
当 --memory-swap 有值时,其内存总量不能超过这个值,所以就是说使用swap的总量是 memory-swap - memory 的值。如果是设置了 -m 300M --memory-swap 300M ,这样内存总量就是300M,超过这个值就会被直接OOM了。
1 | [fdm@localhost ~]$ docker run -it -m 300M --memory-swap 300M --name stress --rm --entrypoint /bin/bash progrium/stress |
内存信息的cgroups文件
对于 docker 来说,它的内存限制是存放在 cgroups 文件系统的。对于某个容器,你可以在 sys/fs/cgroup/memory/docker/<container_id> 目录下看到容器内存相关的文件。
1 | [root@localhost ~]# ls /sys/fs/cgroup/memory/docker/0251c0db55f692c0bdb6de92eacedd57678cf7ba06d5fde6abf9874d34bbdeb5/ |
硬盘
对于磁盘来说,考量的参数是容量和读写速度,因此对容器的磁盘限制也应该从这两个维度出发。目前 docker 支持对磁盘的读写速度进行限制,但是并没有方法能限制容器能使用的磁盘容量。
block IO 权重
默认情况下,所有容器能平等地读写磁盘,可以通过设置 --blkio-weight 参数来改变容器 block IO 的优先级。--blkio-weight 与 --cpu-shares 类似,设置的是相对权重值,默认为 500。
限制读写速度
除了权重之外,docker 还允许你直接限制磁盘的读写速率,对应的参数有:
--device-read-bps:磁盘每秒最多可以读多少比特(bytes)--device-write-bps:磁盘每秒最多可以写多少比特(bytes)
上面两个参数的值都是磁盘以及对应的速率,格式为 <device-path>:<limit>[unit],device-path表示磁盘所在的位置,限制 limit 为正整数,单位可以是 kb、mb 和 gb。
另外两个参数可以限制磁盘读写频率(每秒能执行多少次读写操作):
--device-read-iops:磁盘每秒最多可以执行多少 IO 读操作--device-write-iops:磁盘每秒最多可以执行多少 IO 写操作
上面两个参数的值都是磁盘以及对应的 IO 上限,格式为 <device-path>:<limit>,limit 为正整数,表示磁盘 IO 上限数。
如下测试,限制读的速度不能超过1MB。
1 | [root@master ~]# docker run -it --rm --device /dev/sda:/dev/sda --device-read-bps /dev/sda:1MB ubuntu bash |
使用iops也是类似,不过bs要设置小块,才能看出效果出来。
1 | [root@master ~]# docker run -it --rm --device /dev/sda:/dev/sda --device-read-iops /dev/sda:100 ubuntu bash |
这是docker原理之一,使用cgroup进行资源隔离。
lxcfs
简介
虽然docker使用了namespace以及cgroup来限制与隔离资源使用情况,但是容器中的top/free/df等命令,展示的状态信息是从/proc目录中的相关文件里读取出来的:
1 | /proc/cpuinfo |
就是说在容器内部proc文件系统中可以看到Host宿主机上的proc信息(如:meminfo, cpuinfo,stat, uptime等)。而LXCFS,FUSE filesystem for LXC是一个常驻服务,它启动以后会在指定目录中自行维护与上面列出的/proc目录中的文件同名的文件,容器从lxcfs维护的/proc文件中读取数据时,得到的是容器的状态数据,而不是整个宿主机的状态。
安装方法
有2种方法,一是通过官方 https://github.com/lxc/lxcfs/releases 编译安装,编译前需要安装 fuse-devel。
二是通过rpm包的方法运行安装,目前(20191019)的最新版本为3.1.2,下载之:
1 | wget https://copr-be.cloud.fedoraproject.org/results/ganto/lxc3/epel-7-x86_64/01041891-lxcfs/lxcfs-3.1.2-0.2.el7.x86_64.rpm |
运行lxcfs之后,会在 /var/lib/lxcfs 目录下生成一些系统文件
1 | drwxr-xr-x. 2 root root 0 Oct 19 10:18 cgroup |
使用方法
启动一个容器,用lxcfs维护的/proc文件替换容器中的/proc文件,容器内存设置为256M:
1 | docker run -it -m 256m \ |
再查看一个内存,已经被限制到256M了。
1 | root@169717adb127:/# free -m |
参考资料
https://docs.docker.com/config/containers/resource_constraints/