分享免费的编程资源和教程

网站首页 > 技术教程 正文

从理论到实战,理解cgroups-CPU篇

goqiw 2024-10-11 15:06:13 技术教程 22 ℃ 0 评论

在容器世界里,有两个非常重要的核心技术:Namespace和Cgroups。其中Namespace用于隔离,而Cgroups用于资源限制。今天我们通过理论到实战案例来了解怎么通过cgroups技术限制CPU的使用。

什么是cgroup?


Linux Cgroups 的全称是 Linux Control Group。它最主要的作用,就是限制一个进程组能够使用的资源上限,包括 CPU、内存、磁盘、网络带宽等等。

在 Linux 中,Cgroups 给用户暴露出来的操作接口是文件系统,即它以文件和目录的方式组织在操作系统的/sys/fs/cgroup路径下。我们以CentOS 7为例,看一下cgroups有哪些,其他系统可能挂载的数量和参数会有部分不同,但不妨碍本文的阅读。

[root@centos7 ~]# 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/cpu,cpuacct type cgroup (rw,nosuid,nodev,noexec,relatime,cpu,cpuacct)
cgroup on /sys/fs/cgroup/devices type cgroup (rw,nosuid,nodev,noexec,relatime,devices)
cgroup on /sys/fs/cgroup/hugetlb type cgroup (rw,nosuid,nodev,noexec,relatime,hugetlb)
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/cpuset type cgroup (rw,nosuid,nodev,noexec,relatime,cpuset)
cgroup on /sys/fs/cgroup/blkio type cgroup (rw,nosuid,nodev,noexec,relatime,blkio)
cgroup on /sys/fs/cgroup/freezer type cgroup (rw,nosuid,nodev,noexec,relatime,freezer)
cgroup on /sys/fs/cgroup/pids type cgroup (rw,nosuid,nodev,noexec,relatime,pids)
cgroup on /sys/fs/cgroup/perf_event type cgroup (rw,nosuid,nodev,noexec,relatime,perf_event)
cgroup on /sys/fs/cgroup/memory type cgroup (rw,nosuid,nodev,noexec,relatime,memory)


可以看到,在/sys/fs/cgroup下面有很多诸如cpusetcpumemory这样的子目录,也叫子系统。这些都是我这台机器当前可以被 Cgroups 进行限制的资源种类。而在子系统对应的资源种类下,你就可以看到该类资源具体可以被限制的方法。


Red Hat Enterprise Linux 7 中可用的资源控制器包括

  • blkio —— 对输入 ∕ 输出访问存取块设备设定权限;
  • cpu —— 使用 CPU 调度程序让 cgroup 的任务可以存取 CPU。它与 cpuacct 管控器一起挂载在同一 mount 上;(本文的案例)
  • cpuacct —— 自动生成 cgroup 中任务占用 CPU 资源的报告。它与 cpu 管控器一起挂载在同一 mount 上;
  • cpuset —— 给 cgroup 中的任务分配独立 CPU(在多芯系统中)和内存节点;
  • devices —— 允许或禁止 cgroup 中的任务存取设备;
  • freezer —— 暂停或恢复 cgroup 中的任务;
  • memory —— 对 cgroup 中的任务可用内存做出限制,并且自动生成任务占用内存资源报告;
  • net_cls —— 使用等级识别符(classid)标记网络数据包,这让 Linux 流量控制器(tc 指令)可以识别来自特定 cgroup 任务的数据包;
  • perf_event —— 允许使用 perf 工具来监控 cgroup;
  • hugetlb —— 允许使用大篇幅的虚拟内存页,并且给这些内存页强制设定可用资源量。

比如,对于今天介绍的 CPU 子系统来说,我们就可以看到如下几个参数文件:

[root@centos7 ~]# ls  /sys/fs/cgroup/cpu/
cgroup.clone_children  cgroup.sane_behavior  cpuacct.usage         cpu.cfs_period_us  cpu.rt_period_us   cpu.shares  notify_on_release  tasks
cgroup.procs           cpuacct.stat          cpuacct.usage_percpu  cpu.cfs_quota_us   cpu.rt_runtime_us  cpu.stat    release_agent


