cgroup实践-资源控制

1、Cgroup安装
安装Cgroups需要libcap-devel和libcgroup两个相关的包

1
yum install gcc libcap-devel 

2、Cgroup挂载配置

1
2
3
4
5
6
7
8
Cgroup对应服务名称为cgconfig,cgconfig默认采用“多挂载点”挂载。经过实际测试,发现在CentOS环境中应采用“单挂载点”进行挂载,因此应当卸载原有cgroup文件系统,并禁用cgconfig。
cgclear或者sudo service cgconfig stop # 停止cgconfig,卸载cgroup目录
sudo chkconfig cgconfig off # 禁用cgconfig服务,避免其开机启动
然后采用“单挂载点”方式重新挂载cgroup。
可以直接手动挂载,这样仅当次挂载成功。
mount -t cgroup none /cgroup
然后编辑/etc/fstab/,输入下列内容。这样每次开机后都会自动挂载。
none /cgroup cgroup defaults 0 0

3、常用的Cgroup相关命令和配置文件

1
2
3
4
5
6
7
service cgconfig status|start|stop|restart    #查看已存在子系统
lssubsys –am #查看已存在子系统
cgclear # 清除所有挂载点内部文件,相当于service cgconfig stop
cgconfigparser -l /etc/cgconfig.conf #重新挂载

Cgroup默认挂载点(CentOS):/cgroup
cgconfig配置文件:/etc/cgconfig.conf

4、libcgroup Man Page简介

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
man 1 cgclassify -- cgclassify命令是用来将运行的任务移动到一个或者多个cgroup。
man 1 cgclear -- cgclear 命令是用来删除层级中的所有cgroup。
man 5 cgconfig.conf -- 在cgconfig.conf文件中定义cgroup。
man 8 cgconfigparser -- cgconfigparser命令解析cgconfig.conf文件和并挂载层级。

man 1 cgcreate -- cgcreate在层级中创建新cgroup。
man 1 cgdelete -- cgdelete命令删除指定的cgroup。
man 1 cgexec -- cgexec命令在指定的cgroup中运行任务。
man 1 cgget -- cgget命令显示cgroup参数。
man 5 cgred.conf -- cgred.conf是cgred服务的配置文件。
man 5 cgrules.conf -- cgrules.conf 包含用来决定何时任务术语某些 cgroup的规则。

man 8 cgrulesengd -- cgrulesengd 在 cgroup 中发布任务。
man 1 cgset -- cgset 命令为 cgroup 设定参数。
man 1 lscgroup -- lscgroup 命令列出层级中的 cgroup。
man 1 lssubsys -- lssubsys 命令列出包含指定子系统的层级。

测试一:限制cpu的资源

测试后验证了可以做到:

  • 限制进程的cpu占用百分比
    
  • 限制多个进程组的之间的cpu使用权重
    
  • 指定进程的使用的cpu和内存组(绑定cpu)
    

跑一个耗cpu的脚本

1
2
3
4
x=0
while [ True ];do
x=$x+1
done;

top可以看到这个脚本基本占了100%的cpu资源

1
2
3
4
5
6
7
8
9
top - 15:30:01 up  1:03,  5 users,  load average: 0.30, 0.50, 0.39
Tasks: 210 total, 2 running, 208 sleeping, 0 stopped, 0 zombie
Cpu(s): 6.3%us, 0.1%sy, 0.0%ni, 93.5%id, 0.2%wa, 0.0%hi, 0.0%si, 0.0%st
Mem: 49461228k total, 13412644k used, 36048584k free, 75384k buffers
Swap: 2097148k total, 0k used, 2097148k free, 12498636k cached

PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
11605 root 20 0 104m 1528 1016 R 99.7 0.0 2:30.48 sh
105 root 20 0 0 0 0 S 0.3 0.0 0:00.11 kworker/8:1

创建一个控制组控制这个进程的cpu资源

1
2
3
mkdir -p /cgroup/cpu/foo	     #新建一个控制组foo
echo 50000 > /cgroup/cpu/foo/cpu.cfs_quota_us #将cpu.cfs_quota_us设为50000,相对于cpu.cfs_period_us的100000是50%
echo 11605 > /cgroup/cpu/foo/tasks

