截止到目前为止我们已经深入学习了容器引擎的进程、内存、存储管理,包括对每种资源的限制方法,那本节作为了解,我们就来一起总结一下cgroup机制,查漏补缺,因为后期我们使用k8s,更多的会基于k8s来管理容器,所以本章节内容作为了解即可
一、Cgroup资源配置方法
Docker通过 Cgroup 来控制容器使用的资源配额,包括 CPU、内存、磁盘三大方面, 基本覆盖了常见的资源配额和使用量控制。
Cgroup是Control Groups的缩写,是Linux内核提供的一种可以限制、记录、隔离进程组所使用的物理资源(如CPU、内存、磁盘IO等等)的机制,被LXC、docker等很多项目用于实现进程资源控制。Cgroup本身是提供将进程进行分组化管理的功能和接口的基础结构,I/O或内存的分配控制等具体的资源管理是通过该功能来实现的。这些具体的资源管理功能称为Cgroup子系统,有以下几大子系统实现:
- blkio:设置限制每个块设备的输入输出控制。例如:磁盘,光盘以及usb等待。
- CPU:使用调度程序为cgroup任务提供CPU资源报告。
- cpuacct:产生cgroup任务的CPU资源报告。
- cpuset:如果是多核心的CPU,这个子系统会为cgroup任务分配单独的CPU和内存。
- devices:允许或拒绝cgroup任务对设备的访问。
- freezer:暂停和恢复cgroup任务。
- memiry:设置每个cgroup的内存限制以及产生内存资源报告。
- net_cls:标记每个网络以供cgroup方便使用。
- ns:命名空间子系统。
- perf_event:增加对每个group的监控跟踪的能力,可以监测属于某个特定的group的所有线程以及运行在特定CPU上的线程。
下面开始利用stress压力测试工具来测试CPU和内存适用状况
二、构建stress镜像
使用Dockerfile来创建一个基于Centos的stress工具镜像,用于后续的cpu与内存限制测试
docker build -t centos:stress .
三、CPU Cgroup限制
3.1 概述
Docker 的资源限制和隔离完全基于 Linux cgroups。对 CPU 资源的限制方式也和 cgroups 相同。Docker 提供的 CPU 资源限制选项可以在多核系统上限制容器能利用哪些 vCPU。而对容器最多能使用的 CPU 时间有两种限制方式:一是有多个 CPU 密集型的容器竞争 CPU 时,设置各个容器能使用的 CPU 时间相对比例。二是以绝对的方式设置容器在每个调度周期内最多能使用的 CPU 时间。
3.2 CPU 限制相关参数
docker run命令和 CPU 限制相关的所有选项如下:
其中–cpuset-cpus用于设置容器可以使用的 vCPU 核。
-c,–cpu-shares用于设置多个容器竞争 CPU 时,各个容器相对能分配到的 CPU 时间比例。
–cpu-period和–cpu-quata用于绝对设置容器能使用 CPU 时间。
–cpuset-mems暂用不上,这里不谈。
3.3 CPU 集
对多核 CPU 的服务器,Docker 还可以控制容器运行使用哪些 CPU 内核,即使用–cpuset-cpus 参数。
这对具有多 CPU 的服务器尤其有用,可以对需要高性能计算的容器进行性能最优的配置。
在 NUMA 系统上,我们可以设置容器可以使用的内存节点。
3.4 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% 时间。
案例展示:
- 说明:默认情况下,每个 Docker容器的CPU份额都是1024。单独一个容器的份额是没有意义的。只有在同时运行多个容器时,容器的 CPU 加权的效果才能体现出来。
- 例如,两个容 器 A、B 的 CPU 份额分别为 1000 和 500,在CPU进行时间片分配的时候,容器A比容器B多一倍的机会获得 CPU 的时间片。但分配的结果取决于当时主机和其他容器的运行状态, 实际上也无法保证容器 A一定能获得CPU时间片。比如容器A的进程一直是空闲的,那么容器B是可以获取比容器A更多的CPU时间片的。极端情况下,例如主机上只运行了一个容器,即使它的 CPU 份额只有 50,它也可以独占整个主机的CPU资源。
-
Cgroups 只在容器分配的资源紧缺时,即在需要对容器使用的资源进行限制时,才会生效。因此,无法单纯根据某个容器的CPU份额来确定有多少CPU资源分配给它,资源分配 结果取决于同时运行的其他容器的CPU分配和容器中进程运行情况。可以通过 cpu share 可以设置容器使用 CPU 的优先级,比如启动了两个容器及运行查看 CPU 使用百分比。
容器1
容器2
进入容器使用top对比两个容器的CPU%,比例是1:2
3.5、CPU资源的绝对限制
Linux 通过 CFS(Completely Fair Scheduler,完全公平调度器)来调度各个进程对 CPU 的使用。CFS 默认的调度周期是 100ms。
关于 CFS 的更多信息,参考CFS documentation on bandwidth limiting。
Docker 提供了–cpu-period、–cpu-quota 两个参数控制容器可以分配到的 CPU 时钟周期,两者一般配合使用。
-
–cpu-period 设置当前容器进程的调度周期
-
–cpu-quota 设置在每个周期内容器能使用的 CPU 时间
CFS 周期的有效范围是 1ms~1s
对应的–cpu-period的数值范围是 1000~1000000。
而容器的 CPU 配额必须不小于 1ms,即–cpu-quota的值必须 >= 1000。可以看出这两个选项的单位都是 us。
正确的理解“绝对”
与 –cpu-shares 不同的是,上述配置是指定一个绝对值,容器对 CPU 资源的使用绝对不会超过配置的值。
也就是说
前面我们用–cpu-quota设置容器在一个调度周期内能使用的 CPU 时间时实际上设置的是一个上限。并不是说容器一定会使用这么长的 CPU 时间。比如,我们先启动一个容器,将其绑定到 cpu 1 上执行。给其–cpu-quota和–cpu-period都设置为 50000。
调度周期为 50000,容器在每个周期内最多能使用 50000 cpu 时间。
再用docker stats test01可以观察到该容器对 CPU 的使用率在100%左右。然后,我们再以同样的参数启动另一个容器。
再用docker stats test01 test02可以观察到这两个容器,每个容器对 cpu 的使用率在 50% 左右。说明容器并没有在每个周期内使用 50000 的 cpu 时间。
使用docker stop test02命令结束第二个容器,再加一个参数-c 2048启动它:
再用docker stats test01命令可以观察到第一个容器的 CPU 使用率在 33% 左右,第二个容器的 CPU 使用率在 66% 左右。因为第二个容器的共享值–cpu-shares 是 2048,第一个容器的默认共享值–cpu-shares 是 1024,所以第二个容器在每个周期内能使用的 CPU 时间是第一个容器的两倍。
3.6、CPU 配额控制参数的混合使用
通过 cpuset-cpus 参数指定容器 A 使用 CPU 内核 0,容器 B 只是用 CPU 内核 1。
在主机上只有这两个容器使用对应 CPU 内核的情况,它们各自占用全部的内核资源,cpu-shares 没有明显效果。
cpuset-cpus、cpuset-mems 参数只在多核、多内存节点上的服务器上有效,并且必须与实际的物理配置匹配,否则也无法达到资源控制的目的。
在系统具有多个 CPU 内核的情况下,需要通过 cpuset-cpus 参数为设置容器 CPU 内核才能方便地进行测试。
宿主系统修改为4核心CPU
总结:上面的 centos:stress 镜像安装了 stress 工具,用来测试 CPU 和内存的负载。通过 在两个容器上分别执行 stress -c 1 命令,将会给系统一个随机负载,产生 1 个进程。这 个进程都反复不停的计算由 rand() 产生随机数的平方根,直到资源耗尽。 观察到宿主机上的 CPU 使用率,第三个内核的使用率接近 100%, 并且一批进程的 CPU 使用率明显存在 2:1 的使用比例的对比。
四、memory cgroup限制
4.1 概述
Docker 提供的内存限制功能有以下几点:
容器能使用的内存和交换分区大小。
容器的核心内存大小。
容器虚拟内存的交换行为。
容器内存的软性限制。
是否杀死占用过多内存的容器。
容器被杀死的优先级
一般情况下,达到内存限制的容器过段时间后就会被系统杀死。
4.2 内存限制相关的参数
执行docker run命令时能使用的和内存限制相关的所有选项如下。
4.3 用户内存限制
与操作系统类似,容器可使用的内存包括两部分:物理内存和 Swap。
Docker 通过下面两组参数来控制容器内存的使用量。
- -m 或 –memory:设置内存的使用限额,例如 100M、1024M。需知该参数的最小为 4 M
- –memory-swap:设置 内存+swap 的总大小限制,所以–memory-swap必须比-m,–memory大。
例如:
执行如下命令允许该容器总共可用内存为300M,其中物理内存最多使用 200M 的内存。swap分区最多 100M。
如果让工作线程分配的内存超过 300M,分配的内存超过限额,stress 线程报错,容器退出。
补充:
再次重申两条规则:
- -m 或 –memory:设置内存的使用限额,例如 100M、1024M。需知该参数的最小为 4 M
- –memory-swap:设置 内存+swap 的总大小限制,所以–memory-swap必须比-m,–memory大。
在这两条规则下,一般有四种设置方式。
-
不设置
如果不设置-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:16.04,该容器能使用的内存大小为 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 分区大小。
比如$ docker run -m 1G –memory-swap 3G ubuntu:16.04,该容器能使用的内存大小为 1G,能使用的 swap 分区大小为 2G。容器内的进程能申请到的总内存大小为 3G。 -
设置-m,–memory=a,–memory-swap=-1
给-m参数设置一个正常值,而给–memory-swap设置成 -1。这种情况表示限制容器能使用的内存大小为 a,而不限制容器能使用的 swap 分区大小。
这时候,容器内进程能申请到的内存大小为 a + 宿主机的 swap 大小。
4.4 Memory reservation
这种 memory reservation 机制不知道怎么翻译比较形象。Memory reservation 是一种软性限制,用于节制容器内存使用。给–memory-reservation设置一个比-m小的值后,虽然容器最多可以使用-m使用的内存大小,但在宿主机内存资源紧张时,在系统的下次内存回收时,系统会回收容器的部分内存页,强迫容器的内存占用回到–memory-reservation设置的值大小。
没有设置时(默认情况下)–memory-reservation的值和-m的限定的值相同。将它设置为 0 会设置的比-m的参数大 等同于没有设置。
Memory reservation 是一种软性机制,它不保证任何时刻容器使用的内存不会超过–memory-reservation限定的值,它只是确保容器不会长时间占用超过–memory-reservation限制的内存大小。
例如:
如果容器使用了大于 200M 但小于 500M 内存时,下次系统的内存回收会尝试将容器的内存锁紧到 200M 以下。
例如:
容器可以使用尽可能多的内存。–memory-reservation确保容器不会长时间占用太多内存。
4.5 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:
是正确的使用方法。
而下面这个容器没设置内存限制,却禁用了 OOM killer 是非常危险的:
容器没用内存限制,可能或导致系统无内存可用,并尝试时杀死系统进程来获取更多可用内存。
一般一个容器只有一个进程,这个唯一进程被杀死,容器也就被杀死了。我们可以通过–oom-score-adj选项来设置在系统内存不够时,容器被杀死的优先级。负值更教不可能被杀死,而正值更有可能被杀死。
4.6 核心内存
核心内存和用户内存不同的地方在于核心内存不能被交换出。不能交换出去的特性使得容器可以通过消耗太多内存来堵塞一些系统服务。核心内存包括:
- stack pages(栈页面)
- slab pages
- socket memory pressure
- tcp memory pressure
可以通过设置核心内存限制来约束这些内存。例如,每个进程都要消耗一些栈页面,通过限制核心内存,可以在核心内存使用过多时阻止新进程被创建。
核心内存和用户内存并不是独立的,必须在用户内存限制的上下文中限制核心内存。
假设用户内存的限制值为 U,核心内存的限制值为 K。有三种可能地限制核心内存的方式:
- U != 0,不限制核心内存。这是默认的标准设置方式
- K < U,核心内存时用户内存的子集。这种设置在部署时,每个 cgroup 的内存总量被过度使用。过度使用核心内存限制是绝不推荐的,因为系统还是会用完不能回收的内存。在这种情况下,你可以设置 K,这样 groups 的总数就不会超过总内存了。然后,根据系统服务的质量自有地设置 U。
- K > U,因为核心内存的变化也会导致用户计数器的变化,容器核心内存和用户内存都会触发回收行为。这种配置可以让管理员以一种统一的视图看待内存。对想跟踪核心内存使用情况的用户也是有用的。
例如:容器中的进程最多能使用 500M 内存,在这 500M 中,最多只有 50M 核心内存。
没用设置用户内存限制,所以容器中的进程可以使用尽可能多的内存,但是最多能使用 50M 核心内存。
4.7 Swappiness
默认情况下,容器的内核可以交换出一定比例的匿名页。–memory-swappiness就是用来设置这个比例的。–memory-swappiness可以设置为从 0 到 100。0 表示关闭匿名页面交换。100 表示所有的匿名页都可以交换。默认情况下,如果不适用–memory-swappiness,则该值从父进程继承而来。
例如:
将–memory-swappiness设置为 0 可以保持容器的工作集,避免交换代理的性能损失。
五、Block IO的限制
默认情况下,所有容器能平等地读写磁盘,可以通过设置–blkio-weight 参数来改变 容器 block IO 的优先级。
–blkio-weight 与 –cpu-shares 类似,设置的是相对权重值,默认为 500。
在下面 的例子中,容器 A 读写磁盘的带宽是容器 B 的两倍。
六、bps和iops的限制
bps 是 byte per second,每秒读写的数据量。
iops 是 io per second,每秒 IO 的次数。
可通过以下参数控制容器的 bps 和 iops:
- –device-read-bps,限制读某个设备的 bps。
- –device-write-bps,限制写某个设备的 bps。
- –device-read-iops,限制读某个设备的 iops。
- –device-write-iops,限制写某个设备的 iops。
下面的示例是限制容器写 /dev/sda 的速率为 5 MB/s。
通过 dd 命令测试在容器中写磁盘的速度。因为容器的文件系统是在 host /dev/sda 上 的,在容器中写文件相当于对 host /dev/sda 进行写操作。另外,oflag=direct 指定用 direct IO 方式写文件,这样 –device-write-bps 才能生效。
结果表明限速 5MB/s 左右。作为对比测试,如果不限速,结果如下。