CPU cgroup的调度算法有哪些?


cpu 子系统可以调度 cgroup 对 CPU 的资源使用量。可用以下两个调度程序来管理对 CPU 资源的获取:

  • 完全公平调度算法CFS(Completely Fair Scheduler) — 一个比例分配调度程序,可根据任务优先级 ∕ 权重或 cgroup 分得的份额,在任务群组(cgroups)间按比例分配 CPU 时间(CPU 带宽)。
  • 实时调度算法RT(Real-Time) — 一个任务调度程序,可对实时任务使用 CPU 的时间进行限定。


我们在日常使用的程序大部分都不是实时调度的进程,因此本文着重介绍CFS调度算法,关于CFS,我们先来学习一下CFS相关的配置和参数。

  • cpu.cfs_period_us它是 CFS 算法的一个调度周期,单位为微秒,默认值是 100000,换算成以 microseconds 为单位,也就 100ms,我们通常不会直接调整这个周期值,而是调整下面将要介绍的这个参数。
  • cpu.cfs_quota_us它表示 CFS 算法中,在一个调度周期里这个控制组被允许的运行时间,单位为微秒,比如这个值为 50000 时,就是 50ms。如果将这个值设置为-1,这表示 cgroup 不需要遵循任何 CPU 时间限制,这也是cgroup的默认值。如果用这个值去除以调度周期(也就是cpu.cfs_period_us),50ms/100ms = 0.5,这样这个控制组被允许使用的 CPU 最大配额就是 0.5 个 CPU。从这里能够看出,cpu.cfs_quota_us 是一个绝对值。如果这个值是 200000,也就是 200ms,那么它除以 period,也就是 200ms/100ms=2。你看,结果超过了 1 个 CPU,这就意味着这时控制组需要 2 个 CPU 的资源配额。

另外今天的实战还会用到一个cgroup的参数cgroup.procs,这个参数是表示受到cgroup限制的进程号PID,默认值为空,也就是默认没有任何进程受到这个cgroup组的限制。


好了,我们现在已经有一点概念了,那么现在开始用一个实战来加深理解吧。


实战案例


先决条件

  • Linux(CentOS/Ubuntu均可)
  • Docker(可选)


首先我们在CPU的cgroup目录下(/sys/fs/cgroup/cpu/)新建一个用于测试的目录,命名为demo,这个目录就称为一个“控制组”。你会发现,操作系统会在你新创建的 demo目录下,自动生成该子系统对应的资源限制文件。

[root@centos7 ~]# cd /sys/fs/cgroup/cpu
[root@centos7 cpu]# mkdir demo
[root@centos7 cpu]# ls demo/
cgroup.clone_children  cpuacct.usage         cpu.cfs_quota_us   cpu.shares         tasks
cgroup.procs           cpuacct.usage_percpu  cpu.rt_period_us   cpu.stat
cpuacct.stat           cpu.cfs_period_us     cpu.rt_runtime_us  notify_on_release


我们先来看一下系统帮我们新建的cpu.cfs_period_us,cpu.cfs_quota_us和cgroup.procs 3个文件的默认值:

[root@centos7 cpu]# cd demo/
[root@centos7 demo]# cat cpu.cfs_period_us cpu.cfs_quota_us cgroup.procs 
100000
-1

我们明明cat了三个文件,为什么只有两个值呢?正如刚才说的,cgroup.procs的默认值为空,因此我们得到的输出只有cpu.cfs_period_us,cpu.cfs_quota_us这两个文件的默认值。


/sys/fs/cgroup目录中,默认是不允许新建文件的,因此我们需要在其他目录下新建一个用于测试cgroup限制的程序demo.sh,这里以/tmp目录为例。