然后top的实时统计数据如下,cpu占用率将近50%,看来cgroups关于cpu的控制起了效果

1
2
3
4
5
6
7
8
9
top - 15:32:48 up  1:06,  5 users,  load average: 0.80, 0.68, 0.48
Tasks: 210 total, 2 running, 208 sleeping, 0 stopped, 0 zombie
Cpu(s): 3.2%us, 0.0%sy, 0.0%ni, 96.8%id, 0.0%wa, 0.0%hi, 0.0%si, 0.0%st
Mem: 49461228k total, 13412276k used, 36048952k free, 75400k buffers
Swap: 2097148k total, 0k used, 2097148k free, 12498652k cached

PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
11605 root 20 0 104m 1724 1016 R 50.2 0.0 5:09.97 sh
11639 root 20 0 15200 1200 820 R 0.3 0.0 0:00.03 top

可以看到,进程的 cpu 占用已经被成功地限制到了 50% 。这里,测试的虚拟机只有一个核心。在多核情况下,看到的值会不一样。另外,cfs_quota_us 也是可以大于 cfs_period_us 的,这主要是对于多核情况。有 n 个核时,一个控制组中的进程自然最多就能用到 n 倍的 cpu 时间。

这两个值在 cgroups 层次中是有限制的,下层的资源不能超过上层。具体的说,就是下层的 cpu.cfs_period_us 值不能小于上层的值,cpu.cfs_quota_us 值不能大于上层的值。

另外的一组 cpu.rt_period_us、cpu.rt_runtime_us 对应的是实时进程的限制,平时可能不会有机会用到。

在 cpu 子系统中,cpu.stat 就是用前面那种方法做的资源限制的统计了。nr_periods、nr_throttled 就是总共经过的周期,和其中受限制的周期。throttled_time 就是总共被控制组掐掉的 cpu 使用时间。

还有个 cpu.shares, 它也是用来限制 cpu 使用的。但是与 cpu.cfs_quota_us、cpu.cfs_period_us 有挺大区别。cpu.shares 不是限制进程能使用的绝对的 cpu 时间,而是控制各个组之间的配额。比如

1
2
/cpu/cpu.shares : 1024
/cpu/foo/cpu.shares : 2048

那么当两个组中的进程都满负荷运行时,/foo 中的进程所能占用的 cpu 就是 / 中的进程的两倍。如果再建一个 /foo/bar 的 cpu.shares 也是 1024,且也有满负荷运行的进程,那 /、/foo、/foo/bar 的 cpu 占用比就是 1:2:1 。前面说的是各自都跑满的情况。如果其他控制组中的进程闲着,那某一个组的进程完全可以用满全部 cpu。可见通常情况下,这种方式在保证公平的情况下能更充分利用资源。

此外,还可以限定进程可以使用哪些 cpu 核心。cpuset 子系统就是处理进程可以使用的 cpu 核心和内存节点,以及其他一些相关配置。这部分的很多配置都和 NUMA 有关。其中 cpuset.cpus、cpuset.mems 就是用来限制进程可以使用的 cpu 核心和内存节点的。这两个参数中 cpu 核心、内存节点都用 id 表示,之间用 “,” 分隔。比如 0,1,2 。也可以用 “-” 表示范围,如 0-3 。两者可以结合起来用。如“0-2,6,7”。在添加进程前,cpuset.cpus、cpuset.mems 必须同时设置,而且必须是兼容的,否则会出错。例如

1
2
3
4
5
# echo 0 >/sys/fs/cgroup/cpuset/foo/cpuset.cpus
# echo 0 >/sys/fs/cgroup/cpuset/foo/cpuset.mems
这样, /foo 中的进程只能使用 cpu0 和内存节点0。用

# cat /proc/<pid>/status|grep '_allowed_list'

cgroups 除了用来限制资源使用外,还有资源统计的功能。做云计算的计费就可以用到它。有一个 cpuacct 子系统专门用来做 cpu 资源统计。cpuacct.stat 统计了该控制组中进程用户态和内核态的 cpu 使用量,单位是 USER_HZ,也就是 jiffies、cpu 滴答数。每秒的滴答数可以用 getconf CLK_TCK 来获取,通常是 100。将看到的值除以这个值就可以换算成秒。