[root@centos7 demo]# cat > /tmp/demo.sh << EOF
> #!/bin/sh
> while :;
> do :;
> done
> EOF
[root@centos7 demo]# chmod +x /tmp/demo.sh 


显然,这个脚本的意思是执行一个死循环,可以把计算机当前的CPU 吃到 100%,现在我们运行一下程序,并且使用top命令观察一下CPU使用率。根据它的输出,我们可以看到这个脚本在主机后台运行的进程号(PID)是 11990。

[root@centos7 demo]# /tmp/demo.sh &
[1] 11990
[root@centos7 demo]# top -p 11990
...
   PID USER      PR  NI    VIRT    RES    SHR S %CPU %MEM     TIME+ COMMAND                               
 11990 root      20   0  113192   2596   2444 R 99.9  0.1   0:15.39 demo.sh


接下来,我们通过修改cpu.cfs_quota_us文件限制这个程序的CPU使用率。

例如,我们要限制此进程只能使用50%(0.5个)CPU,那么根据CFS算法,在cpu.cfs_period_us的单位时间100ms里,cpu.cfs_quota_us的值应该设置为50ms,计算过程是 50ms/100ms=0.5。

[root@centos7 demo]# echo 50000 > /sys/fs/cgroup/cpu/demo/cpu.cfs_quota_us


仅仅只有cpu.cfs_period_us,cpu.cfs_quota_us这两个文件,系统是不知道对哪个进程进行限制的,因此别忘了,我们还有一个参数还没用到,那就是cgroup.procs,现在我们需要把进程号PID写入cgroup.procs文件里。

[root@centos7 demo]# echo 11990 > /sys/fs/cgroup/cpu/demo/cgroup.procs


现在我们再使用top命令看看刚才进程的CPU使用率

[root@centos7 demo]# top -p 11990
...
   PID USER      PR  NI    VIRT    RES    SHR S %CPU %MEM     TIME+ COMMAND                               
 11990 root      20   0  113192   2596   2444 R 50.3  0.1  24:25.85 demo.sh


可以看到,cgroup系统已经对我们的进程做了CPU使用率的限制。大家可以通过调整cpu.cfs_period_us,cpu.cfs_quota_us不同的值来测试不同的使用率限制。


测试完之后,记得把测试的程序停掉,避免持续消耗CPU资源,再把刚才用于测试的demo目录删掉。

[root@centos7 demo]# kill -15 11990
[1]+  Terminated              /tmp/demo.sh
[root@centos7 demo]# rmdir /sys/fs/cgroup/cpu/demo/


加餐:


既然我们已经对CPU cgroup和CFS有点了解了,那么docker又是怎么限制容器的CPU使用率的呢?


实际上docker做了一个很巧妙的设计,即在每一个cgroup的子系统下为每个容器新建一个资源控制组(也就是一个目录,相当于我们刚才的demo目录),控制组的名称以容器名来命名,然后在容器启动的时候,把主机对应的进程PID写到相应的cgroup.procs里面就可以了。


至于控制组的资源限制数值是从哪里来呢?我们可以从docker run命令里面看到有两个参数--cpu-period和--cpu-quota,也就是说,资源控制数值是通过用户在启动容器的时候指定的。


现在我们启动一个容器来验证一下,与前面的案例一样,我们希望把CPU使用率限制在50%,即--cpu-period=100000和--cpu-quota=50000:

[root@centos7 demo]# docker run -it --cpu-period=100000 --cpu-quota=50000 busybox /bin/sh
/ # 


现在我们同样写入一个用于测试的sh脚本:

/ # cat > /tmp/demo.sh << EOF
> #!/bin/sh
> while :;
> do :;
> done
> EOF
/ # chmod +x /tmp/demo.sh 
/ # 


接着在后台执行这个sh脚本,并且通过top命令观察容器里面的CPU使用率,可以看到,demo.sh所在的进程已经被限制只能使用50%的CPU了:

/ # /tmp/demo.sh &