测试二:限制进程的内存资源

测试后验证了:

  • 限制了资源的占用,达到内存以后,进程直接杀掉

测试方法:

跑一个耗内存的脚本,内存不断增长

1
2
3
4
x="a"
while [ True ];do
x=$x$x
done;

top看内存占用稳步上升

1
2
3
4
    PID USER      PR  NI  VIRT  RES  SHR S %CPU %MEM    TIME+  COMMAND
30215 root 20 0 871m 501m 1036 R 99.8 26.7 0:38.69 sh
30215 root 20 0 1639m 721m 1036 R 98.7 38.4 1:03.99 sh
30215 root 20 0 1639m 929m 1036 R 98.6 49.5 1:13.73 sh

下面用cgroups控制这个进程的内存资源

1
2
3
mkdir -p /cgroup/memory/foo
echo 1048576 > /cgroup/memory/foo/memory.limit_in_bytes #分配1MB的内存给这个控制组
echo 30215 > /cgroup/memory/foo/tasks

发现之前的脚本被kill掉

1
2
[root@localhost ~]# sh /home/test.sh 
已杀死

因为这是强硬的限制内存,当进程试图占用的内存超过了cgroups的限制,会触发out of memory,导致进程被kill掉。

实际情况中对进程的内存使用会有一个预估,然后会给这个进程的限制超配50%比如,除非发生内存泄露等异常情况,才会因为cgroups的限制被kill掉。

也可以通过配置关掉cgroups oom kill进程,通过memory.oom_control来实现(oom_kill_disable 1),但是尽管进程不会被直接杀死,但进程也进入了休眠状态,无法继续执行,仍然无法服务。

关于内存的控制,还有以下配置文件,关于虚拟内存的控制,以及权值比重式的内存控制等

1
2
3
4
5
6
7
[root@localhost /]# ls /cgroup/memory/foo/
cgroup.event_control memory.force_empty memory.memsw.failcnt
memory.memsw.usage_in_bytes memory.soft_limit_in_bytes memory.usage_in_bytes tasks
cgroup.procs memory.limit_in_bytes memory.memsw.limit_in_bytes
memory.move_charge_at_immigrate memory.stat memory.use_hierarchy
memory.failcnt memory.max_usage_in_bytes memory.memsw.max_usage_in_bytes
memory.oom_control memory.swappiness notify_on_release

测试三:限制进程的IO资源

测试验证了:

  • 能够控制io设备的读写速度

跑一个耗io的脚本

1
2
3
4
5
6
    dd if=/dev/sda of=/dev/null 

通过iotop看io占用情况,磁盘速度到了284M/s


30252 be/4 root 284.71 M/s 0.00 B/s 0.00 % 0.00 % dd if=/dev/sda of=/dev/null

下面用cgroups控制这个进程的io资源

1
2
3
4
5
mkdir -p /cgroup/blkio/foo

echo '8:0 1048576' > /cgroup/blkio/foo/blkio.throttle.read_bps_device
#8:0对应主设备号和副设备号,可以通过ls -l /dev/sda查看
echo 30252 > /cgroup/blkio/foo/tasks

再通过iotop看,确实将读速度降到了1M/s

1
30252 be/4 root      993.36 K/s    0.00 B/s  0.00 %  0.00 % dd if=/dev/sda of=/dev/null  

对于io还有很多其他可以控制层面和方式,如下

1
2
3
4
5
6
7
[root@localhost ~]# ls /cgroup/blkio/foo/
blkio.io_merged blkio.io_serviced blkio.reset_stats
blkio.throttle.io_serviced blkio.throttle.write_bps_device blkio.weight cgroup.procs
blkio.io_queued blkio.io_service_time blkio.sectors
blkio.throttle.read_bps_device blkio.throttle.write_iops_device blkio.weight_device notify_on_release
blkio.io_service_bytes blkio.io_wait_time blkio.throttle.io_service_bytes
blkio.throttle.read_iops_device blkio.time cgroup.event_control tasks

blkio 子系统里东西很多。不过大部分都是只读的状态报告,可写的参数就只有下面这几个:

1
2
3
4
5
6
7
8
9
blkio.throttle.read_bps_device
blkio.throttle.read_iops_device
blkio.throttle.write_bps_device
blkio.throttle.write_iops_device
blkio.weight
blkio.weight_device

这些都是用来控制进程的磁盘 io 的。很明显地分成两类,其中带“throttle”的,顾名思义就是节流阀,将流量限制在某个值下。而“weight”就是分配 io 的权重。
再看看 blkio.weight 。blkio 的 throttle 和 weight 方式和 cpu 子系统的 quota 和 shares 有点像,都是一种是绝对限制,另一种是相对限制,并且在不繁忙的时候可以充分利用资源,权重值的范围在 10 – 1000 之间。

测试权重方式要麻烦一点。因为不是绝对限制,所以会受到文件系统缓存的影响。如在虚拟机中测试,要关闭虚机如我用的 VirtualBox 在宿主机上的缓存。如要测试读 io 的效果,先生成两个几个 G 的大文件 /tmp/file_1,/tmp/file_2 ,可以用 dd 搞。然后设置两个权重

1
2
# echo 500 >/sys/fs/cgroup/blkio/foo/blkio.weight
# echo 100 >/sys/fs/cgroup/blkio/bar/blkio.weight

测试前清空文件系统缓存,以免干扰测试结果

1
2
sync
echo 3 >/proc/sys/vm/drop_caches

在这两个控制组中用 dd 产生 io 测试效果。

1
2
3
4
# cgexec -g "blkio:foo" dd if=/tmp/file_1 of=/dev/null &
[1] 1838
# cgexec -g "blkio:bar" dd if=/tmp/file_2 of=/dev/null &
[2] 1839

还是用 iotop 看看效果

1
2
3
TID  PRIO  USER     DISK READ  DISK WRITE  SWAPIN     IO>    COMMAND
1839 be/4 root 48.14 M/s 0.00 B/s 0.00 % 99.21 % dd if=/tmp/file_2 of=/dev/null
1838 be/4 root 223.59 M/s 0.00 B/s 0.00 % 16.44 % dd if=/tmp/file_1 of=/dev/null

两个进程每秒读的字节数虽然会不断变动,但是大致趋势还是维持在 1:5 左右,和设定的 weight 比例一致。blkio.weight_device 是分设备的。写入时,前面再加上设备号即可。

实践记录

1、假如已经配置好一个资源组,现在想让一个服务按这个组的资源分配来运行服务,而不需要去找到进程号再写入到tasks中

1
[root@lab8105 ~]# cgexec -g cpu:daemons/ftp top

这个运行以后有会自动将top进程号写入到tasks当中去

2、查询一个组里面设置的资源的限制

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
设置的值会显示出来,没有设置的就会提示没有找到

[root@lab8105 ~]# cgget daemons/ftp
daemons/ftp:
cgget: cannot find controller 'cpuset' in group 'daemons/ftp'
cpu.rt_period_us: 1000000
cpu.rt_runtime_us: 0
cpu.stat: nr_periods 0
nr_throttled 0
throttled_time 0
cpu.cfs_period_us: 5000
cpu.cfs_quota_us: -1
cpu.shares: 1000
cgget: cannot find controller 'cpuacct' in group 'daemons/ftp'
cgget: cannot find controller 'memory' in group 'daemons/ftp'
cgget: cannot find controller 'devices' in group 'daemons/ftp'
cgget: cannot find controller 'freezer' in group 'daemons/ftp'
cgget: cannot find controller 'net_cls' in group 'daemons/ftp'
cgget: cannot find controller 'blkio' in group 'daemons/ftp'

3、需要用两个限制条件对进程进行限制

1
[root@lab8105 ~]# cgexec -g cpu:daemons/ftp -g memory:daemons/ftp top

4、默认情况下是一个大根,然后分了几个资源系统,还支持做一个子系统组,即单独组建一个资源组,然后对这个资源组里面进行配置,具体方法如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
mount {
cpu = /cgroup/cpu_and_mem;
memory = /cgroup/cpu_and_mem;
}