/ # top
...
  PID  PPID USER     STAT   VSZ %VSZ CPU %CPU COMMAND
   12     1 root     R     1304  0.0   0 50.0 {demo.sh} /bin/sh /tmp/demo.sh
    1     0 root     S     1324  0.0   0  0.0 /bin/sh
   13     1 root     R     1312  0.0   0  0.0 top


那么运行docker的主机是怎样做到这个限制的呢?


首先我们先找到容器的container_id,接着根据container_id进入对应的CPU cgroup的目录(基于docker的容器会统一放置在/sys/fs/cgroup/cpu/docker/$container_id目录下)

[root@centos7 ~]# docker ps
CONTAINER ID   IMAGE     COMMAND     CREATED          STATUS          PORTS     NAMES
720772929dca   busybox   "/bin/sh"   31 minutes ago   Up 31 minutes             unruffled_hofstadter
[root@centos7 ~]# cd /sys/fs/cgroup/cpu/docker/720772929dcaeff7d5ff90bdb37c87ac63fae782162878dbf26ecee5bfe4ddc6/
[root@centos7 720772929dcaeff7d5ff90bdb37c87ac63fae782162878dbf26ecee5bfe4ddc6]# ls .
cgroup.clone_children  cpuacct.usage         cpu.cfs_quota_us   cpu.shares         tasks
cgroup.procs           cpuacct.usage_percpu  cpu.rt_period_us   cpu.stat
cpuacct.stat           cpu.cfs_period_us     cpu.rt_runtime_us  notify_on_release


我们先来看看用于资源控制的两个文件cpu.cfs_period_us和cpu.cfs_quota_us的数值,确认了这个容器的CPU资源限制跟我们期待的值是一致的:

[root@centos7 720772929dcaeff7d5ff90bdb37c87ac63fae782162878dbf26ecee5bfe4ddc6]# cat cpu.cfs_period_us cpu.cfs_quota_us
100000
50000


接着再看看cgroup.procs文件,里面有两个PID,分别是12683和12717。

[root@centos7 720772929dcaeff7d5ff90bdb37c87ac63fae782162878dbf26ecee5bfe4ddc6]# cat cgroup.procs 
12683
12717


先从PID=12683的进程来分析,在主机执行以下命令看看PID=12683实际运行了什么命令:

[root@centos7 720772929dcaeff7d5ff90bdb37c87ac63fae782162878dbf26ecee5bfe4ddc6]# ps -ef|grep 12683|grep -v grep
root      12683  12663  0 14:51 pts/0    00:00:00 /bin/sh
root      12717  12683 49 14:55 pts/0    00:21:43 /bin/sh /tmp/demo.sh


通过屏幕输出可以看到PID=12683的进程就是容器启动时的命令(/bin/sh),而PID=12717是正在运行的程序demo.sh的PID,它的父进程是PID=12683。


其实啊,docker是通过把容器的1号进程(在演示的容器里面,PID=1的进程是/bin/sh)映射到主机的12683号进程,然后把主机的进程PID=12683以及这个PID下的其他后续创建的所有进程都写入cgroup.procs里面,这样就完成对一个容器的CPU资源限制,所以cgroup.procs里面可以有不止一个PID,并且这些PID均受到同一个CPU cgroup的资源限制,是不是很巧妙?



总结


通过这篇文章,首先给大家介绍了Linux Cgroup的概念以及cgroup的子系统有哪些,紧接着介绍了CPU cgroup的CFS调度算法和实战案例,最后通过加餐学习了docker是怎么通过cgroup限制不同容器的CPU使用率限制。希望大家在通读这篇文章后对Cgroup的机制有所了解并且在实际使用中可以举一反三。


参考资料


[1] Red Hat Enterprise Linux 7 资源管理指南

https://access.redhat.com/documentation/zh-cn/red_hat_enterprise_linux/7/html/resource_management_guide/index

Tags:

本文暂时没有评论,来添加一个吧(●'◡'●)

欢迎 发表评论:

最近发表
标签列表