group daemons/ftp {
cpu {
cpu.shares = "1000";
cpu.cfs_period_us = "5000";
}
memory {
memory.swappiness = "20";
}
}

5、需要创建控制组群,如上的daemons/ftp,想通过命令行的方式创建

1
2
3
4
5
[root@lab8105 ~]# cgcreate -g cpu:/zp -g memory:/zp	
如上命令使用后会在/cgroup/cpu/中多了zp目录,并且里面是继承的上级的cpu里面的参数,这样就创建了一个zp的组群

删除组群的方式如下(删除cgroup时,其所有任务都移动到了父组群当中):
[root@lab8105 ~]# cgdelete cpu:/zp memory:/zp

6、设置里面的配置参数

1
2
3
4
5
6
7
8
需要设置
/cgroup/cpu/daemons/ftp/cpu.shares
执行
[root@lab8105 ~]# cgset -r cpu.shares=500 daemons/ftp
daemons/ftp路径是相对于根的,如果想设置根的这个参数那么就执行
[root@lab8105 ~]# gset -r cpuacct.usage=0 /
这里需要注意,只有某些参数是可以修改的,某些参数是不能修改的
也可以直接echo的方式进行参数的设置

7,移动某个进程到控制组群当中(动态的进行资源的调配)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
移动指定进程到指定的控制组当中,创建两个资源组,使用上面的cpu的脚本,然后运行后,使用top进行监控
group half {
cpu {
cpu.cfs_period_us="100000";
cpu.cfs_quota_us="50000";
}
memory {
memory.swappiness = "50";
}
}

group eighty {
cpu {
cpu.cfs_period_us = "100000";
cpu.cfs_quota_us="50000";
}
memory {
memory.swappiness = "80";
}
}
[root@lab8105 ~]# cgclassify -g cpu:half 14245
top监控看到cpu的占用为50%
[root@lab8105 ~]# cgclassify -g cpu:eighty 14245
top监控看到cpu的占用为80%
注意支持多进程,多资源组同时移动
[root@lab8105 ~]# cgclassify -g cpu,memory:eighty 14245 14565
备用方法就是直接echo

8、通过规则对指定的进程进行控制

我们还可以通过设置规则来让 cgred(cgroup 规则引擎后台程序)自动将进程分配给特定组。cgred 后台程序根据 /etc/cgrules.conf 文件中的设置将任务移到 cgroup 中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
[root@lab8105 ~]# vim /etc/cgrules.conf 
[root@lab8105 ~]# man cgrules.conf

# /etc/cgrules.conf
#The format of this file is described in cgrules.conf(5)
#manual page.
#
# Example:
#<user> <controllers> <destination>
#@student cpu,memory usergroup/student/
#peter cpu test1/
#% memory test2/
# End of file
#
root:cpu.sh cpu half/
root cpu half/

启动监控进程服务
[root@lab8105 ~]# /etc/init.d/cgred

效果如下,运行相同的命令,所占用的cpu的资源按指定的比例进行占用

1
2
3
4
5
6
7
8
9
10
[root@lab8105 ~]# top
top - 16:00:40 up 1 day, 1:34, 5 users, load average: 1.57, 1.13, 0.90
Tasks: 216 total, 3 running, 213 sleeping, 0 stopped, 0 zombie
Cpu(s): 6.9%us, 0.0%sy, 0.0%ni, 93.1%id, 0.0%wa, 0.0%hi, 0.0%si, 0.0%st
Mem: 49461228k total, 49346312k used, 114916k free, 47374260k buffers
Swap: 2097148k total, 0k used, 2097148k free, 33116k cached

PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
14648 root 20 0 104m 1716 1012 R 99.7 0.0 4:58.38 cpu1.sh
14565 root 20 0 104m 1704 1012 R 10.0 0.0 4:30.53 cpu.sh

如上所述,指定用户,可以指定进程进行控制,也可以指定用户的所有进程进行控制,后台的做的操作就是把进行的号移动到了指定的资源组的task当中去了