Cgroup – Linux的IO资源隔离

Cgroup – Linux的IO资源隔离

Zorro] icon

Hi,我是Zorro。微信公众号:Linux系统技术。这是我的微博地址,我会不定期在这里更新文章,如果你有兴趣,可以来关注我呦。

您的支持是我持续写下去的动力,所以本文无耻的接受捐赠。如果你觉得值得,请刷下面二维码,捐赠九毛九。

mm_facetoface_collect_qrcode_1465221734716

今天我们来谈谈:

##Linux的IO隔离

跟内存管理那部分复杂度类似,IO的资源隔离要讲清楚也是比较麻烦的。这部分内容都是这样,配置起来简单,但是要理解清楚确没那么简单。这次是跟Linux内核的IO实现有关系。对于IO的速度限制,实现思路跟CPU和内存都不一样。CPU是针对进程占用时间的比例限制,内存是空间限制,而当我们讨论IO资源隔离的时候,实际上有两个资源需要考虑,一个是空间,另一个是速度。对于空间来说,这个很简单,大不了分区就是了。现实手段中,分区、LVM、磁盘配额、目录配额等等,不同的分区管理方式,不同的文件系统都给出了很多不同的解决方案。所以,空间的限制实际上不是cgroup要解决的问题,那就是说,我们在这里要解决的问题是:如何进行IO数据传输的速度限制。

限速这件事情,现实中有很多模型、算法去解决这个问题。比如,如果我们想控制高速公路上的汽车单位时间通过率,就让收费站每隔固定时间周期只允许通过固定个数的车就好了。这是一种非常有效的控制手段--漏斗算法。现实中这种算法可能在特定情况下会造成资源浪费以及用户的体验不好,于是又演化出令牌桶算法。这里我们不去详细分析这些算法,但是我们要知道,对io的限速基本是一个漏斗算法的限速效果。无论如何,这种限速都要有个“收费站”这样的设施来执行限速,那么对于Linux的IO体系来说,这个”收费站”建在哪里呢?于是我们就必须先来了解一下:

###Linux的IO体系

Linux的IO体系是个层级还算清晰的结构,它基本上分成了如图示这样几层:

Linux的IO体系层次结构

Linux的IO体系层次

我们可以通过追踪一个read()系统调用来一窥这些层次的结构,当read()系统调用发生,内核首先会通过汇编指令引发一个软中断,然后根据中断传入的参数查询系统调用影射表,找到read()对应的内核调用方法名,并去执行相关调用,这个系统调用名一般情况下就是sys_read()。从此,便开始了调用在内核中处理的过程的第一步:

  1. VFS层:虚拟文件系统层。由于内核要跟多种文件系统打交道,而每一种文件系统所实现的数据结构和相关方法都可能不尽相同,所以,内核抽象了这一层,专门用来适配各种文件系统,并对外提供统一操作接口。
  2. 文件系统层:不同的文件系统实现自己的操作过程,提供自己特有的特征,具体不多说了,大家愿意的话自己去看代码即可。
  3. 页缓存层:我们的老朋友了,如果不了解缓存是什么的,可以先来看看Linux内存资源管理部分。
  4. 通用块层:由于绝大多数情况的io操作是跟块设备打交道,所以Linux在此提供了一个类似vfs层的块设备操作抽象层。下层对接各种不同属性的块设备,对上提供统一的Block IO请求标准。
  5. IO调度层:因为绝大多数的块设备都是类似磁盘这样的设备,所以有必要根据这类设备的特点以及应用的不同特点来设置一些不同的调度算法和队列。以便在不同的应用环境下有针对性的提高磁盘的读写效率,这里就是大名鼎鼎的Linux电梯所起作用的地方。针对机械硬盘的各种调度方法就是在这实现的。
  6. 块设备驱动层:驱动层对外提供相对比较高级的设备操作接口,往往是C语言的,而下层对接设备本身的操作方法和规范。
  7. 块设备层:这层就是具体的物理设备了,定义了各种真对设备操作方法和规范。

根据这几层的特点,如果你是设计者,你会在哪里实现真对块设备的限速策略呢?6、7都是相关具体设备的,如果在这个层次提供,那就不是内核全局的功能,而是某些设备自己的feture。文件系统层也可以实现,但是如果要全局实现也是不可能的,需要每种文件系统中都实现一遍,成本太高。所以,可以实现限速的地方比较合适的是VFS、缓存层、通用块层和IO调度层。而VFS和page cache这样的机制并不是面向块设备设计的,都是做其他事情用的,虽然也在io体系中,但是并不适合用来做block io的限速。所以这几层中,最适合并且成本最低就可以实现的地方就是IO调度层和通用块层。IO调度层本身已经有队列了,我们只要在队列里面实现一个限速机制即可,但是在IO调度层实现的限速会因为不同调度算法的侧重点不一样而有很多局限性,从通用块层实现的限速,原则上就可以对几乎所有的块设备进行带宽和iops的限制。截止目前(4.3.3内核),IO限速主要实现在这两层中。

根据IO调度层和通用块层的特点,这两层分别实现了两种不同策略的IO控制策略,也是目前blkio子系统提供的两种控制策略,一个是权重比例方式的控制,另一个是针对IO带宽和IOPS的控制。

###IO调度层

我们需要先来认识一下IO调度层。这一层要解决的核心问题是,如何提高块设备IO的整体性能?这一层也主要是针对用途最广泛的机械硬盘结构而设计的。众所周知,机械硬盘的存储介质是磁介质,并且是盘状,用磁头在盘片上移动进行数据的寻址,这类似播放一张唱片。这种结构的特点是,顺序的数据读写效率比较理想,但是如果一旦对盘片有随机读写,那么大量的时间都会浪费在磁头的移动上,这时候就会导致每次IO的响应时间很长,极大的降低IO的响应速度。磁头在盘片上寻道的操作,类似电梯调度,如果在寻道的过程中,能把路过的相关磁道的数据请求都“顺便”处理掉,那么就可以在比较小影响响应速度的前提下,提高整体IO的吞吐量。所以,一个好的IO调度算法的需求就此产生。在最开始的阶段,Linux就把这个算法命名为Linux电梯算法。目前在内核中默认开启了三种算法,其实严格算应该是两种,因为第一种叫做noop,就是空操作调度算法,也就是没有任何调度操作,并不对io请求进行排序,仅仅做适当的io合并的一个fifo队列。

目前内核中默认的调度算法应该是cfq,叫做完全公平队列调度。这个调度算法人如其名,它试图给所有进程提供一个完全公平的IO操作环境。它为每个进程创建一个同步IO调度队列,并默认以时间片和请求数限定的方式分配IO资源,以此保证每个进程的IO资源占用是公平的,cfq还实现了针对进程级别的优先级调度,这里我们不去细节解释。我们在此只需要知道,既然时间片分好了,优先级实现了,那么cfq肯定是实现进程级别的权重比例分配的最好方案。内核就是这么做的,cgroup blkio的权重比例限制就是基于cfq调度器实现的。如果你要使用权重比例分配,请先确定对应的块设备的IO调度算法是cfq。

查看和修改的方法是:

[zorro@zorrozou-pc0 ~]$ cat /sys/block/sda/queue/scheduler
noop deadline [cfq]
[zorro@zorrozou-pc0 ~]$ echo cfq > /sys/block/sda/queue/scheduler

cfq是通用服务器比较好的IO调度算法选择,对桌面用户也是比较好的选择。但是对于很多IO压力较大的场景就并不是很适应,尤其是IO压力集中在某些进程上的场景。因为这种场景我们需要更多的满足某个或者某几个进程的IO响应速度,而不是让所有的进程公平的使用IO,比如数据库应用。

deadline调度(最终期限调度)就是更适应这样的场景的解决方案。deadline实现了四个队列,其中两个分别处理正常read和write,按扇区号排序,进行正常io的合并处理以提高吞吐量.因为IO请求可能会集中在某些磁盘位置,这样会导致新来的请求一直被合并,于是可能会有其他磁盘位置的io请求被饿死。于是实现了另外两个处理超时read和write的队列,按请求创建时间排序,如果有超时的请求出现,就放进这两个队列,调度算法保证超时(达到最终期限时间)的队列中的请求会优先被处理,防止请求被饿死。由于deadline的特点,无疑在这里无法区分进程,也就不能实现针对进程的io资源控制。

其实不久前,内核还是默认标配四种算法,还有一种叫做as的算法(Anticipatory scheduler),预测调度算法。一个高大上的名字,搞得我一度认为Linux内核都会算命了。结果发现,无非是在基于deadline算法做io调度的之前等一小会时间,如果这段时间内有可以合并的io请求到来,就可以合并处理,提高deadline调度的在顺序读写情况下的数据吞吐量。其实这根本不是啥预测,我觉得不如叫撞大运调度算法。估计结果是不仅没有提高吞吐量,还降低了响应速度,所以内核干脆把它从默认配置里删除了。毕竟Linux的宗旨是实用。

根据以上几种io调度算法的简单分析,我们也能对各种调度算法的使用场景有一些大致的思路了。从原理上看,cfq是一种比较通用的调度算法,是一种以进程为出发点考虑的调度算法,保证大家尽量公平。deadline是一种以提高机械硬盘吞吐量为思考出发点的调度算法,只有当有io请求达到最终期限的时候才进行调度,非常适合业务比较单一并且IO压力比较重的业务,比如数据库。而noop呢?其实如果我们把我们的思考对象拓展到固态硬盘,那么你就会发现,无论cfq还是deadline,都是针对机械硬盘的结构进行的队列算法调整,而这种调整对于固态硬盘来说,完全没有意义。对于固态硬盘来说,IO调度算法越复杂,效率就越低,因为额外要处理的逻辑越多。所以,固态硬盘这种场景下,使用noop是最好的,deadline次之,而cfq由于复杂度的原因,无疑效率最低。但是,如果你想对你的固态硬盘做基于权重比例的IO限速的话,那就没啥办法了,毕竟这时候,效率并不是你的需求,要不你限速干嘛?

您的支持是我持续写下去的动力,所以本文无耻的接受捐赠。如果你觉得值得,请刷下面二维码,捐赠九毛九。

mm_facetoface_collect_qrcode_1465221734716

###通用块设备层

这层的作用我这里就不再复述了,本节其实主要想解释的是,既然这里实现了对blkio的带宽和iops的速度限制,那么有没有什么需要注意的地方?这自然是有的。首先我们还是要先来搞清楚IO中的几个概念。

一般IO

一个正常的文件io,需要经过vfs -> buffer\page cache -> 文件系统 -> 通用块设备层 -> IO调度层 -> 块设备驱动 -> 硬件设备这所有几个层次。其实这就是一般IO。当然,不同的状态可能会有变化,比如一个进程正好open并read一个已经存在于page cache中的数据。这样的事情我们排出在外不分析。那么什么是比较特别的io呢?

Direct IO

中文也可以叫直接IO操作,其特点是,VFS之后跳过buffer\page cache层,直接从文件系统层进行操作。那么就意味着,无论读还是写,都不会进行cache。我们基本上可以理解这样的io看起来效率要低很多,直接看到的速度就是设备的速度,并且缺少了cache层对数据的缓存之后,文件系统和数据块的操作效率直接暴露给了应用程序,块的大小会直接影响io速度。

Sync IO & write-through:

中文叫做同步IO操作,如果是写操作的话也叫write-through,这个操作往往容易跟上面的DIO搞混,因为看起来他们速度上差不多,但是是有本质区别的。这种方式写的数据要等待存储写入返回才能成功返回,所以跟DIO效率差不多,但是,写的数据仍然是要在cache中写入的,这样其他一般IO的程度仍然可以使用cache机制加速IO操作。所以,这里的sync的意思就是,在执行write操作的时候,让cache和存储上的数据一致。那么他跟一般IO其实一样,数据是要经过cache层的。

write-back:

既然明白了write-thuough,那么write-back就好理解了,无非就是将目前在cache中还没写回存储的脏数据写回到存储。这个名词一般指的是一个独立的过程,这个过程不是随着应用的写而发生,这往往是内核自己找个时间来单独操作的。说白了就是,应用写文件,感觉自己很快写完了,其实内核都把数据放倒cache里了,然后内核自己找时间再去写回到存储上。实际上write-back只是在一般IO的情况下,保证数据一致性的一种机制而已。

有人将IO过程中,以是否使用缓冲(缓存)的区别,将IO分成了缓存IO(Buffered IO)和直接IO(Direct io)。其实就是名词上的不同而已。这里面的buffer的含义跟内存中buffer cache有概念上的不同。实际上这里Buffered IO的含义,相当于内存中的buffer cache+page cache,就是IO经过缓存的意思。到这我们思考一个问题,如果cgroup针对IO的资源限制实现在了通用块设备层,那么将会对哪些IO操作有影响呢?其实原则上说都有影响,因为绝大多数数据都是要经过通用块设备层写入存储的,但是对于应用程序来说感受可能不一样。在一般IO的情况下,应用程序很可能很快的就写完了数据(在数据量小于缓存空间的情况下),然后去做其他事情了。这时应用程序感受不到自己被限速了,而内核在处理write-back的阶段,由于没有相关page cache中的inode是属于那个cgroup的信息记录,所以所有的page cache的回写只能放到cgroup的root组中进行限制,而不能在其他cgroup中进行限制,因为root组的cgroup一般是不做限制的,所以就相当于目前的cgroup的blkio对buffered IO是没有限速支持的。这个功能将在使用了unified-hierarchy体系的cgroup v2中的部分文件系统(ext系列)已经得到得到支持,目前这个功能还在开发中,据说将在4.5版本的内核中正式发布。

而在Sync IO和Direct IO的情况下,由于应用程序写的数据是不经过缓存层的,所以能直接感受到速度被限制,一定要等到整个数据按限制好的速度写完或者读完,才能返回。这就是当前cgroup的blkio限制所能起作用的环境限制。了解了这个之后,我们就可以来看:

##blkio配置方法

###权重比例分配

我们这次直接使用命令行的方式对cgroup进行操作。在我们的系统上,我们现在想创建两个cgroup组,一个叫test1,一个叫test2。我们想让这两个组的进程在对/dev/sdb,设备号为8:16的这个磁盘进行读写的时候按权重比例进行io资源的分配。具体配置方法如下:

首先确认系统上已经mount了相关的cgroup目录:

[root@zorrozou-pc0 ~]# ls /sys/fs/cgroup/blkio/
blkio.io_merged blkio.io_service_bytes_recursive blkio.io_wait_time blkio.sectors blkio.throttle.read_iops_device blkio.weight tasks
blkio.io_merged_recursive blkio.io_serviced blkio.io_wait_time_recursive blkio.sectors_recursive blkio.throttle.write_bps_device blkio.weight_device
blkio.io_queued blkio.io_serviced_recursive blkio.leaf_weight blkio.throttle.io_service_bytes blkio.throttle.write_iops_device cgroup.clone_children
blkio.io_queued_recursive blkio.io_service_time blkio.leaf_weight_device blkio.throttle.io_serviced blkio.time cgroup.procs
blkio.io_service_bytes blkio.io_service_time_recursive blkio.reset_stats blkio.throttle.read_bps_device blkio.time_recursive notify_on_release

然后创建两个针对blkio的cgroup

[root@zorrozou-pc0 ~]# mkdir /sys/fs/cgroup/blkio/test1
[root@zorrozou-pc0 ~]# mkdir /sys/fs/cgroup/blkio/test2

相关目录下会自动产生相关配置项:

[root@zorrozou-pc0 ~]# ls /sys/fs/cgroup/blkio/test{1,2}
/sys/fs/cgroup/blkio/test1:
blkio.io_merged blkio.io_service_bytes_recursive blkio.io_wait_time blkio.sectors blkio.throttle.read_iops_device blkio.weight tasks
blkio.io_merged_recursive blkio.io_serviced blkio.io_wait_time_recursive blkio.sectors_recursive blkio.throttle.write_bps_device blkio.weight_device
blkio.io_queued blkio.io_serviced_recursive blkio.leaf_weight blkio.throttle.io_service_bytes blkio.throttle.write_iops_device cgroup.clone_children
blkio.io_queued_recursive blkio.io_service_time blkio.leaf_weight_device blkio.throttle.io_serviced blkio.time cgroup.procs
blkio.io_service_bytes blkio.io_service_time_recursive blkio.reset_stats blkio.throttle.read_bps_device blkio.time_recursive notify_on_release

/sys/fs/cgroup/blkio/test2:
blkio.io_merged blkio.io_service_bytes_recursive blkio.io_wait_time blkio.sectors blkio.throttle.read_iops_device blkio.weight tasks
blkio.io_merged_recursive blkio.io_serviced blkio.io_wait_time_recursive blkio.sectors_recursive blkio.throttle.write_bps_device blkio.weight_device
blkio.io_queued blkio.io_serviced_recursive blkio.leaf_weight blkio.throttle.io_service_bytes blkio.throttle.write_iops_device cgroup.clone_children
blkio.io_queued_recursive blkio.io_service_time blkio.leaf_weight_device blkio.throttle.io_serviced blkio.time cgroup.procs
blkio.io_service_bytes blkio.io_service_time_recursive blkio.reset_stats blkio.throttle.read_bps_device blkio.time_recursive notify_on_release

之后我们就可以进行限制了。针对cgroup进行权重限制的配置有blkio.weight,是单纯针对cgroup进行权重配置的,还有blkio.weight_device可以针对设备单独进行限制,我们都来试试。首先我们想设置test1和test2使用任何设备的io权重比例都是1:2:

[root@zorrozou-pc0 zorro]# echo 100 > /sys/fs/cgroup/blkio/test1/blkio.weight
[root@zorrozou-pc0 zorro]# echo 200 > /sys/fs/cgroup/blkio/test2/blkio.weight

注意权重设置的取值范围为:10-1000。然后我们来写一个测试脚本:

#!/bin/bash

testfile1=/home/test1
testfile2=/home/test2

if [ -e $testfile1 ]
then
rm -rf $testfile1
fi

if [ -e $testfile2 ]
then
rm -rf $testfile2
fi

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

cgexec -g blkio:test1 dd if=/dev/zero of=$testfile1 oflag=direct bs=1M count=1023 &

cgexec -g blkio:test2 dd if=/dev/zero of=$testfile2 oflag=direct bs=1M count=1023 &

我们dd的时候使用的是direct标记,在这使用sync和不加任何标记的话都达不到效果。因为权重限制是基于cfq实现,cfq要标记进程,而buffered IO都是内核同步,无法标记进程。使用iotop查看限制效果:

[root@zorrozou-pc0 zorro]# iotop -b -n1|grep direct
1519 be/4 root 0.00 B/s 110.00 M/s 0.00 % 99.99 % dd if=/dev/zero of=/home/test2 oflag=direct bs=1M count=1023
1518 be/4 root 0.00 B/s 55.00 M/s 0.00 % 99.99 % dd if=/dev/zero of=/home/test1 oflag=direct bs=1M count=1023

却是达到了1:2比例限速的效果。此时对于磁盘读取的限制效果也一样,具体测试用例大家可以自己编写。读取的时候要注意,仍然要保证读取的文件不在page cache中,方法就是:echo 3 > /proc/sys/vm/drop_caches。因为在page cache中的数据已经在内存里了,直接修改是直接改内存中的内容,只有write-back的时候才会经过cfq。

我们再来试一下针对设备的权重分配,请注意设备号的填写格式:

[root@zorrozou-pc0 zorro]# echo “8:16 400” > /sys/fs/cgroup/blkio/test1/blkio.weight_device
[root@zorrozou-pc0 zorro]# echo “8:16 200” > /sys/fs/cgroup/blkio/test2/blkio.weight_device

[root@zorrozou-pc0 zorro]# iotop -b -n1|grep direct
1800 be/4 root 0.00 B/s 102.24 M/s 0.00 % 99.99 % dd if=/dev/zero of=/home/test1 oflag=direct bs=1M count=1023
1801 be/4 root 0.00 B/s 51.12 M/s 0.00 % 99.99 % dd if=/dev/zero of=/home/test2 oflag=direct bs=1M count=1023

我们会发现,这时权重确实是按照最后一次的设置,test1和test2变成了2:1的比例,而不是1:2了。这里要说明的就是,注意blkio.weight_device的设置会覆盖blkio.weight的设置,因为前者的设置精确到了设备,Linux在这里的策略是,越精确越优先。

您的支持是我持续写下去的动力,所以本文无耻的接受捐赠。如果你觉得值得,请刷下面二维码,捐赠九毛九。

mm_facetoface_collect_qrcode_1465221734716

###读写带宽和iops限制

针对读写带宽和iops的限制都是绝对值限制,所以我们不用两个cgroup做对比了。我们就设置test1的写带宽速度为1M/s:

[root@zorrozou-pc0 zorro]# echo “8:16 1048576” > /sys/fs/cgroup/blkio/test1/blkio.throttle.write_bps_device

[root@zorrozou-pc0 zorro]# sync
[root@zorrozou-pc0 zorro]# echo 3 > /proc/sys/vm/drop_caches

[root@zorrozou-pc0 zorro]# cgexec -g blkio:test1 dd if=/dev/zero of=/home/test oflag=direct count=1024 bs=1M
^C21+0 records in
21+0 records out
22020096 bytes (22 MB) copied, 21.012 s, 1.0 MB/s

此时不用dd命令执行完,稍等一下中断执行就能看到速度确实限制在了1M/s。写的同时,iostat显示为:

[zorro@zorrozou-pc0 ~]$ iostat -x 1
Linux 4.3.3-2-ARCH (zorrozou-pc0.tencent.com) 2016年01月15日 x86_64 (4 CPU)

avg-cpu: %user %nice %system %iowait %steal %idle
0.50 0.00 0.50 25.13 0.00 73.87

Device: rrqm/s wrqm/s r/s w/s rkB/s wkB/s avgrq-sz avgqu-sz await r_await w_await svctm %util
sda 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00
sdb 0.00 0.00 0.00 1.00 0.00 1024.00 2048.00 0.00 0.00 0.00 0.00 0.00 0.00
dm-0 0.00 0.00 0.00 1.00 0.00 1024.00 2048.00 1.00 1000.00 0.00 1000.00 1000.00 100.00
dm-1 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00
dm-2 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00
dm-3 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00

avg-cpu: %user %nice %system %iowait %steal %idle
1.25 0.00 0.50 24.81 0.00 73.43

Device: rrqm/s wrqm/s r/s w/s rkB/s wkB/s avgrq-sz avgqu-sz await r_await w_await svctm %util
sda 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00
sdb 0.00 5.00 0.00 6.00 0.00 1060.00 353.33 0.06 9.33 0.00 9.33 9.50 5.70
dm-0 0.00 0.00 0.00 10.00 0.00 1060.00 212.00 1.08 109.00 0.00 109.00 100.00 100.00
dm-1 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00
dm-2 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00
dm-3 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00

avg-cpu: %user %nice %system %iowait %steal %idle
1.25 0.00 1.00 24.44 0.00 73.32

Device: rrqm/s wrqm/s r/s w/s rkB/s wkB/s avgrq-sz avgqu-sz await r_await w_await svctm %util
sda 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00
sdb 0.00 0.00 0.00 1.00 0.00 1024.00 2048.00 0.00 0.00 0.00 0.00 0.00 0.00
dm-0 0.00 0.00 0.00 1.00 0.00 1024.00 2048.00 1.00 993.00 0.00 993.00 1000.00 100.00
dm-1 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00
dm-2 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00
dm-3 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00

avg-cpu: %user %nice %system %iowait %steal %idle
1.50 0.25 0.75 24.50 0.00 73.00

Device: rrqm/s wrqm/s r/s w/s rkB/s wkB/s avgrq-sz avgqu-sz await r_await w_await svctm %util
sda 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00
sdb 0.00 0.00 0.00 1.00 0.00 1024.00 2048.00 0.00 0.00 0.00 0.00 0.00 0.00
dm-0 0.00 0.00 0.00 1.00 0.00 1024.00 2048.00 1.00 1000.00 0.00 1000.00 1000.00 100.00
dm-1 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00
dm-2 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00
dm-3 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00

可以看到写的速度确实为1024wkB/s左右。我们再来试试读,先创建一个大文件,此处没有限速:

[root@zorrozou-pc0 zorro]# dd if=/dev/zero of=/home/test oflag=direct count=1024 bs=1M
1024+0 records in
1024+0 records out
1073741824 bytes (1.1 GB) copied, 10.213 s, 105 MB/s

然后进行限速设置并确认:

[root@zorrozou-pc0 zorro]# sync
[root@zorrozou-pc0 zorro]# echo 3 > /proc/sys/vm/drop_caches
[root@zorrozou-pc0 zorro]# echo “8:16 1048576” > /sys/fs/cgroup/blkio/test1/blkio.throttle.read_bps_device
[root@zorrozou-pc0 zorro]# cgexec -g blkio:test1 dd if=/home/test of=/dev/null iflag=direct count=1024 bs=1M
^C15+0 records in
14+0 records out
14680064 bytes (15 MB) copied, 15.0032 s, 978 kB/s

iostat结果:

avg-cpu: %user %nice %system %iowait %steal %idle
0.75 0.00 0.75 24.63 0.00 73.88

Device: rrqm/s wrqm/s r/s w/s rkB/s wkB/s avgrq-sz avgqu-sz await r_await w_await svctm %util
sda 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00
sdb 0.00 0.00 2.00 0.00 1024.00 0.00 1024.00 0.00 0.00 0.00 0.00 0.00 0.00
dm-0 0.00 0.00 2.00 0.00 1024.00 0.00 1024.00 1.65 825.00 825.00 0.00 500.00 100.00
dm-1 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00
dm-2 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00
dm-3 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00

avg-cpu: %user %nice %system %iowait %steal %idle
0.75 0.00 0.50 24.87 0.00 73.87

Device: rrqm/s wrqm/s r/s w/s rkB/s wkB/s avgrq-sz avgqu-sz await r_await w_await svctm %util
sda 0.00 2.00 0.00 2.00 0.00 16.00 16.00 0.02 10.00 0.00 10.00 10.00 2.00
sdb 0.00 0.00 2.00 0.00 1024.00 0.00 1024.00 0.00 0.00 0.00 0.00 0.00 0.00
dm-0 0.00 0.00 2.00 0.00 1024.00 0.00 1024.00 1.65 825.00 825.00 0.00 500.00 100.00
dm-1 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00
dm-2 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00
dm-3 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00

最后是iops的限制,我就不废话了,直接上命令执行结果:

[root@zorrozou-pc0 zorro]# echo “8:16 20” > /sys/fs/cgroup/blkio/test1/blkio.throttle.write_iops_device
[root@zorrozou-pc0 zorro]# rm /home/test
[root@zorrozou-pc0 zorro]# sync
[root@zorrozou-pc0 zorro]# echo 3 > /proc/sys/vm/drop_caches
[root@zorrozou-pc0 zorro]# cgexec -g blkio:test1 dd if=/dev/zero of=/home/test oflag=direct count=1024 bs=1M
^C121+0 records in
121+0 records out
126877696 bytes (127 MB) copied, 12.0576 s, 10.5 MB/s

[zorro@zorrozou-pc0 ~]$ iostat -x 1
avg-cpu: %user %nice %system %iowait %steal %idle
0.50 0.00 0.25 24.81 0.00 74.44

Device: rrqm/s wrqm/s r/s w/s rkB/s wkB/s avgrq-sz avgqu-sz await r_await w_await svctm %util
sda 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00
sdb 0.00 0.00 0.00 20.00 0.00 10240.00 1024.00 0.00 0.00 0.00 0.00 0.00 0.00
dm-0 0.00 0.00 0.00 20.00 0.00 10240.00 1024.00 2.00 100.00 0.00 100.00 50.00 100.00
dm-1 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00
dm-2 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00
dm-3 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00

avg-cpu: %user %nice %system %iowait %steal %idle
0.75 0.00 0.25 24.31 0.00 74.69

Device: rrqm/s wrqm/s r/s w/s rkB/s wkB/s avgrq-sz avgqu-sz await r_await w_await svctm %util
sda 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00
sdb 0.00 0.00 0.00 20.00 0.00 10240.00 1024.00 0.00 0.00 0.00 0.00 0.00 0.00
dm-0 0.00 0.00 0.00 20.00 0.00 10240.00 1024.00 2.00 100.00 0.00 100.00 50.00 100.00
dm-1 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00
dm-2 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00
dm-3 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00

iops的读限制我就不在废话了,大家可以自己做实验测试一下。

##其他相关文件

###针对权重比例限制的相关文件

blkio.leaf_weight[_device]

其意义等同于blkio.weight[_device],主要表示当本cgroup中有子cgroup的时候,本cgroup的进程和子cgroup中的进程所分配的资源比例是怎么样的。举个例子说吧,假设有一组cgroups的关系是这样的:

root
/ | \
A B leaf
400 200 200

leaf就表示root组下的进程所占io资源的比例。
此时A组中的进程可以占用的比例为:400/(400+200+200) * 100% = 50%
B为:200/(400+200+200) * 100% = 25%
而root下的进程为:200/(400+200+200) * 100% = 25%

blkio.time

统计相关设备的分配给本组的io处理时间,单位为ms。权重就是依据此时间比例进行分配的。

blkio.sectors

统计本cgroup对设备的读写扇区个数。

blkio.io_service_bytes

统计本cgroup对设备的读写字节个数。

blkio.io_serviced

统计本cgroup对设备的读写操作个数。

blkio.io_service_time

统计本cgroup对设备的各种操作时间。时间单位是ns。

blkio.io_wait_time

统计本cgroup对设备的各种操作的等待时间。时间单位是ns。

blkio.io_merged

统计本cgroup对设备的各种操作的合并处理次数。

blkio.io_queued

统计本cgroup对设备的各种操作的当前正在排队的请求个数。

blkio.*_recursive

这一堆文件是相对应的不带_recursive的文件的递归显示版本,所谓递归的意思就是,它会显示出包括本cgroup在内的衍生cgroup的所有信息的总和。

###针对带宽和iops限制的相关文件

blkio.throttle.io_serviced

统计本cgroup对设备的读写操作个数。

blkio.throttle.io_service_bytes

统计本cgroup对设备的读写字节个数。

blkio.reset_stats

对本文件写入一个int可以对以上所有文件的值置零,重新开始累计。

##最后

其实一直纠结要不要写这部分IO隔离的文档,因为看上去意义不大。一则因为目前IO隔离似乎工作场景里用的不多,二则因为目前内核中这部分代码还在进行较大变化的调整,还要继续加入其它功能。从内核Linux 3.16版本之后,cgroup调整方向,开始了基于unified hierarchy架构的cgroup v2。IO部分在write-back部分进行了较大调整,加入了对buffered IO的资源限制。我们这次系统环境为ArchLinux,内核版本为Linux 4.3.3,虽然环境中的unified hierarchy的开发版本功能已经部分支持了,但是思考再三还是暂时不加入到此文档中。新架构的cgoup v2预计会跟随Linux 4.5一起推出,到时候我们再做详细分析好了。

您的支持是我持续写下去的动力,所以本文无耻的接受捐赠。如果你觉得值得,请刷下面二维码,捐赠九毛九。

mm_facetoface_collect_qrcode_1465221734716

附送一张更详细的Linux 4.0 IO协议栈框架图

Linux IO协议栈框架图

Cgroup – Linux内存资源管理

#Cgroup – Linux内存资源管理
Zorro] icon

Hi,我是Zorro。这是我的微博地址,我会不定期在这里更新文章,如果你有兴趣,可以来关注我呦。

另外,我的其他联系方式:

Email: <mini.jerry@gmail.com>

QQ: 30007147

您的支持是我持续写下去的动力,所以本文无耻的接受捐赠。如果你觉得值得,请刷下面二维码,捐赠九毛九。

mm_facetoface_collect_qrcode_1465221734716

在聊cgroup的内存限制之前,我们有必要先来讲解一下:

##Linux内存管理基础知识

###free命令

无论从任何角度看,Linux的内存管理都是一坨麻烦的事情,当然我们也可以用一堆、一片、一块、一筐来形容这个事情,但是毫无疑问,用一坨来形容它简直恰当无比。在理解它之前,我甚至不会相信精妙的和恶心可以同时形容同一件事情,是的,在我看来它就是这样的。其实我只是做个铺垫,让大家明白,我们下面要讲的内容,绝不是一个成体系的知识,所以,学习起来也确实很麻烦。甚至,我写这个技术文章之前一度考虑了很久该怎么写?从哪里开始写?思考了半天,还是不能免俗,我们无奈,仍然先从free命令说起:

[root@zorrozou-pc ~]# free
total used free shared buffers cached
Mem: 131904480 6681612 125222868 0 478428 4965180
-/+ buffers/cache: 1238004 130666476
Swap: 2088956 0 2088956

这个命令几乎是每一个使用过Linux的人必会的命令,但越是这样的命令,似乎真正明白的人越少(我是说比例越少)。一般情况下,对此命令的理解可以分这几个阶段:

  1. 我擦,内存用了好多,6个多G,可是我什么都没有运行啊?为什么会这样?Linux好占内存。
  2. 嗯,根据我专业的眼光看出来,内存才用了1G多点,还有很多剩余内存可用。buffers/cache占用的较多,说明系统中有进程曾经读写过文件,但是不要紧,这部分内存是当空闲来用的。
  3. free显示的是这样,好吧我知道了。神马?你问我这些内存够不够,我当然不知道啦!我特么怎么知道你程序怎么写的?

如果你的认识在第一种阶段,那么请你继续补充关于Linux的buffers/cache的知识。如果你处在第二阶段,好吧,你已经是个老手了,但是需要提醒的是,上帝给你关上一扇门的同时,肯定都会给你放一条狗的。是的,Linux的策略是:内存是用来用的,而不是用来看的。但是,只要是用了,就不是没有成本的。有什么成本,凭你对buffer/cache的理解,应该可以想的出来。一般我比较认同第三种情况,一般光凭一个free命令的显示,是无法判断出任何有价值的信息的,我们需要结合业务的场景以及其他输出综合判断目前遇到的问题。当然也可能这种人给人的第一感觉是他很外行,或者他真的是外行。

无论如何,free命令确实给我门透露了一些有用的信息,比如内存总量,剩余多少,多少用在了buffers/cache上,Swap用了多少,如果你用了其它参数还能看到一些其它内容,这里不做一一列举。那么这里又引申出另一些概念,什么是buffer?什么是cache?什么是swap?由此我们就直接引出另一个命令:

[root@zorrozou-pc ~]# cat /proc/meminfo
MemTotal: 131904480 kB
MemFree: 125226660 kB
Buffers: 478504 kB
Cached: 4966796 kB
SwapCached: 0 kB
Active: 1774428 kB
Inactive: 3770380 kB
Active(anon): 116500 kB
Inactive(anon): 3404 kB
Active(file): 1657928 kB
Inactive(file): 3766976 kB
Unevictable: 0 kB
Mlocked: 0 kB
SwapTotal: 2088956 kB
SwapFree: 2088956 kB
Dirty: 336 kB
Writeback: 0 kB
AnonPages: 99504 kB
Mapped: 20760 kB
Shmem: 20604 kB
Slab: 301292 kB
SReclaimable: 229852 kB
SUnreclaim: 71440 kB
KernelStack: 3272 kB
PageTables: 3320 kB
NFS_Unstable: 0 kB
Bounce: 0 kB
WritebackTmp: 0 kB
CommitLimit: 68041196 kB
Committed_AS: 352412 kB
VmallocTotal: 34359738367 kB
VmallocUsed: 493196 kB
VmallocChunk: 34291062284 kB
HardwareCorrupted: 0 kB
AnonHugePages: 49152 kB
HugePages_Total: 0
HugePages_Free: 0
HugePages_Rsvd: 0
HugePages_Surp: 0
Hugepagesize: 2048 kB
DirectMap4k: 194816 kB
DirectMap2M: 3872768 kB
DirectMap1G: 132120576 kB

以上显示的内容都是些什么鬼?

其实这个问题的答案也是另一个问题的答案,即:Linux是如何使用内存的?了解清楚这个问题是很有必要的,因为只有先知道了Linux如何使用内存,我们在能知道内存可以如何限制,以及,做了限制之后会有什么问题?我们在此先例举出几个常用概念的意义:

内存,作为一种相对比较有限的资源,内核在考虑其管理时,无非应该主要从以下出发点考虑:

  1. 内存够用时怎么办?
  2. 内存不够用时怎么办?

在内存够用时,内核的思路是,如何尽量提高资源的利用效率,以加快系统整体响应速度和吞吐量?于是内存作为一个CPU和I/O之间的大buffer的功能就呼之欲出了。为此,内核设计了以下系统来做这个功能:
###Buffers/Cached
buffer和cache是两个在计算机技术中被用滥的名词,放在不通语境下会有不同的意义。在内存管理中,我们需要特别澄清一下,这里的buffer指Linux内存的:Buffer cache。这里的cache指Linux内存中的:Page cache。翻译成中文可以叫做缓冲区缓存和页面缓存。在历史上,它们一个(buffer)被用来当成对io设备写的缓存,而另一个(cache)被用来当作对io设备的读缓存,这里的io设备,主要指的是块设备文件和文件系统上的普通文件。但是现在,它们的意义已经不一样了。在当前的内核中,page cache顾名思义就是针对内存页的缓存,说白了就是,如果有内存是以page进行分配管理的,都可以使用page cache作为其缓存来使用。当然,不是所有的内存都是以页(page)进行管理的,也有很多是针对块(block)进行管理的,这部分内存使用如果要用到cache功能,则都集中到buffer cache中来使用。(从这个角度出发,是不是buffer cache改名叫做block cache更好?)然而,也不是所有块(block)都有固定长度,系统上块的长度主要是根据所使用的块设备决定的,而页长度在X86上无论是32位还是64位都是4k。

而明白了这两套缓存系统的区别,也就基本可以理解它们究竟都可以用来做什么了。

什么是page cache

Page cache主要用来作为文件系统上的文件数据的缓存来用,尤其是针对当进程对文件有read/write操作的时候。如果你仔细想想的话,作为可以映射文件到内存的系统调用:mmap是不是很自然的也应该用到page cache?如果你再仔细想想的话,malloc会不会用到page cache?

以上提出的问题都请自己思考,本文档不会给出标准答案。

在当前的实现里,page cache也被作为其它文件类型的缓存设备来用,所以事实上page cache也负责了大部分的块设备文件的缓存工作。

什么是buffer cache

Buffer cache则主要是设计用来在系统对块设备进行读写的时候,对块进行数据缓存的系统来使用。但是由于page cache也负责块设备文件读写的缓存工作,于是,当前的buffer cache实际上要负责的工作比较少。这意味着某些对块的操作会使用buffer cache进行缓存,比如我们在格式化文件系统的时候。

一般情况下两个缓存系统是一起配合使用的,比如当我们对一个文件进行写操作的时候,page cache的内容会被改变,而buffer cache则可以用来将page标记为不同的缓冲区,并记录是哪一个缓冲区被修改了。这样,内核在后续执行脏数据的回写(writeback)时,就不用将整个page写回,而只需要写回修改的部分即可。

有搞大型系统经验的人都知道,缓存就像万金油,只要哪里有速度差异产生的瓶颈,就可以在哪里抹。但是其成本之一就是,需要维护数据的一致性。内存缓存也不例外,内核需要维持其一致性,在脏数据产生较快或数据量较大的时候,缓存系统整体的效率一样会下降,因为毕竟脏数据写回也是要消耗IO的。这个现象也会表现在这样一种情况下,就是当你发现free的时候,内存使用量较大,但是去掉了buffer/cache的使用之后剩余确很多。以一般的理解,都会认为此时进程如果申请内存,内核会将buffer/cache占用的内存当成空闲的内存分给进程,这是没错的。但是其成本是,在分配这部分已经被buffer/cache占用的内存的时候,内核会先对其上面的脏数据进行写回操作,保证数据一致后才会清空并分给进程使用。如果此时你的进程是突然申请大量内存,而且你的业务是一直在产生很多脏数据(比如日志),并且系统没有及时写回的时候,此时系统给进程分配内存的效率会很慢,系统IO也会很高。那么此时你还以为buffer/cache可以当空闲内存使用么?

思考题:Linux什么时候会将脏数据写回到外部设备上?这个过程如何进行人为干预?

这足可以证明一点,以内存管理的复杂度,我们必须结合系统上的应用状态来评估系统监控命令所给出的数据,才是做评估的正确途径。如果你不这样做,那么你就可以轻而易举的得出“Linux系统好烂啊!“这样的结论。也许此时,其实是你在这个系统上跑的应用很烂的缘故导致的问题。

接下来,当内存不够用的时候怎么办?

我们好像已经分析了一种内存不够用的状态,就是上述的大量buffer/cache把内存几乎占满的情况。但是基于Linux对内存的使用原则,这不算是不够用,但是这种状态导致IO变高了。我们进一步思考,假设系统已经清理了足够多的buffer/cache分给了内存,而进程还在嚷嚷着要内存咋办?

此时内核就要启动一系列手段来让进程尽量在此时能够正常的运行下去。

请注意我在这说的是一种异常状态!我之所以要这样强调是因为,很多人把内存用满了当称一种正常状态。他们认为,当我的业务进程在内存使用到压力边界的情况下,系统仍然需要保证让业务进程有正常的状态!这种想法显然是缘木求鱼了。另外我还要强调一点,系统提供的是内存管理的机制和手段,而内存用的好不好,主要是业务进程的事情,责任不能本末倒置。

###谁该SWAP?

首先是Swap机制。Swap是交换技术,这种技术是指,当内存不够用的时候,我们可以选择性的将一块磁盘、分区或者一个文件当成交换空间,将内存上一些临时用不到的数据放到交换空间上,以释放内存资源给急用的进程。

哪些数据可能会被交换出去呢?从概念上判断,如果一段内存中的数据被经常访问,那么就不应该被交换到外部设备上,因为这样的数据如果交换出去的话会导致系统响应速度严重下降。内存管理需要将内存区分为活跃的(Active)和不活跃的(Inactive),再加上一个进程使用的用户空间内存映射包括文件影射(file)和匿名影射(anon),所以就包括了Active(anon)、Inactive(anon)、Active(file)和Inactive(file)。你说神马?啥是文件影射(file)和匿名影射(anon)?好吧,我们可以这样简单的理解,匿名影射主要是诸如进程使用malloc和mmap的MAP_ANONYMOUS的方式申请的内存,而文件影射就是使用mmap影射的文件系统上的文件,这种文件系统上的文件既包括普通的文件,也包括临时文件系统(tmpfs)。这意味着,Sys V的IPC和POSIX的IPC(IPC是进程间通信机制,在这里主要指共享内存,信号量数组和消息队列)都是通过文件影射方式体现在用户空间内存中的。这两种影射的内存都会被算成进程的RSS,但是也一样会被显示在cache的内存计数中,在相关cgroup的另一项统计中,共享内存的使用和文件缓存(file cache)也都会被算成是cgroup中的cache使用的总量。这个统计显示的方法是:

[root@zorrozou-pc ~]# cat /cgroup/memory/memory.stat
cache 94429184
rss 102973440
rss_huge 50331648
mapped_file 21512192
swap 0
pgpgin 656572990
pgpgout 663474908
pgfault 2871515381
pgmajfault 1187
inactive_anon 3497984
active_anon 120524800
inactive_file 39059456
active_file 34484224
unevictable 0
hierarchical_memory_limit 9223372036854775807
hierarchical_memsw_limit 9223372036854775807
total_cache 94429184
total_rss 102969344
total_rss_huge 50331648
total_mapped_file 21520384
total_swap 0
total_pgpgin 656572990
total_pgpgout 663474908
total_pgfault 2871515388
total_pgmajfault 1187
total_inactive_anon 3497984
total_active_anon 120524800
total_inactive_file 39059456
total_active_file 34484224
total_unevictable 0

好吧,说了这么半天终于联系到一个cgroup的内存限制相关的文件了。在这需要说明的是,你之所以看见我废话这么多,是因为我们必须先基本理清楚Linux系统的内存管理方式,才能进一步对cgroup中的内存限制做规划使用,否则同样的名词会有很多的歧义。就比如我们在观察某一个cgroup中的cache占用数据的时候,我们究竟该怎么理解它?真的把它当成空闲空间来看么?

我们撤的有点远,回过头来说说这些跟Swap有什么关系?还是刚才的问题,什么内容该被从内存中交换出去呢?文件cache是一定不需要的,因为既然是cache,就意味着它本身就是硬盘上的文件(当然你现在应该知道了,它也不仅仅只有文件),那么如果是硬盘上的文件,就不用swap交换出去,只要写回脏数据,保持数据一致之后清除就可以了,这就是刚才说过的缓存清楚机制。但是我们同时也要知道,并不是所有被标记为cache的空间都能被写回硬盘的(是的,比如共享内存)。那么能交换出去内存应该主要包括有Inactive(anon)这部分内存。主要注意的是,内核也将共享内存作为计数统计近了Inactive(anon)中去了(是的,共享内存也可以被Swap)。还要补充一点,如果内存被mlock标记加锁了,则也不会交换,这是对内存加mlock锁的唯一作用。刚才我们讨论的这些计数,很可能会随着Linux内核的版本改变而产生变化,但是在比较长的一段时间内,我们可以这样理解。

我们基本搞清了swap这个机制的作用效果,那么既然swap是内部设备和外部设备的数据拷贝,那么加一个缓存就显得很有必要,这个缓存就是swapcache,在memory.stat文件中,swapcache是跟anon page被一起记录到rss中的,但是并不包含共享内存。另外再说明一下,HugePages也是不会交换的。显然,当前的swap空间用了多少,总共多少,这些我们也可以在相关的数据中找到答案。

以上概念中还有一些名词大家可能并不清楚其含义,比如RSS或HugePages。请自行查资料补上这些知识。为了让大家真的理解什么是RSS,请思考ps aux命令中显示的VSZ,RSS和cat /proc/pid/smaps中显示的:PSS这三个进程占用内存指标的差别?

您的支持是我持续写下去的动力,所以本文无耻的接受捐赠。如果你觉得值得,请刷下面二维码,捐赠九毛九。

mm_facetoface_collect_qrcode_1465221734716

###何时SWAP?
搞清楚了谁该swap,那么还要知道什么时候该swap。这看起来比较简单,内存耗尽而且cache也没什么可以回收的时候就应该触发swap。其实现实情况也没这么简单,实际上系统在内存压力可能不大的情况下也会swap,这种情况并不是我们今天要讨论的范围。

思考题:除了内存被耗尽的时候要swap,还有什么时候会swap?如何调整内核swap的行为?如何查看当前系统的swap空间有哪些?都是什么类型?什么是swap权重?swap权重有什么意义?

其实绝大多数场景下,什么时候swap并不重要,而swap之后的事情相对却更重要。大多数的内存不够用,只是临时不够用,比如并发突增等突发情况,这种情况的特点是时间持续短,此时swap机制作为一种临时的中转措施,可以起到对业务进程的保护作用。因为如果没有swap,内存耗尽的结果一般都是触发oom killer,会杀掉此时积分比较高的进程。如果更严重的话,内存不够用还会触发进程D状态死锁,这一般发生在多个进程同时要申请内存的时候,此时oom killer机制也可能会失效,因为需要被干掉的积分比较高的进程很可能就是需要申请内存的进程,而这个进程本身因为正在争抢内存而导致陷入D状态,那么此时kill就可能是对它无效的。

但是swap也不是任何时候都有很好的保护效果。如果内存申请是长期并大量的,那么交换出去的数据就会因为长时间驻留在外部设备上,导致进程调用这段内存的几率大大增加,当进程很频繁的使用它已经被交换出去的内存时,就会让整个系统处在io繁忙的状态,此时进程的响应速度会严重下降,导致整个系统夯死。对于系统管理员来说,这种情况是完全不能接受的,因为故障之后的第一要务是赶紧恢复服务,但是swap频繁使用的IO繁忙状态会导致系统除了断电重启之外,没有其它可靠手段可以让系统从这种状态中恢复回来,所以这种情况是要尽力避免的。此时,如果有必要,我们甚至可以考虑不用swap,哪怕内存过量使用被oom,或者进程D状态都是比swap导致系统卡死的情况更好处理的状态。如果你的环境需求是这样的,那么可以考虑关闭swap。

###进程申请内存的时候究竟会发生什么?

刚才我们从系统宏观的角度简要说明了一下什么是buffer/cache以及swap。下面我们从一个更加微观的角度来把一个内存申请的过程以及相关机制什么时候触发给串联起来。本文描述的过程是基于Linux 3.10内核版本的,Linux 4.1基本过程变化不大。如果你想确认在你的系统上究竟是什么样子,请自行翻阅相关内核代码。

进程申请内存可能用到很多种方法,最常见的就是malloc和mmap。但是这对于我们并不重要,因为无论是malloc还是mmap,或是其他的申请内存的方法,都不会真正的让内核去给进程分配一个实际的物理内存空间。真正会触发分配物理内存的行为是缺页异常

缺页异常就是我们可以在memory.stat中看到的total_pgfault,这种异常一般分两种,一种叫major fault,另一种叫minor fault。这两种异常的主要区别是,进程所请求的内存数据是否会引发磁盘io?如果会引发,就是一个majfault,如果不引发,那就是minfault。就是说如果产生了major fault,这个数据基本上就意味着已经被交换到了swap空间上。

缺页异常的处理过程大概可以整理为以下几个路径:

首先检查要访问的虚拟地址是否合法,如果合法则继续查找和分配一个物理页,步骤如下:

  1. 检查发生异常的虚拟地址是不是在物理页表中不存在?如果是,并且是匿名影射,则申请置0的匿名影射内存,此时也有可能是影射了某种虚拟文件系统,比如共享内存,那么就去影射相关的内存区,或者发生COW写时复制申请新内存。如果是文件影射,则有两种可能,一种是这个影射区是一个page cache,直接将相关page cache区影射过来即可,或者COW新内存存放需要影射的文件内容。如果page cache中不存在,则说明这个区域已经被交换到swap空间上,应该去处理swap。
  2. 如果页表中已经存在需要影射的内存,则检查是否要对内存进行写操作,如果不写,那就直接复用,如果要写,就发生COW写时复制,此时的COW跟上面的处理过程不完全相同,在内核中,这里主要是通过do_wp_page方法实现的。

如果需要申请新内存,则都会通过alloc_page_vma申请新内存,而这个函数的核心方法是__alloc_pages_nodemask,也就是Linux内核著名的内存管理系统**伙伴系统**的实现。

分配过程先会检查空闲页表中有没有页可以申请,实现方法是:get_page_from_freelist,我们并不关心正常情况,分到了当然一切ok。更重要的是异常处理,如果空闲中没有,则会进入__alloc_pages_slowpath方法进行处理。这个处理过程的主逻辑大概这样:

  1. 唤醒kswapd进程,把能换出的内存换出,让系统有内存可用。
  2. 继续检查看看空闲中是否有内存。有了就ok,没有继续下一步:
  3. 尝试清理page cache,清理的时候会将进程置为D状态。如果还申请不到内存则:
  4. 启动oom killer干掉一些进程释放内存,如果这样还不行则:
  5. 回到步骤1再来一次!

当然以上逻辑要符合一些条件,但是这一般都是系统默认的状态,比如,你必须启用oom killer机制等。另外这个逻辑中有很多其它状态与本文无关,比如检查内存水印、检查是否是高优先级内存申请等等,当然还有关于numa节点状态的判断处理,我没有一一列出。另外,以上逻辑中,不仅仅只有清理cache的时候会使进程进入D状态,还有其它逻辑也会这样做。这就是为什么在内存不够用的情况下,oom killer有时也不生效,因为可能要干掉的进程正好陷入这个逻辑中的D状态了。

以上就是内存申请中,大概会发生什么的过程。当然,我们这次主要是真对本文的重点cgroup内存限制进行说明,当我们处理限制的时候,更多需要关心的是当内存超限了会发生什么?对边界条件的处理才是我们这次的主题,所以我并没有对正常申请到的情况做细节说明,也没有对用户态使用malloc什么时候使用sbrk还是mmap来申请内存做出细节说明,毕竟那是程序正常状态的时候的事情,后续可以另写一个内存优化的文章主要讲解那部分。

下面我们该进入正题了:

##Cgroup内存限制的配置

当限制内存时,我们最好先想清楚如果内存超限了会发生什么?该怎么处理?业务是否可以接受这样的状态?这就是为什么我们在讲如何限制之前说了这么多基础知识的“废话”。其实最简单的莫过于如何进行限制了,我们的系统环境还是沿用上一次讲解CPU内存隔离的环境,使用cgconfig和cgred服务进行cgroup的配置管理。还是创建一个zorro用户,对这个用户产生的进程进行内存限制。基础配置方法不再多说,如果不知道的请参考这个文档

环境配置好之后,我们就可以来检查相关文件了。内存限制的相关目录根据cgconfig.config的配置放在了/cgroup/memory目录中,如果你跟我做了一样的配置,那么这个目录下的内容应该是这样的:

[root@zorrozou-pc ~]# ls /cgroup/memory/
cgroup.clone_children memory.failcnt memory.kmem.slabinfo memory.kmem.usage_in_bytes memory.memsw.limit_in_bytes memory.oom_control memory.usage_in_bytes shrek
cgroup.event_control memory.force_empty memory.kmem.tcp.failcnt memory.limit_in_bytes memory.memsw.max_usage_in_bytes memory.pressure_level memory.use_hierarchy tasks
cgroup.procs memory.kmem.failcnt memory.kmem.tcp.limit_in_bytes memory.max_usage_in_bytes memory.memsw.usage_in_bytes memory.soft_limit_in_bytes zorro
cgroup.sane_behavior memory.kmem.limit_in_bytes memory.kmem.tcp.max_usage_in_bytes memory.meminfo memory.move_charge_at_immigrate memory.stat notify_on_release
jerry memory.kmem.max_usage_in_bytes memory.kmem.tcp.usage_in_bytes memory.memsw.failcnt memory.numa_stat memory.swappiness release_agent

其中,zorro、jerry、shrek都是目录概念跟cpu隔离的目录树结构类似。相关配置文件内容:

[root@zorrozou-pc ~]# cat /etc/cgconfig.conf mount {
cpu = /cgroup/cpu;
cpuset = /cgroup/cpuset;
cpuacct = /cgroup/cpuacct;
memory = /cgroup/memory;
devices = /cgroup/devices;
freezer = /cgroup/freezer;
net_cls = /cgroup/net_cls;
blkio = /cgroup/blkio;
}

group zorro {
cpu {
cpu.shares = 6000;

cpu.cfs_quota_us = “600000”;

}
cpuset {

cpuset.cpus = “0-7,12-19”;

cpuset.mems = “0-1”;

}
memory {
}
}

配置中添加了一个真对memory的空配置项,我们稍等下再给里面添加配置。

[root@zorrozou-pc ~]# cat /etc/cgrules.conf
zorro cpu,cpuset,cpuacct,memory zorro
jerry cpu,cpuset,cpuacct,memory jerry
shrek cpu,cpuset,cpuacct,memory shrek

文件修改完之后记得重启相关服务:

[root@zorrozou-pc ~]# service cgconfig restart
[root@zorrozou-pc ~]# service cgred restart

让我们继续来看看真对内存都有哪些配置参数:

[root@zorrozou-pc ~]# ls /cgroup/memory/zorro/
cgroup.clone_children memory.kmem.failcnt memory.kmem.tcp.limit_in_bytes memory.max_usage_in_bytes memory.memsw.usage_in_bytes memory.soft_limit_in_bytes
cgroup.event_control memory.kmem.limit_in_bytes memory.kmem.tcp.max_usage_in_bytes memory.meminfo memory.move_charge_at_immigrate memory.stat notify_on_release
cgroup.procs memory.kmem.max_usage_in_bytes memory.kmem.tcp.usage_in_bytes memory.memsw.failcnt memory.numa_stat memory.swappiness tasks
memory.failcnt memory.kmem.slabinfo memory.kmem.usage_in_bytes memory.memsw.limit_in_bytes memory.oom_control memory.usage_in_bytes
memory.force_empty memory.kmem.tcp.failcnt memory.limit_in_bytes memory.memsw.max_usage_in_bytes memory.pressure_level memory.use_hierarchy

首先我们已经认识了memory.stat文件了,这个文件内容不能修改,它实际上是输出当前cgroup相关内存使用信息的。常见的数据及其含义我们刚才也已经说过了,在此不再复述。

您的支持是我持续写下去的动力,所以本文无耻的接受捐赠。如果你觉得值得,请刷下面二维码,捐赠九毛九。

mm_facetoface_collect_qrcode_1465221734716

###cgroup内存限制

memory.memsw.limit_in_bytes:内存+swap空间使用的总量限制。

memory.limit_in_bytes:内存使用量限制。

这两项的意义很清楚了,如果你决定在你的cgroup中关闭swap功能,可以把两个文件的内容设置为同样的值即可。至于为什么相信大家都能想清楚。

###OOM控制

memory.oom_control:内存超限之后的oom行为控制。
这个文件中有两个值:

oom_kill_disable 0

默认为0表示打开oom killer,就是说当内存超限时会触发干掉进程。如果设置为1表示关闭oom killer,此时内存超限不会触发内核杀掉进程。而是将进程夯住(hang/sleep),实际上内核中就是将进程设置为D状态,并且将相关进程放到一个叫做OOM-waitqueue的队列中。这时的进程可以kill杀掉。如果你想继续让这些进程执行,可以选择这样几个方法:

  1. 增加内存,让进程有内存可以继续申请。
  2. 杀掉一些进程,让本组内有内存可用。
  3. 把一些进程移到别的cgroup中,让本cgroup内有内存可用。
  4. 删除一些tmpfs的文件,就是占用内存的文件,比如共享内存或者其它会占用内存的文件。

说白了就是,此时只有当cgroup中有更多内存可以用了,在OOM-waitqueue队列中被挂起的进程就可以继续运行了。

under_oom 0

这个值只是用来看的,它表示当前的cgroup的状态是不是已经oom了,如果是,这个值将显示为1。我们就是通过设置和监测这个文件中的这两个值来管理cgroup内存超限之后的行为的。在默认场景下,如果你使用了swap,那么你的cgroup限制内存之后最常见的异常效果是IO变高,如果业务不能接受,我们一般的做法是关闭swap,那么cgroup内存oom之后都会触发kill掉进程,如果我们用的是LXC或者Docker这样的容器,那么还可能干掉整个容器。当然也经常会因为kill进程的时候因为进程处在D状态,而导致整个Docker或者LXC容器根本无法被杀掉。至于原因,在前面已经说的很清楚了。当我们遇到这样的困境时该怎么办?一个好的办法是,关闭oom killer,让内存超限之后,进程挂起,毕竟这样的方式相对可控。此时我们可以检查under_oom的值,去看容器是否处在超限状态,然后根据业务的特点决定如何处理业务。我推荐的方法是关闭部分进程或者重启掉整个容器,因为可以想像,容器技术所承载的服务应该是在整体软件架构上有容错的业务,典型的场景是web服务。容器技术的特点就是生存周期短,在这样的场景下,杀掉几个进程或者几个容器,都应该对整体服务的稳定性影响不大,而且容器的启动速度是很快的,实际上我们应该认为,容器的启动速度应该是跟进程启动速度可以相媲美的。你的业务会因为死掉几个进程而表现不稳定么?如果不会,请放心的干掉它们吧,大不了很快再启动起来就是了。但是如果你的业务不是这样,那么请根据自己的情况来制定后续处理的策略。

当我们进行了内存限制之后,内存超限的发生频率要比使用实体机更多了,因为限制的内存量一般都是小于实际物理内存的。所以,使用基于内存限制的容器技术的服务应该多考虑自己内存使用的情况,尤其是内存超限之后的业务异常处理应该如何让服务受影响的程度降到更低。在系统层次和应用层次一起努力,才能使内存隔离的效果达到最好。

###内存资源审计

memory.memsw.usage_in_bytes:当前cgroup的内存+swap的使用量。

memory.usage_in_bytes:当前cgroup的内存使用量。

memory.max_usage_in_bytes:cgroup的最大内存使用量。

memory.memsw.max_usage_in_bytes:cgroup最大的内存+swap的使用量。

这些文件都是只读的,用来查看相关状态信息,只能看不能改。

如果你的内核配置打开了CONFIG_MEMCG_KMEM选项的话,那么可以看到当前cgroup的内核内存使用的限制和状态统计信息,他们都是以memory.kmem开头的文件。你可以通过memory.kmem.limit_in_bytes来限制内核使用的内存大小,通过memory.kmem.slabinfo来查看内核slab分配器的状态。现在还能通过memory.kmem.tcp开头的文件来限制cgroup中使用tcp协议的内存资源使用和状态查看。

所有名字中有failcnt的文件里面的值都是相关资源超限的次数的计数,可以通过echo 0将这些计数重置。如果你的服务器是NUMA架构的话,可以通过memory.numa_stat这个文件来查看cgroup中的NUMA相关状态。memory.swappiness跟/proc/sys/vm/swappiness的概念一致,用来调整cgroup使用swap的状态,如果大家认真做了本文前面的思考题的话,应该知道这个文件是干嘛的,本文不会详细解释关于swappiness的细节算法,以后将在性能调整系列文章中详细解释相关参数。

###内存软限制以及内存超卖

memory.soft_limit_in_bytes:内存软限制。

如果超过了memory.limit_in_bytes所定义的限制,那么进程会被oom killer干掉或者被暂停,这相当于硬限制,因为进程无法申请超过自身cgroup限制的内存,但是软限制确是可以突破的。我们假定一个场景,如果你的实体机上有四个cgroup,实体机的内存总量是64G,那么一般情况我们会考虑给每个cgroup限制到16G内存。但是现实情况并不会这么理想,首先实体机上其他进程和内核会占用部分内存,这将导致实际上每个cgroup都不会真的有16G内存可用,如果四个cgroup都尽量占用内存的话,他们可能谁都不会到达内存的上限触发超限的行为,这可能将导致进程都抢不到内存而被饿死。类似的情况还可能发上在内存超卖的环境中,比如,我们仍然只有64G内存,但是确开了8个cgroup,每个都限制了16G内存。这样每个cgroup分配的内存之和达到了128G,但是实际内存量只有64G。这种情况是出于绝大多数应用可能不会占用满所有的内存来考虑的,这样就可以把本来属于它的那份内存“借用”给其它cgroup。以上这样的情况都会出现类似的问题,就是,如果全局内存已经耗尽了,但是某些cgroup还没达到他的内存使用上限,而它们此时如果要申请内存的话,此时该从哪里回收内存?如果我们配置了memory.soft_limit_in_bytes,那么内核将去回收那些内存超过了这个软限制的cgroup的内存,尽量缩减它们的内存占用达到软限制的量以下,以便让没有达到软限制的cgroup有内存可以用。当然,在没有这样的内存竞争以及没有达到硬限制的情况下,软限制是不会生效的。还有就是,软限制的起作用时间可能会比较长,毕竟内核要平衡多个cgroup的内存使用。

根据软限制的这些特点,我们应该明白如果想要软限制生效,应该把它的值设置成小于硬限制。

您的支持是我持续写下去的动力,所以本文无耻的接受捐赠。如果你觉得值得,请刷下面二维码,捐赠九毛九。

mm_facetoface_collect_qrcode_1465221734716

###进程迁移时的内存charge

memory.move_charge_at_immigrate:打开或者关闭进程迁移时的内存记账信息。

进程可以在多个cgroup之间切换,所以内存限制必须考虑当发生这样的切换时,进程进入的新cgroup中记录的内存使用量是重新从0累计还是把原来cgroup中的信息迁移过来?当这个开关设置为0的时候是关闭这个功能,相当于不累计之前的信息,默认是1,迁移的时候要在新的cgroup中累积(charge)原来信息,并把旧group中的信息给uncharge掉。如果新cgroup中没有足够的空间容纳新来的进程,首先内核会在cgroup内部回收内存,如果还是不够,就会迁移失败。

###内存压力通知机制

最后,内存的资源隔离还提供了一种压力通知机制。当cgoup内的内存使用量达到某种压力状态的时候,内核可以通过eventfd的机制来通知用户程序,这个通知是通过cgroup.event_controlmemory.pressure_level来实现的。使用方法是:

使用eventfd()创建一个eventfd,假设叫做efd,然后open()打开memory.pressure_level的文件路径,产生一个另一个fd,我们暂且叫它cfd,然后将这两个fd和我们要关注的内存压力级别告诉内核,让内核帮我们关注条件是否成立,通知方式就是把以上信息按这样的格式:”<event_fd:efd> “写入cgroup.event_control。然后就可以去等着efd是否可读了,如果能读出信息,则代表内存使用已经触发相关压力条件。

压力级别的level有三个:

“low”:表示内存使用已经达到触发内存回收的压力级别。

“medium”:表示内存使用压力更大了,已经开始触发swap以及将活跃的cache写回文件等操作了。

“critical”:到这个级别,就意味着内存已经达到上限,内核已经触发oom killer了。

程序从efd读出的消息内容就是这三个级别的关键字。我们可以通过这个机制,建立一个内存压力管理系统,在内存达到相应级别的时候,触发响应的管理策略,来达到各种自动化管理的目的。

下面给出一个监控程序的例子:

#include
#include
#include
#include
#include #include #include
#include
#include

#include <sys/eventfd.h>

#define USAGE_STR “Usage: cgroup_event_listener ”

int main(int argc, char **argv)
{
int efd = -1;
int cfd = -1;
int event_control = -1;
char event_control_path[PATH_MAX];
char line[LINE_MAX];
int ret;

if (argc != 3)
errx(1, “%s”, USAGE_STR);

cfd = open(argv[1], O_RDONLY);
if (cfd == -1)
err(1, “Cannot open %s”, argv[1]);

ret = snprintf(event_control_path, PATH_MAX, “%s/cgroup.event_control”,
dirname(argv[1]));
if (ret >= PATH_MAX)
errx(1, “Path to cgroup.event_control is too long”);

event_control = open(event_control_path, O_WRONLY);
if (event_control == -1)
err(1, “Cannot open %s”, event_control_path);

efd = eventfd(0, 0);
if (efd == -1)
err(1, “eventfd() failed”);

ret = snprintf(line, LINE_MAX, “%d %d %s”, efd, cfd, argv[2]);
if (ret >= LINE_MAX)
errx(1, “Arguments string is too long”);

ret = write(event_control, line, strlen(line) + 1);
if (ret == -1)
err(1, “Cannot write to cgroup.event_control”);

while (1) {
uint64_t result;

ret = read(efd, &result, sizeof(result));
if (ret == -1) {
if (errno == EINTR)
continue;
err(1, “Cannot read from eventfd”);
}
assert(ret == sizeof(result));

ret = access(event_control_path, W_OK);
if ((ret == -1) && (errno == ENOENT)) {
puts(“The cgroup seems to have removed.”);
break;
}

if (ret == -1)
err(1, “cgroup.event_control is not accessible any more”);

printf(“%s %s: crossed\n”, argv[1], argv[2]);
}

return 0;

##最后

Linux的内存限制要说的就是这么多了,当我们限制了内存之后,相对于使用实体机,实际上对于应用来说可用内存更少了,所以业务会相对更经常地暴露在内存资源紧张的状态下。相对于虚拟机(kvm,xen),多个cgroup之间是共享内核的,我们可以从内存限制的角度思考一些关于“容器”技术相对于虚拟机和实体机的很多特点:

  1. 内存更紧张,应用的内存泄漏会导致相对更严重的问题。
  2. 容器的生存周期时间更短,如果实体机的开机运行时间是以年计算的,那么虚拟机则是以月计算的,而容器应该跟进程的生存周期差不多,顶多以天为单位。所以,容器里面要跑的应用应该可以被经常重启。
  3. 当有多个cgroup(容器)同时运行时,我们不能再以实体机或者虚拟机对资源的使用的理解来规划整体运营方式,我们需要更细节的理解什么是cache,什么是swap,什么是共享内存,它们会被统计到哪些资源计数中?在内核并不冲突的环境,这些资源都是独立给某一个业务使用的,在理解上即使不是很清晰,也不会造成歧义。但是在cgroup中,我们需要彻底理解这些细节,才能对遇到的情况进行预判,并规划不同的处理策略。

也许我们还可以从中得到更多的理解,大家一起来想喽?

您的支持是我持续写下去的动力,所以本文无耻的接受捐赠。如果你觉得值得,请刷下面二维码,捐赠九毛九。

mm_facetoface_collect_qrcode_1465221734716

Cgroup – 从CPU资源隔离说起

Cgroup – 从CPU资源隔离说起

Zorro] icon

Hi,我是Zorro。这是我的微博地址,我会不定期在这里更新文章,如果你有兴趣,可以来关注我呦。

本文有配套视频演示,一起服用效果更佳。

您的支持是我持续写下去的动力,所以本文无耻的接受捐赠。如果你觉得值得,请刷下面二维码,捐赠九毛九。

mm_facetoface_collect_qrcode_1465221734716

另外,我的其他联系方式:

Email: <mini.jerry@gmail.com>

QQ: 30007147

今天我们来谈谈:

##什么是Cgroup?

cgroups,其名称源自控制组群(control groups)的简写,是Linux内核的一个功能,用来限制,控制与分离一个进程组群的资源(如CPU、内存、磁盘输入输出等)。

–引自维基百科:cgroup

引用官方说法总是那么冰冷的让人不好理解,所以我还是稍微解释一下:

一个正在运行着服务的计算机系统,跟我们高中上课的情景还是很相似的。如果把系统中的每个进程理解为一个同学的话,那么班主任就是操作系统的核心(kernel),负责管理班里的同学。而cgroup,就是班主任控制学生行为的一种手段,所以,它起名叫control groups。

既然是一种控制手段,那么cgroup能控制什么呢?当然是资源啦!对于计算机来说,资源大概可以分成以下几个部分:

  • 计算资源
  • 内存资源
  • io资源
  • 网络资源

这就是我们常说的内核四大子系统。当我们学习内核的时候,我们也基本上是围绕这四大子系统进行研究。
我们今天要讨论的,主要是cgroup是如何对系统中的CPU资源进行隔离和分配的。其他资源的控制,我们以后有空再说喽。

##如何看待CPU资源?

由于进程和线程在Linux的CPU调度看来没啥区别,所以本文后续都会用进程这个名词来代表内核的调度对象,一般来讲也包括线程

如果要分配资源,我们必须先搞清楚这个资源是如何存在的,或者说是如何组织的。我想CPU大家都不陌生,我们都在系统中用过各种工具查看过CPU的使用率,比如说以下这个命令和它的输出:

[zorro@zorrozou-pc0 ~]$ mpstat -P ALL 1 1
Linux 4.2.5-1-ARCH (zorrozou-pc0) 2015年12月22日 x86_64 (4 CPU)mt

16时01分08秒 CPU %usr %nice %sys %iowait %irq %soft %steal %guest %gnice %idle
16时01分09秒 all 0.25 0.00 0.25 0.00 0.00 0.00 0.00 0.00 0.00 99.50
16时01分09秒 0 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 100.00
16时01分09秒 1 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 100.00
16时01分09秒 2 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 100.00
16时01分09秒 3 0.00 0.00 1.00 0.00 0.00 0.00 0.00 0.00 0.00 99.00

Average: CPU %usr %nice %sys %iowait %irq %soft %steal %guest %gnice %idle
Average: all 0.25 0.00 0.25 0.00 0.00 0.00 0.00 0.00 0.00 99.50
Average: 0 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 100.00
Average: 1 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 100.00
Average: 2 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 100.00
Average: 3 0.00 0.00 1.00 0.00 0.00 0.00 0.00 0.00 0.00 99.00

显示的内容具体什么意思,希望大家都能了解,我就不在这细解释了。根据显示内容我们知道,这个计算机有4个cpu核心,目前的cpu利用率几乎是0,就是说系统整体比较闲。

从这个例子大概可以看出,我们对cpu资源的评估一般有两个观察角度:

  • 核心个数
  • 百分比

目前的计算机基本都是多核甚至多cpu系统,一个服务器上存在几个到几十个cpu核心的情况都很常见。所以,从这个角度看,cgroup应该提供一种手段,可以给进程们指定它们可以占用的cpu核心,以此来做到cpu计算资源的隔离。
百分比这个概念我们需要多解释一下:这个百分比究竟是怎么来的呢?难道每个cpu核心的计算能力就像一个带刻度表的水杯一样?一个进程要占用就会占用到它的一定刻度么?

当然不是啦!这个cpu的百分比是按时间比率计算的。基本思路是:一个CPU一般就只有两种状态,要么被占用,要么不被占用。当有多个进程要占用cpu的时候,那么操作系统在一个cpu核心上是进行分时处理的。比如说,我们把一秒钟分成1000份,那么每一份就是1毫秒,假设现在有5个进程都要用cpu,那么我们就让它们5个轮着使用,比如一人一毫秒,那么1秒过后,每个进程只占用了这个CPU的200ms,使用率为20%。整体cpu使用比率为100%。
同理,如果只有一个进程占用,而且它只用了300ms,那么在这一秒的尺度看来,cpu的占用时间是30%。于是显示出来的状态就是占用30%的CPU时间。

这就是内核是如何看待和分配计算资源的。当然实际情况要比这复杂的多,但是基本思路就是这样。Linux内核是通过CPU调度器CFS--完全公平调度器对CPU的时间进行调度的,由于本文的侧重点是cgroup而不是CFS,对这个题目感兴趣的同学可以到这里进一步学习。CFS是内核可以实现真对CPU资源隔离的核心手段,因此,理解清楚CFS对理解清楚CPU资源隔离会有很大的帮助。

##如何隔离CPU资源?

根据CPU资源的组织形式,我们就可以理解cgroup是如何对CPU资源进行隔离的了。

无非也是两个思路,一个是分配核心进行隔离,另一个是分配CPU使用时间进行隔离。

再介绍如何做隔离之前,我们先来介绍一下我们的实验系统环境:没有特殊情况,我们的实验环境都是一台24核心、128G内存的服务器,上面安装的系统可以认为是Centos7.

###搭建测试环境

我们将使用cgconfig服务和cgred服务对cgroup进行配置和使用。我们将配置两个group,一个叫zorro,另一个叫jerry。它们分别也是系统上的两个账户,其中zorro用户所运行的进程都默认在zorro group中进行限制,jerry用户所运行的进程都放到jerry group中进行限制。配置文件内容和配置方法如下:

本文并不对以下配置方法的具体含义做解释,大家只要知道如此配置可以达到相关试验环境要求即可。如果大家对配置的细节感兴趣,可以自行查找相关资料进行学习。

首先添加两个用户,zorro和jerry:

[root@zorrozou-pc ~]# useradd zorro
[root@zorrozou-pc ~]# useradd jerry

修改/etc/cgrules.conf,添加两行内容:

[root@zorrozou-pc ~]# cat /etc/cgrules.conf
zorro cpu,cpuacct zorro
jerry cpu,cpuacct jerry

修改/etc/cgconfig.conf,添加以下内容:

[root@zorrozou-pc ~]# cat /etc/cgconfig.conf
mount {
cpuset = /cgroup/cpuset;
cpu = /cgroup/cpu;
cpuacct = /cgroup/cpuacct;
memory = /cgroup/memory;
devices = /cgroup/devices;
freezer = /cgroup/freezer;
net_cls = /cgroup/net_cls;
blkio = /cgroup/blkio;
}

group zorro {
cpuset {
cpuset.cpus = “1,2”;
}
}

group jerry {
cpuset {
cpuset.cpus = “3,4”;
}
}
重启cgconfig服务和cgred服务:

[root@zorrozou-pc ~]# service cgconfig restart
[root@zorrozou-pc ~]# service cgred restart

根据上面的配置,我们给zorro组合jerry组分别配置了cpuset的隔离设置,那么在cgroup的相关目录下应该出现相关组的配置文件:
本文中所出现的的含义,如无特殊说明都是对应cgroup的控制组,而非用户组身份。
我们可以通过检查相关目录内容来检查一下环境是否配置完成:

[root@zorrozou-pc ~]# ls /cgroup/cpuset/{zorro,jerry}
/cgroup/cpuset/jerry:
cgroup.clone_children cpuset.cpu_exclusive cpuset.mem_exclusive cpuset.memory_pressure cpuset.mems cpuset.stat
cgroup.event_control cpuset.cpuinfo cpuset.mem_hardwall cpuset.memory_spread_page cpuset.sched_load_balance notify_on_release
cgroup.procs cpuset.cpus cpuset.memory_migrate cpuset.memory_spread_slab cpuset.sched_relax_domain_level tasks

/cgroup/cpuset/zorro:
cgroup.clone_children cpuset.cpu_exclusive cpuset.mem_exclusive cpuset.memory_pressure cpuset.mems cpuset.stat
cgroup.event_control cpuset.cpuinfo cpuset.mem_hardwall cpuset.memory_spread_page cpuset.sched_load_balance notify_on_release
cgroup.procs cpuset.cpus cpuset.memory_migrate cpuset.memory_spread_slab cpuset.sched_relax_domain_level tasks

至此,我们的实验环境已经搭建完成。

您的支持是我持续写下去的动力,所以本文无耻的接受捐赠。如果你觉得值得,请刷下面二维码,捐赠九毛九。

mm_facetoface_collect_qrcode_1465221734716

###测试用例设计
无论是针对CPU核心的隔离还是针对CPU时间的隔离,我们都需要一个可以消耗大量的CPU运算资源的程序来进行测试,考虑到我们是一个多CPU核心的环境,所以我们的测试用例一定也是一个可以并发使用多个CPU核心的计算型测试用例。针对这个需求,我们首先设计了一个使用多线程并发进行筛质数的简单程序。这个程序可以打印出从100010001到100020000数字范围内的质数有哪些。并发48个工作线程从一个共享的count整型变量中取数进行计算。程序源代码如下:

#include #include
#include

#define NUM 48
#define START 100010001
#define END 100020000

pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
static int count = 0;

void *prime(void *p)
{
int n, i, flag;

while (1) {
if (pthread_mutex_lock(&mutex) != 0) {
perror(“pthread_mutex_lock()”);
pthread_exit(NULL);
}
while (count == 0) {
if (pthread_cond_wait(&cond, &mutex) != 0) {
perror(“pthread_cond_wait()”);
pthread_exit(NULL);
}
}
if (count == -1) {
if (pthread_mutex_unlock(&mutex) != 0) {
perror(“pthread_mutex_unlock()”);
pthread_exit(NULL);
}
break;
}
n = count;
count = 0;
if (pthread_cond_broadcast(&cond) != 0) {
perror(“pthread_cond_broadcast()”);
pthread_exit(NULL);
}
if (pthread_mutex_unlock(&mutex) != 0) {
perror(“pthread_mutex_unlock()”);
pthread_exit(NULL);
}
flag = 1;
for (i=2;i<n/2;i++) {
if (n%i == 0) {
flag = 0;
break;
}
}
if (flag == 1) {
printf(“%d is a prime form %d!\n”, n, pthread_self());
}
}
pthread_exit(NULL);
}

int main(void)
{
pthread_t tid[NUM];
int ret, i;

for (i=0;i<NUM;i++) {
ret = pthread_create(&tid[i], NULL, prime, NULL);
if (ret != 0) {
perror(“pthread_create()”);
exit(1);
}
}

for (i=START;i<END;i+=2) {
if (pthread_mutex_lock(&mutex) != 0) {
perror(“pthread_mutex_lock()”);
pthread_exit(NULL);
}
while (count != 0) {
if (pthread_cond_wait(&cond, &mutex) != 0) {
perror(“pthread_cond_wait()”);
pthread_exit(NULL);
}
}
count = i;
if (pthread_cond_broadcast(&cond) != 0) {
perror(“pthread_cond_broadcast()”);
pthread_exit(NULL);
}
if (pthread_mutex_unlock(&mutex) != 0) {
perror(“pthread_mutex_unlock()”);
pthread_exit(NULL);
}
}

if (pthread_mutex_lock(&mutex) != 0) {
perror(“pthread_mutex_lock()”);
pthread_exit(NULL);
}
while (count != 0) {
if (pthread_cond_wait(&cond, &mutex) != 0) {
perror(“pthread_cond_wait()”);
pthread_exit(NULL);
}
}
count = -1;
if (pthread_cond_broadcast(&cond) != 0) {
perror(“pthread_cond_broadcast()”);
pthread_exit(NULL);
}
if (pthread_mutex_unlock(&mutex) != 0) {
perror(“pthread_mutex_unlock()”);
pthread_exit(NULL);
}

for (i=0;i<NUM;i++) { ret = pthread_join(tid[i], NULL); if (ret != 0) { perror(“pthread_join()”); exit(1); } } exit(0); } 我们先来看一下这个程序在不做限制的情况下的执行效果和执行时间: [root@zorrozou-pc ~/test]# time ./prime_thread …… 100019603 is a prime form 2068363008! 100019471 is a prime form 1866938112! 100019681 is a prime form 1934079744! 100019597 is a prime form 1875330816! 100019701 is a prime form 2059970304! 100019657 is a prime form 1799796480! 100019761 is a prime form 1808189184! 100019587 is a prime form 1824974592! 100019659 is a prime form 2076755712! 100019837 is a prime form 1959257856! 100019923 is a prime form 2034792192! 100019921 is a prime form 1908901632! 100019729 is a prime form 1850152704! 100019863 is a prime form -2109106432! 100019911 is a prime form -2125891840! 100019749 is a prime form 2101933824! 100019879 is a prime form 2026399488! 100019947 is a prime form 1942472448! 100019693 is a prime form 1917294336! 100019683 is a prime form 2051577600! 100019873 is a prime form 2110326528! 100019929 is a prime form -2134284544! 100019977 is a prime form 1892116224! real 0m8.945s user 3m32.095s sys 0m0.235s [root@zorrozou-pc ~]# mpstat -P ALL 1 11:21:51 CPU %usr %nice %sys %iowait %irq %soft %steal %guest %gnice %idle 11:21:52 all 99.92 0.00 0.08 0.00 0.00 0.00 0.00 0.00 0.00 0.00 11:21:52 0 100.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 11:21:52 1 100.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 11:21:52 2 100.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 11:21:52 3 100.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 11:21:52 4 99.00 0.00 1.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 11:21:52 5 100.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 11:21:52 6 100.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 11:21:52 7 100.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 11:21:52 8 100.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 11:21:52 9 100.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 11:21:52 10 100.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 11:21:52 11 99.01 0.00 0.99 0.00 0.00 0.00 0.00 0.00 0.00 0.00 11:21:52 12 100.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 11:21:52 13 100.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 11:21:52 14 100.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 11:21:52 15 100.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 11:21:52 16 99.01 0.00 0.00 0.00 0.99 0.00 0.00 0.00 0.00 0.00 11:21:52 17 100.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 11:21:52 18 100.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 11:21:52 19 100.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 11:21:52 20 100.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 11:21:52 21 100.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 11:21:52 22 100.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 11:21:52 23 100.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 经过多次测试,程序执行时间基本稳定: [root@zorrozou-pc ~/test]# time ./prime_thread &> /dev/null

real 0m8.953s
user 3m31.950s
sys 0m0.227s
[root@zorrozou-pc ~/test]# time ./prime_thread &> /dev/null

real 0m8.932s
user 3m31.984s
sys 0m0.231s
[root@zorrozou-pc ~/test]# time ./prime_thread &> /dev/null

real 0m8.954s
user 3m31.794s
sys 0m0.224s

所有相关环境都准备就绪,后续我们将在此程序的基础上进行各种隔离的测试。

###针对CPU核心进行资源隔离
针对CPU核心进行隔离,其实就是把要运行的进程绑定到指定的核心上运行,通过让不同的进程占用不同的核心,以达到运算资源隔离的目的。其实对于Linux来说,这种手段并不新鲜,也并不是在引入cgroup之后实现的,早在内核使用O1调度算法的时候,就已经支持通过taskset命令来绑定进程的cpu核心了。

好的,废话少说,我们来看看这在cgroup中是怎么配置的。

其实通过刚才的/etc/cgconfig.conf配置文件的内容,我们已经配置好了针对不同的组占用核心的设置,来回顾一下:

group zorro {
cpuset {
cpuset.cpus = “1,2”;
}
}
这段配置内容就是说,将zorro组中的进程都放在编号为1,2的cpu核心上运行。这里要说明的是,cpu核心的编号一般是从0号开始的。24个核心的服务器编号范围是从0-23.我们可以通过查看/proc/cpuinfo的内容来确定相关物理cpu的个数和核心的个数。我们截取一段来看一下:

[root@zorrozou-pc ~/test]# cat /proc/cpuinfo
processor : 23
vendor_id : GenuineIntel
cpu family : 6
model : 63
model name : Intel(R) Xeon(R) CPU E5-2620 v3 @ 2.40GHz
stepping : 2
microcode : 0x2b
cpu MHz : 2599.968
cache size : 15360 KB
physical id : 1
siblings : 12
core id : 5
cpu cores : 6
apicid : 27
initial apicid : 27
fpu : yes
fpu_exception : yes
cpuid level : 15
wp : yes
flags : fpu vme de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat pse36 clflush dts acpi mmx fxsr sse sse2 ss ht tm pbe syscall nx pdpe1gb rdtscp lm constant_tsc arch_perfmon pebs bts rep_good nopl xtopology nonstop_tsc aperfmperf eagerfpu pni pclmulqdq dtes64 monitor ds_cpl vmx smx est tm2 ssse3 fma cx16 xtpr pdcm pcid dca sse4_1 sse4_2 x2apic movbe popcnt tsc_deadline_timer aes xsave avx f16c rdrand lahf_lm abm ida arat epb xsaveopt pln pts dtherm tpr_shadow vnmi flexpriority ept vpid fsgsbase tsc_adjust bmi1 avx2 smep bmi2 erms invpcid
bogomips : 4796.38
clflush size : 64
cache_alignment : 64
address sizes : 46 bits physical, 48 bits virtual
power management:

其中processor : 23就是核心编号,说明我们当前显示的是这个服务器上的第24个核心,physical id : 1表示的是这个核心所在的物理cpu是哪个。这个编号也是从0开始,表示这个核心在第二个物理cpu上。那就意味着,我这个服务器是一个双物理cpu的服务器,那就可能意味着我们的系统时NUMA架构。另外还有一个要注意的是core id : 5这个子段,这里面隐含着一个可能的含义:你的服务器是否开启了超线程。众所周知,开启了超线程的服务器,在系统看来,一个核心会编程两个核心来看待。那么我们再确定一下是否开了超线程,可以grep一下:

[root@zorrozou-pc ~/test]# cat /proc/cpuinfo |grep -e “core id” -e “physical id”
physical id : 0
core id : 0
physical id : 0
core id : 1
physical id : 0
core id : 2
physical id : 0
core id : 3
physical id : 0
core id : 4
physical id : 0
core id : 5
physical id : 1
core id : 0
physical id : 1
core id : 1
physical id : 1
core id : 2
physical id : 1
core id : 3
physical id : 1
core id : 4
physical id : 1
core id : 5
physical id : 0
core id : 0
physical id : 0
core id : 1
physical id : 0
core id : 2
physical id : 0
core id : 3
physical id : 0
core id : 4
physical id : 0
core id : 5
physical id : 1
core id : 0
physical id : 1
core id : 1
physical id : 1
core id : 2
physical id : 1
core id : 3
physical id : 1
core id : 4
physical id : 1
core id : 5

这个内容显示出我的服务器是开启了超线程的,因为有同一个physical id : 1core id : 5可能出现两次,那么就说明这个物理cpu上的5号核心在系统看来出现了2个,那么肯定意味着开了超线程。

我在此要强调超线程这个事情,因为在一个开启了超线程的服务器上运行我们当前的测试用例是很可能得不到预想的结果的。因为从原理上看,超线程技术虽然使cpu核心变多了,但是在本测试中并不能反映出相应的性能提高。我们后续会通过cpuset的资源隔离先来说明一下这个问题,然后在后续的测试中,我们将采用一些手段规避这个问题。

我们先通过一个cpuset的配置来反映一下超线程对本测试的影响,顺便学习一下cgroup的cpuset配置方法。

您的支持是我持续写下去的动力,所以本文无耻的接受捐赠。如果你觉得值得,请刷下面二维码,捐赠九毛九。

mm_facetoface_collect_qrcode_1465221734716

  1. 不绑定核心测试:

将/etc/cgconfig.conf文件中zorro组相关配置修改为以下状态,之后重启cgconfig服务:

group zorro {
cpuset {
cpuset.cpus = “0-23”;
cpuset.mems = “0-1”;
}
}

[root@zorrozou-pc ~]# service cgconfig restart

切换用户身份到zorro,并察看zorro组的配置:

[root@zorrozou-pc ~]# su – zorro
[zorro@zorrozou-pc ~]$ cat /cgroup/cpuset/zorro/cpuset.cpus
0-23
zorro用户对应的进程已经绑定在0-23核心上执行,我们看一下执行结果:

[zorro@zorrozou-pc ~/test]$ time ./prime_thread_zorro &> /dev/null

real 0m8.956s
user 3m31.990s
sys 0m0.246s
[zorro@zorrozou-pc ~/test]$ time ./prime_thread_zorro &> /dev/null

real 0m8.944s
user 3m31.956s
sys 0m0.247s

执行速度跟刚才一样,这相当于没绑定的情况。下面,我们对zorro组的进程绑定一半的cpu核心进行测试,先测试绑定0-11号核心,将cpuset.cpus = “0-23”改为cpuset.cpus = “0-11”

请注意每次修改完/etc/cgconfig.conf文件内容都应该重启cgconfig服务,并重新登陆zorro账户。过程不再复述。

将核心绑定到0-11之后的测试结果如下:

[zorro@zorrozou-pc ~/test]$ time ./prime_thread_zorro &> /dev/null

real 0m9.457s
user 1m52.773s
sys 0m0.155s
[zorro@zorrozou-pc ~/test]$ time ./prime_thread_zorro &> /dev/null

real 0m9.460s
user 1m52.589s
sys 0m0.153s

14:52:02 CPU %usr %nice %sys %iowait %irq %soft %steal %guest %gnice %idle
14:52:03 all 49.92 0.00 0.08 0.00 0.08 0.00 0.00 0.00 0.00 49.92
14:52:03 0 100.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00
14:52:03 1 100.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00
14:52:03 2 100.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00
14:52:03 3 100.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00
14:52:03 4 100.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00
14:52:03 5 100.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00
14:52:03 6 99.01 0.00 0.99 0.00 0.00 0.00 0.00 0.00 0.00 0.00
14:52:03 7 100.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00
14:52:03 8 100.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00
14:52:03 9 100.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00
14:52:03 10 100.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00
14:52:03 11 100.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00
14:52:03 12 0.00 0.00 0.00 0.00 2.00 0.00 0.00 0.00 0.00 98.00
14:52:03 13 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 100.00
14:52:03 14 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 100.00
14:52:03 15 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 100.00
14:52:03 16 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 100.00
14:52:03 17 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 100.00
14:52:03 18 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 100.00
14:52:03 19 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 100.00
14:52:03 20 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 100.00
14:52:03 21 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 100.00
14:52:03 22 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 100.00
14:52:03 23 0.00 0.00 0.99 0.00 0.00 0.00 0.00 0.00 0.00 99.01

此时会发现一个现象,执行的总体时间变化不大,大概慢了0.5秒,但是user时间下降了将近一半。

我们再降核心绑定成0-5,12-17测试一下,就是cpuset.cpus = “0-5,12-17”,测试结果如下:

[zorro@zorrozou-pc ~/test]$ time ./prime_thread_zorro &> /dev/null

real 0m17.821s
user 3m32.425s
sys 0m0.223s
[zorro@zorrozou-pc ~/test]$ time ./prime_thread_zorro &> /dev/null
real 0m17.839s
user 3m32.375s
sys 0m0.223s

15:03:03 CPU %usr %nice %sys %iowait %irq %soft %steal %guest %gnice %idle
15:03:04 all 49.94 0.00 0.04 0.00 0.04 0.00 0.00 0.00 0.00 49.98
15:03:04 0 100.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00
15:03:04 1 100.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00
15:03:04 2 100.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00
15:03:04 3 100.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00
15:03:04 4 100.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00
15:03:04 5 100.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00
15:03:04 6 0.00 0.00 0.99 0.00 0.00 0.00 0.00 0.00 0.00 99.01
15:03:04 7 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 100.00
15:03:04 8 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 100.00
15:03:04 9 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 100.00
15:03:04 10 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 100.00
15:03:04 11 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 100.00
15:03:04 12 100.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00
15:03:04 13 100.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00
15:03:04 14 100.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00
15:03:04 15 99.01 0.00 0.99 0.00 0.00 0.00 0.00 0.00 0.00 0.00
15:03:04 16 99.01 0.00 0.99 0.00 0.00 0.00 0.00 0.00 0.00 0.00
15:03:04 17 100.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00
15:03:04 18 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 100.00
15:03:04 19 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 100.00
15:03:04 20 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 100.00
15:03:04 21 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 100.00
15:03:04 22 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 100.00
15:03:04 23 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 100.00

这次测试的结果就比较符合我们的常识,看上去cpu核心少了一半,于是执行时间增加了几乎一倍。那么是什么原因导致我们绑定到0-11核心的时候看上去性能没有下降呢?

在此我们不去过多讨论超线程的技术细节,简单来说:0-5核心是属于物理cpu0的6个实际核心,6-11是属于物理cpu1的6个实际核心,当我们使用这12个核心的时候,运算覆盖了两个物理cpu的所有真实核心。而12-17核心是对应0-5核心超线程出来的6个核心,18-23则是对应6-11核心超线程出来的6个。我们的测试应用并不能充分利用超线程之后的运算资源,所以,从我们的测试用例角度看来,只要选择了合适核心,12核跟24核的效果几本差别不大。了解了超线程的这个问题,我们后续的测试过程就要注意对比的环境。从本轮测试看来,我们应该用绑定0-5,12-17的测试结果来参考绑定一半cpu核心的效果,而不是绑定到“0-11”上的结果。从测试结果看,减少一半核心之后,确实让运算时间增加了一倍。

出个两个思考题吧:

  1. 我们发现第二轮绑定0-11核心测试的user时间和绑定0-23的测试时间减少一倍,而real时间几乎没变,这是为什么?

  2. 我们发现第三轮绑定0-5,12-17核心测试的user时间和绑定0-23的测试时间几乎一样,而real时间增加了一倍,这是为什么?

至此,如何使用cgroup的cpuset对cpu核心进行资源分配的方法大家应该学会了,这里需要强调一点:

配置中cpuset.mems = “0-1”这段配置非常重要,它相当于打开cpuset功能的开关,本身的意义是用来配置cpu使用的内存节点的,不配置这个字段的结果将是cpuset.cpus设置无效。字段具体含义,请大家自行补脑。

您的支持是我持续写下去的动力,所以本文无耻的接受捐赠。如果你觉得值得,请刷下面二维码,捐赠九毛九。

mm_facetoface_collect_qrcode_1465221734716

###针对CPU时间进行资源隔离

再回顾一下系统对cpu资源的使用方式--分时使用。分时使用要有一个基本本的时间调度单元,这个单元的意思是说,在这样一段时间范围内,我们将多少比例分配给某个进程组。我们刚才举的例子是说1秒钟,但是实际情况是1秒钟这个时间周期对计算机来说有点长。Linux内核将这个时间周期定义放在cgroup相关目录下的一个文件里,这个文件在我们服务器上是:

[root@zorrozou-pc ~]# cat /cgroup/cpu/zorro/cpu.cfs_period_us
100000

这个数字的单位是微秒,就是说,我们的cpu时间周期是100ms。还有一点需要注意的是,这个时间是针对单核来说的。

那么针对cgroup的限制放在哪里呢?

[root@zorrozou-pc ~]# cat /cgroup/cpu/zorro/cpu.cfs_quota_us
-1

就是这个cpu.cfs_quota_us文件。这里的cfs就是完全公平调度器,我们的资源隔离就是靠cfs来实现的。-1表示目前无限制。

限制方法很简单,就是设置cpu.cfs_quota_us这个文件的值,调度器会根据这个值的大小决定进程组在一个时间周期内(即100ms)使用cpu时间的比率。比如这个值我们设置成50000,那么就是时间周期的50%,于是这个进程组只能在一个cpu上占用50%的cpu时间。理解了这个概念,我们就可以思考一下,如果想让我们的进程在24核的服务器上不绑定核心的情况下占用所有核心的50%的cpu时间,该如何设置?计算公式为:

(50% * 100000 * cpu核心数)

在此设置为1200000,我们来试一下。修改cgconfig.conf内容,然后重启cgconfig:

group zorro {
cpu {
cpu.cfs_quota_us = “1200000”;
}
}

[root@zorrozou-pc ~]# service cgconfig restart

测试结果如下:

[zorro@zorrozou-pc ~/test]$ time ./prime_thread_zorro &> /dev/null

real 0m17.322s
user 3m27.116s
sys 0m0.266s
[zorro@zorrozou-pc ~/test]$ time ./prime_thread_zorro &> /dev/null

real 0m17.347s
user 3m27.208s
sys 0m0.260s

16:15:12 CPU %usr %nice %sys %iowait %irq %soft %steal %guest %gnice %idle
16:15:13 all 49.92 0.00 0.08 0.00 0.04 0.00 0.00 0.00 0.00 49.96
16:15:13 0 51.49 0.00 0.00 0.00 0.99 0.00 0.00 0.00 0.00 47.52
16:15:13 1 51.49 0.00 0.99 0.00 0.00 0.00 0.00 0.00 0.00 47.52
16:15:13 2 54.46 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 45.54
16:15:13 3 51.52 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 48.48
16:15:13 4 48.51 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 51.49
16:15:13 5 48.04 0.00 0.98 0.00 0.00 0.00 0.00 0.00 0.00 50.98
16:15:13 6 49.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 51.00
16:15:13 7 49.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 51.00
16:15:13 8 49.49 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 50.51
16:15:13 9 49.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 51.00
16:15:13 10 48.00 0.00 1.00 0.00 0.00 0.00 0.00 0.00 0.00 51.00
16:15:13 11 49.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 51.00
16:15:13 12 49.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 51.00
16:15:13 13 49.49 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 50.51
16:15:13 14 49.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 51.00
16:15:13 15 50.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 50.00
16:15:13 16 50.51 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 49.49
16:15:13 17 49.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 51.00
16:15:13 18 50.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 50.00
16:15:13 19 50.50 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 49.50
16:15:13 20 50.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 50.00
16:15:13 21 50.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 50.00
16:15:13 22 50.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 50.00
16:15:13 23 50.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 50.00

我们可以看到,基本跟绑定一半的cpu核心数的效果一样,从这个简单的对比来看,使用cpu核心数绑定的方法和使用cpu分配时间的方法,在隔离上效果几乎是相同的。但是考虑到超线程的影响,我们使用cpu时间比率的方式很可能根cpuset的方式有些差别,为了看到这个差别,我们将针对cpuset和cpuquota进行一个对比测试,测试结果如下表:

cpu比率(核心数) cpuset realtime cpuquota realtime
8.3%(2) 1m46.557s 1m36.786s
16.7%(4) 0m53.271s 0m51.067s
25%(6) 0m35.528s 0m34.539s
33.3%(8) 0m26.643s 0m25.923s
50%(12) 0m17.839s 0m17.347s
66.7%(16) 0m13.384s 0m13.015s
100%(24) 0m8.972s 0m8.932s

思考题时间又到了:请解释这个表格测试得到的数字的差异。

我们现在已经学会了如何使用cpuset和cpuquota两种方式对cpu资源进行分配,但是这两种分配的缺点也是显而易见的--就是分配完之后,进程都最多只能占用相关比例的cpu资源。即使服务器上还有空闲资源,这两种方式都无法将资源“借来使用”。

那么有没有一种方法,既可以保证在系统忙的情况下让cgroup进程组只占用相关比例的资源,而在系统闲的情况下,又可以借用别人的资源,以达到资源利用率最大话的程度呢?当然有!那就是--

###权重CPU资源隔离

这里的权重其实是shares。我把它叫做权重是因为这个值可以理解为对资源占用的权重。这种资源隔离方式事实上也是对cpu时间的进行分配。区别是作用在cfs调度器的权重值上。从用户的角度看,无非就是给每个cgroup配置一个share值,cpu在进行时间分配的时候,按照share的大小比率来确定cpu时间的百分比。它对比cpuquota的优势是,当进程不在cfs可执行调度队列中的时候,这个权重是不起作用的。就是说,一旦其他cgroup的进程释放cpu的时候,正在占用cpu的进程可以全占所有计算资源。而当有多个cgroup进程都要占用cpu的时候,大家按比例分配。

我们照例通过实验来说明这个情况,配置方法也很简单,修改cgconfig.conf,添加字段,并重启服务:

group zorro {
cpu {
cpu.shares = 1000;
}
}

[root@zorrozou-pc ~]# service cgconfig restart

配置完之后,我们就给zorro组配置了一个shares值为1000,但是实际上如果系统中只有这一个组的话,cpu看起来对他是没有限制的。现在的执行效果是这样:

[root@zorrozou-pc ~]# mpstat -P ALL 1

17:17:29 CPU %usr %nice %sys %iowait %irq %soft %steal %guest %gnice %idle
17:17:30 all 99.88 0.00 0.12 0.00 0.00 0.00 0.00 0.00 0.00 0.00
17:17:30 0 100.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00
17:17:30 1 100.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00
17:17:30 2 100.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00
17:17:30 3 99.01 0.00 0.99 0.00 0.00 0.00 0.00 0.00 0.00 0.00
17:17:30 4 99.00 0.00 1.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00
17:17:30 5 100.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00
17:17:30 6 100.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00
17:17:30 7 100.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00
17:17:30 8 100.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00
17:17:30 9 100.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00
17:17:30 10 100.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00
17:17:30 11 100.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00
17:17:30 12 100.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00
17:17:30 13 100.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00
17:17:30 14 100.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00
17:17:30 15 100.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00
17:17:30 16 100.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00
17:17:30 17 99.00 0.00 1.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00
17:17:30 18 100.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00
17:17:30 19 100.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00
17:17:30 20 100.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00
17:17:30 21 100.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00
17:17:30 22 100.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00
17:17:30 23 100.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00

[zorro@zorrozou-pc ~/test]$ time ./prime_thread_zorro &> /dev/null

real 0m8.937s
user 3m32.190s
sys 0m0.225s

如显示,cpu我们是独占的。那么什么时候有隔离效果呢?是系统中有别的cgroup也要占用cpu的时候,就能看出效果了。比如此时我们再添加一个jerry,shares值也配置为1000,并且让jerry组一直有占用cpu的进程在运行。

group jerry {
cpu {
cpu.shares = “1000”;
}
}

top – 17:24:26 up 1 day, 5 min, 2 users, load average: 41.34, 16.17, 8.17
Tasks: 350 total, 2 running, 348 sleeping, 0 stopped, 0 zombie
Cpu0 :100.0%us, 0.0%sy, 0.0%ni, 0.0%id, 0.0%wa, 0.0%hi, 0.0%si, 0.0%st
Cpu1 : 99.7%us, 0.3%sy, 0.0%ni, 0.0%id, 0.0%wa, 0.0%hi, 0.0%si, 0.0%st
Cpu2 :100.0%us, 0.0%sy, 0.0%ni, 0.0%id, 0.0%wa, 0.0%hi, 0.0%si, 0.0%st
Cpu3 : 99.7%us, 0.3%sy, 0.0%ni, 0.0%id, 0.0%wa, 0.0%hi, 0.0%si, 0.0%st
Cpu4 : 99.7%us, 0.3%sy, 0.0%ni, 0.0%id, 0.0%wa, 0.0%hi, 0.0%si, 0.0%st
Cpu5 :100.0%us, 0.0%sy, 0.0%ni, 0.0%id, 0.0%wa, 0.0%hi, 0.0%si, 0.0%st
Cpu6 : 99.7%us, 0.3%sy, 0.0%ni, 0.0%id, 0.0%wa, 0.0%hi, 0.0%si, 0.0%st
Cpu7 :100.0%us, 0.0%sy, 0.0%ni, 0.0%id, 0.0%wa, 0.0%hi, 0.0%si, 0.0%st
Cpu8 :100.0%us, 0.0%sy, 0.0%ni, 0.0%id, 0.0%wa, 0.0%hi, 0.0%si, 0.0%st
Cpu9 : 99.7%us, 0.0%sy, 0.0%ni, 0.0%id, 0.0%wa, 0.3%hi, 0.0%si, 0.0%st
Cpu10 :100.0%us, 0.0%sy, 0.0%ni, 0.0%id, 0.0%wa, 0.0%hi, 0.0%si, 0.0%st
Cpu11 : 99.7%us, 0.3%sy, 0.0%ni, 0.0%id, 0.0%wa, 0.0%hi, 0.0%si, 0.0%st
Cpu12 :100.0%us, 0.0%sy, 0.0%ni, 0.0%id, 0.0%wa, 0.0%hi, 0.0%si, 0.0%st
Cpu13 : 99.7%us, 0.3%sy, 0.0%ni, 0.0%id, 0.0%wa, 0.0%hi, 0.0%si, 0.0%st
Cpu14 :100.0%us, 0.0%sy, 0.0%ni, 0.0%id, 0.0%wa, 0.0%hi, 0.0%si, 0.0%st
Cpu15 :100.0%us, 0.0%sy, 0.0%ni, 0.0%id, 0.0%wa, 0.0%hi, 0.0%si, 0.0%st
Cpu16 :100.0%us, 0.0%sy, 0.0%ni, 0.0%id, 0.0%wa, 0.0%hi, 0.0%si, 0.0%st
Cpu17 :100.0%us, 0.0%sy, 0.0%ni, 0.0%id, 0.0%wa, 0.0%hi, 0.0%si, 0.0%st
Cpu18 : 99.3%us, 0.7%sy, 0.0%ni, 0.0%id, 0.0%wa, 0.0%hi, 0.0%si, 0.0%st
Cpu19 :100.0%us, 0.0%sy, 0.0%ni, 0.0%id, 0.0%wa, 0.0%hi, 0.0%si, 0.0%st
Cpu20 :100.0%us, 0.0%sy, 0.0%ni, 0.0%id, 0.0%wa, 0.0%hi, 0.0%si, 0.0%st
Cpu21 : 99.7%us, 0.3%sy, 0.0%ni, 0.0%id, 0.0%wa, 0.0%hi, 0.0%si, 0.0%st
Cpu22 :100.0%us, 0.0%sy, 0.0%ni, 0.0%id, 0.0%wa, 0.0%hi, 0.0%si, 0.0%st
Cpu23 :100.0%us, 0.0%sy, 0.0%ni, 0.0%id, 0.0%wa, 0.0%hi, 0.0%si, 0.0%st
Mem: 131904480k total, 4938020k used, 126966460k free, 136140k buffers
Swap: 2088956k total, 0k used, 2088956k free, 3700480k cached

PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
13945 jerry 20 0 390m 872 392 S 2397.2 0.0 48:42.54 jerry

我们以jerry用户身份执行了一个进程一直100%占用cpu,从上面的显示可以看到,这个进程占用了2400%的cpu,是因为每个cpu核心算100%,24个核心就是2400%。此时我们再以zorro身份执行筛质数的程序,并察看这个程序占用cpu的百分比:

top – 19:44:11 up 1 day, 2:25, 3 users, load average: 60.91, 50.92, 48.85
Tasks: 336 total, 3 running, 333 sleeping, 0 stopped, 0 zombie
Cpu0 : 99.7%us, 0.3%sy, 0.0%ni, 0.0%id, 0.0%wa, 0.0%hi, 0.0%si, 0.0%st
Cpu1 :100.0%us, 0.0%sy, 0.0%ni, 0.0%id, 0.0%wa, 0.0%hi, 0.0%si, 0.0%st
Cpu2 : 99.7%us, 0.3%sy, 0.0%ni, 0.0%id, 0.0%wa, 0.0%hi, 0.0%si, 0.0%st
Cpu3 :100.0%us, 0.0%sy, 0.0%ni, 0.0%id, 0.0%wa, 0.0%hi, 0.0%si, 0.0%st
Cpu4 :100.0%us, 0.0%sy, 0.0%ni, 0.0%id, 0.0%wa, 0.0%hi, 0.0%si, 0.0%st
Cpu5 :100.0%us, 0.0%sy, 0.0%ni, 0.0%id, 0.0%wa, 0.0%hi, 0.0%si, 0.0%st
Cpu6 :100.0%us, 0.0%sy, 0.0%ni, 0.0%id, 0.0%wa, 0.0%hi, 0.0%si, 0.0%st
Cpu7 :100.0%us, 0.0%sy, 0.0%ni, 0.0%id, 0.0%wa, 0.0%hi, 0.0%si, 0.0%st
Cpu8 :100.0%us, 0.0%sy, 0.0%ni, 0.0%id, 0.0%wa, 0.0%hi, 0.0%si, 0.0%st
Cpu9 :100.0%us, 0.0%sy, 0.0%ni, 0.0%id, 0.0%wa, 0.0%hi, 0.0%si, 0.0%st
Cpu10 :100.0%us, 0.0%sy, 0.0%ni, 0.0%id, 0.0%wa, 0.0%hi, 0.0%si, 0.0%st
Cpu11 : 99.7%us, 0.3%sy, 0.0%ni, 0.0%id, 0.0%wa, 0.0%hi, 0.0%si, 0.0%st
Cpu12 : 99.7%us, 0.3%sy, 0.0%ni, 0.0%id, 0.0%wa, 0.0%hi, 0.0%si, 0.0%st
Cpu13 :100.0%us, 0.0%sy, 0.0%ni, 0.0%id, 0.0%wa, 0.0%hi, 0.0%si, 0.0%st
Cpu14 : 99.7%us, 0.0%sy, 0.0%ni, 0.0%id, 0.0%wa, 0.3%hi, 0.0%si, 0.0%st
Cpu15 : 99.7%us, 0.3%sy, 0.0%ni, 0.0%id, 0.0%wa, 0.0%hi, 0.0%si, 0.0%st
Cpu16 :100.0%us, 0.0%sy, 0.0%ni, 0.0%id, 0.0%wa, 0.0%hi, 0.0%si, 0.0%st
Cpu17 : 99.7%us, 0.3%sy, 0.0%ni, 0.0%id, 0.0%wa, 0.0%hi, 0.0%si, 0.0%st
Cpu18 :100.0%us, 0.0%sy, 0.0%ni, 0.0%id, 0.0%wa, 0.0%hi, 0.0%si, 0.0%st
Cpu19 :100.0%us, 0.0%sy, 0.0%ni, 0.0%id, 0.0%wa, 0.0%hi, 0.0%si, 0.0%st
Cpu20 :100.0%us, 0.0%sy, 0.0%ni, 0.0%id, 0.0%wa, 0.0%hi, 0.0%si, 0.0%st
Cpu21 :100.0%us, 0.0%sy, 0.0%ni, 0.0%id, 0.0%wa, 0.0%hi, 0.0%si, 0.0%st
Cpu22 : 99.7%us, 0.3%sy, 0.0%ni, 0.0%id, 0.0%wa, 0.0%hi, 0.0%si, 0.0%st
Cpu23 :100.0%us, 0.0%sy, 0.0%ni, 0.0%id, 0.0%wa, 0.0%hi, 0.0%si, 0.0%st
Mem: 131904480k total, 1471772k used, 130432708k free, 144216k buffers
Swap: 2088956k total, 0k used, 2088956k free, 322404k cached

PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
13945 jerry 20 0 390m 872 392 S 1200.3 0.0 3383:04 jerry
9311 zorro 20 0 390m 872 392 R 1197.0 0.0 0:51.56 prime_thread_zo

通过top我们可以看到,以zorro用户身份执行的进程和jerry进程平分了cpu,每人50%。zorro筛质数执行的时间为:

[zorro@zorrozou-pc ~/test]$ time ./prime_thread_zorro &> /dev/null

real 0m15.152s
user 2m58.637s
sys 0m0.220s
[zorro@zorrozou-pc ~/test]$ time ./prime_thread_zorro &> /dev/null

real 0m15.465s
user 3m0.706s
sys 0m0.221s

根据这个时间看起来,基本与通过cpuquota方式分配50%的cpu时间以及通过cpuset方式分配12个核心的情况相当,而且效率还稍微高一些。当然我要说明的是,这里几乎两秒左右的效率的提高并不具备很大的参考性,它与jerry进程执行的运算是有很大相关性的。此时jerry进程执行的是一个多线程的while死循环,占满所有cpu跑。当我们把jerry进程执行的内容同样变成筛质数的时候,zorro用户的进程执行效率的参考值就比较标准了:

[zorro@zorrozou-pc ~/test]$ time ./prime_thread_zorro &> /dev/null

real 0m17.521s
user 3m32.684s
sys 0m0.254s
[zorro@zorrozou-pc ~/test]$ time ./prime_thread_zorro &> /dev/null

real 0m17.597s
user 3m32.682s
sys 0m0.253s

如程序执行显示,执行效率基本与cpuset和cpuquota相当。

这又引发了另一个问题请大家思考:为什么jerry用户执行的运算的逻辑不同会影响zorro用户的运算效率?

我们可以将刚才cpuset和cpuquota的对比列表加入cpushare一列来一起对比了,为了方便参考,我们都以cpuset为基准进行比较:

shares zorro/shares jerry(核心数) cpuset realtime cpushare realtime cpuquota realtime
2000/22000(2) 1m46.557s 1m41.691s 1m36.786s
4000/20000(4) 0m53.271s 0m51.801s 0m51.067s
6000/18000(6) 0m35.528s 0m35.152s 0m34.539s
8000/16000(8) 0m26.643s 0m26.372s 0m25.923s
12000/12000(12) 0m17.839s 0m17.694s 0m17.347s
16000/8000(16) 0m13.384s 0m13.388s 0m13.015s
24000/0(24) 0m8.972s 0m8.943s 0m8.932s

请注意一个问题,由于cpushares无法像cpuquota或者cpuset那样只执行zorro用户的进程,所以在进行cpushares测试的时候,必须让jerry用户同时执行相同的筛质数程序,才能使两个用户分别分到相应比例的cpu时间。这样可能造成本轮测试结果的不准确。通过对比看到,当比率分别都配置了相当于两个核心的计算能力的情况下,本轮测试是cpuquota方式消耗了1m36.786s稍快一些。为了保证相对公平的环境作为参照,我们将重新对这轮测试进行数据采集,这次在cpuset和cpuquota的压测时,都用jerry用户执行一个干扰程序作为参照,重新分析数据。当然,cpushares的测试数据就不必重新测试了:

shares zorro/shares jerry(核心数) cpuset realtime cpushare realtime cpuquota realtime
2000/22000(2) 1m46.758s 1m41.691s 1m42.341s
4000/20000(4) 0m53.340s 0m51.801s 0m51.512s
6000/18000(6) 0m35.525s 0m35.152s 0m34.392s
8000/16000(8) 0m26.738s 0m26.372s 0m25.772s
12000/12000(12) 0m17.793s 0m17.694s 0m17.256s
16000/8000(16) 0m13.366s 0m13.388s 0m13.155s
24000/0(24) 0m8.930s 0m8.943s 0m8.939s

至此,cgroup中针对cpu的三种资源隔离都介绍完了,分析我们的测试数据可以得出一些结论:

  1. 三种cpu资源隔离的效果基本相同,在资源分配比率相同的情况下,它们都提供了差不多相同的计算能力。
  2. cpuset隔离方式是以分配核心的方式进行资源隔离,可以提供的资源分配最小粒度是核心,不能提供更细粒度的资源隔离,但是隔离之后运算的相互影响最低。需要注意的是在服务器开启了超线程的情况下,要小心选择分配的核心,否则不同cgroup间的性能差距会比较大。
  3. cpuquota给我们提供了一种比cpuset可以更细粒度的分配资源的方式,并且保证了cgroup使用cpu比率的上限,相当于对cpu资源的硬限制。
  4. cpushares给我们提供了一种可以按权重比率弹性分配cpu时间资源的手段:当cpu空闲的时候,某一个要占用cpu的cgroup可以完全占用剩余cpu时间,充分利用资源。而当其他cgroup需要占用的时候,每个cgroup都能保证其最低占用时间比率,达到资源隔离的效果。

大家可以根据这三种不同隔离手段特点,针对自己的环境来选择不同的方式进行cpu资源的隔离。当然,这些手段也可以混合使用,以达到更好的QOS效果。

您的支持是我持续写下去的动力,所以本文无耻的接受捐赠。如果你觉得值得,请刷下面二维码,捐赠九毛九。

mm_facetoface_collect_qrcode_1465221734716

但是可是but,这就完了么?
显然并没有。。。。。。

以上测试只针对了一种计算场景,这种场景在如此的简单的情况下,影响测试结果的条件已经很复杂了。如果是其他情况呢?我们线上真正跑业务的环境会这么单纯么?显然不会。我们不可能针对所有场景得出结论,想要找到适用于自己场景的隔离方式,还是需要在自己的环境中进行充分测试。在此只能介绍方法,以及针对一个场景的参考数据,仅此而已。单就这一个测试来说,它仍然不够全面,无法体现出内核cpu资源隔离的真正面目。众所周知,cpu使用主要分两个部分,user和sys。上面这个测试,由于测试用例的选择,只关注了user的使用。那么如果我们的sys占用较多会变成什么样呢?

##CPU资源隔离在sys较高的情况下是什么表现?

###内核资源不冲突的情况

首先我们简单说一下什么叫sys较高。先看mpstat命令的输出:

[root@zorrozou-pc ~]# mpstat 1
Linux 3.10.90-1-linux (zorrozou-pc) 12/24/15 x86_64 (24 CPU)

16:08:52 CPU %usr %nice %sys %iowait %irq %soft %steal %guest %gnice %idle
16:08:53 all 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 100.00
16:08:54 all 0.00 0.00 0.04 0.00 0.04 0.00 0.00 0.00 0.00 99.92
16:08:55 all 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 100.00
16:08:56 all 0.04 0.00 0.04 0.00 0.00 0.00 0.00 0.00 0.00 99.92
16:08:57 all 0.04 0.00 0.04 0.00 0.00 0.00 0.00 0.00 0.00 99.92
16:08:58 all 0.00 0.00 0.04 0.00 0.00 0.00 0.00 0.00 0.00 99.96

Average: all 0.01 0.00 0.03 0.00 0.01 0.00 0.00 0.00 0.00 99.95

这里面我们看到cpu的使用比率分了很多栏目,我们一般评估进程占用CPU的时候,最重要的是%user和%sys。%sys一般是指,进程陷入内核执行时所占用的时间,这些时间是内核在工作。常见的情况时,进程执行过程中之行了某个系统调用,而陷入内核态执行所产生的cpu占用。

所以在这一部分,我们需要重新提供一个测试用例,让sys部分的cpu占用变高。基于筛质数进行改造即可,我们这次让每个筛质数的线程,在做运算之前都用非阻塞方式open()打开一个文件,每次拿到一个数运算的时候,循环中都用系统调用read()读一下文件。以此来增加sys占用时间的比率。先来改程序:

#include #include
#include
#include
#include
#include <sys/stat.h>
#include <sys/types.h>
#include

#define NUM 48
#define START 1010001
#define END 1020000

pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
static int count = 0;

void *prime(void *p)
{
int n, i, flag;
int num, fd, ret;
char name[BUFSIZ];
char buf[BUFSIZ];

bzero(name, BUFSIZ);

num = (int *)p;
sprintf(name, “/tmp/tmpfilezorro%d”, num);

fd = open(name, O_RDWR|O_CREAT|O_TRUNC|O_NONBLOCK , 0644);
if (fd < 0) {
perror(“open()”);
exit(1);
}

while (1) {
if (pthread_mutex_lock(&mutex) != 0) {
perror(“pthread_mutex_lock()”);
pthread_exit(NULL);
}
while (count == 0) {
if (pthread_cond_wait(&cond, &mutex) != 0) {
perror(“pthread_cond_wait()”);
pthread_exit(NULL);
}
}
if (count == -1) {
if (pthread_mutex_unlock(&mutex) != 0) {
perror(“pthread_mutex_unlock()”);
pthread_exit(NULL);
}
break;
}
n = count;
count = 0;
if (pthread_cond_broadcast(&cond) != 0) {
perror(“pthread_cond_broadcast()”);
pthread_exit(NULL);
}
if (pthread_mutex_unlock(&mutex) != 0) {
perror(“pthread_mutex_unlock()”);
pthread_exit(NULL);
}
flag = 1;
for (i=2;i<n/2;i++) {
ret = read(fd, buf, BUFSIZ);
if (ret < 0) {
perror(“read()”);
}
if (n%i == 0) {
flag = 0;
break;
}
}
if (flag == 1) {
printf(“%d is a prime form %d!\n”, n, pthread_self());
}
}

close(fd);
pthread_exit(NULL);
}

int main(void)
{
pthread_t tid[NUM];
int ret, i, num;

for (i=0;i<NUM;i++) {
ret = pthread_create(&tid[i], NULL, prime, (void *)i);
if (ret != 0) {
perror(“pthread_create()”);
exit(1);
}
}

for (i=START;i<END;i+=2) {
if (pthread_mutex_lock(&mutex) != 0) {
perror(“pthread_mutex_lock()”);
pthread_exit(NULL);
}
while (count != 0) {
if (pthread_cond_wait(&cond, &mutex) != 0) {
perror(“pthread_cond_wait()”);
pthread_exit(NULL);
}
}
count = i;
if (pthread_cond_broadcast(&cond) != 0) {
perror(“pthread_cond_broadcast()”);
pthread_exit(NULL);
}
if (pthread_mutex_unlock(&mutex) != 0) {
perror(“pthread_mutex_unlock()”);
pthread_exit(NULL);
}
}
if (pthread_mutex_lock(&mutex) != 0) {
perror(“pthread_mutex_lock()”);
pthread_exit(NULL);
}
while (count != 0) {
if (pthread_cond_wait(&cond, &mutex) != 0) {
perror(“pthread_cond_wait()”);
pthread_exit(NULL);
}
}
count = -1;
if (pthread_cond_broadcast(&cond) != 0) {
perror(“pthread_cond_broadcast()”);
pthread_exit(NULL);
}
if (pthread_mutex_unlock(&mutex) != 0) {
perror(“pthread_mutex_unlock()”);
pthread_exit(NULL);
}

for (i=0;i<NUM;i++) { ret = pthread_join(tid[i], NULL); if (ret != 0) { perror(“pthread_join()”); exit(1); } } exit(0); } 我们将筛质数的范围缩小了两个数量级,并且每个线程都打开一个文件,每次计算的循环中都read一遍。此时这个进程执行的时候的cpu使用状态是这样的: 17:20:46 CPU %usr %nice %sys %iowait %irq %soft %steal %guest %gnice %idle 17:20:47 all 53.04 0.00 46.96 0.00 0.00 0.00 0.00 0.00 0.00 0.00 17:20:47 0 53.00 0.00 47.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 17:20:47 1 53.00 0.00 47.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 17:20:47 2 53.00 0.00 47.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 17:20:47 3 53.00 0.00 47.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 17:20:47 4 53.00 0.00 47.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 17:20:47 5 53.00 0.00 47.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 17:20:47 6 53.00 0.00 47.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 17:20:47 7 53.00 0.00 47.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 17:20:47 8 53.00 0.00 47.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 17:20:47 9 53.00 0.00 47.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 17:20:47 10 53.00 0.00 47.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 17:20:47 11 53.47 0.00 46.53 0.00 0.00 0.00 0.00 0.00 0.00 0.00 17:20:47 12 52.00 0.00 48.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 17:20:47 13 53.00 0.00 47.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 17:20:47 14 53.47 0.00 46.53 0.00 0.00 0.00 0.00 0.00 0.00 0.00 17:20:47 15 53.00 0.00 47.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 17:20:47 16 53.00 0.00 47.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 17:20:47 17 53.00 0.00 47.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 17:20:47 18 53.00 0.00 47.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 17:20:47 19 53.00 0.00 47.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 17:20:47 20 53.00 0.00 47.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 17:20:47 21 53.00 0.00 47.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 17:20:47 22 53.00 0.00 47.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 17:20:47 23 53.00 0.00 47.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 [zorro@zorrozou-pc ~/test]$ time ./prime_sys &> /dev/null

real 0m12.227s
user 2m34.869s
sys 2m17.239s

测试用例已经基本符合我们的测试条件,可以达到近50%的sys占用,下面开始进行对比测试。测试方法根上一轮一样,仍然用jerry账户运行一个相同的程序在另一个cgroup不断的循环,然后分别看在不同资源分配比率下的zorro用户筛质数程序运行的时间。以下是测试结果:

shares zorro/shares jerry(核心数) cpuset realtime cpushare realtime cpuquota realtime
2000/22000(2) 2m27.666s 2m27.599s 2m27.918s
4000/20000(4) 1m12.621s 1m14.345s 1m13.581s
6000/18000(6) 0m48.612s 0m49.474s 0m48.730s
8000/16000(8) 0m36.412s 0m37.269s 0m36.784s
12000/12000(12) 0m24.611s 0m24.624s 0m24.628s
16000/8000(16) 0m18.401s 0m18.688s 0m18.480s
24000/0(24) 0m12.188s 0m12.487s 0m12.147s
shares zorro/shares jerry(核心数) cpuset systime cpushare systime cpuquota systime
2000/22000(2) 2m20.115s 2m21.024s 2m21.854s
4000/20000(4) 2m16.450s 2m21.103s 2m20.352s
6000/18000(6) 2m18.273s 2m20.455s 2m20.039s
8000/16000(8) 2m18.054s 2m20.611s 2m19.891s
12000/12000(12) 2m20.358s 2m18.331s 2m20.363s
16000/8000(16) 2m17.724s 2m18.958s 2m18.637s
24000/0(24) 2m16.723s 2m17.707s 2m16.176s

这次我们多了一个表格专门记录systime时间占用。根据数据结果我们会发现,在这次测试循环中,三种隔离方式都呈现出随着资源的增加进程是执行的总时间线性下降,并且隔离效果区别不大。由于调用read的次数一样,systime的使用基本都稳定在一个固定的时间范围内。这说明,在sys占用较高的情况下,各种cpu资源隔离手段都表现出比较理想的效果。

您的支持是我持续写下去的动力,所以本文无耻的接受捐赠。如果你觉得值得,请刷下面二维码,捐赠九毛九。

mm_facetoface_collect_qrcode_1465221734716

###内核资源冲突的情况

但是现实的生产环境往往并不是这么理想的,有没有可能在某种情况下,各种CPU资源隔离的手段并不会表现出这么理想的效果呢?有没有可能不同的隔离方式会导致进程的执行会有影响呢?其实这是很可能发生的。我们上一轮测试中,每个cgroup中的线程打开的文件都不是同一个文件,内核在处理这种场景的时候,并不需要使用内核中的一些互斥资源(比如自旋锁或者屏障)进行竞争条件的处理。如果环境变成大家read的是同一个文件,那么情况就可能有很大不同了。下面我们来测试一下每个zorro组中的所有线程都open同一个文件并且read时的执行效果,我们照例把测试用例代码贴出来:

#include #include
#include
#include
#include
#include <sys/stat.h>
#include <sys/types.h>
#include

#define NUM 48
#define START 1010001
#define END 1020000

pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
static int count = 0;
#define PATH “/etc/passwd”

void *prime(void *p)
{
int n, i, flag;
int num, fd, ret;
char name[BUFSIZ];
char buf[BUFSIZ];

fd = open(PATH, O_RDONLY|O_NONBLOCK);
if (fd < 0) {
perror(“open()”);
exit(1);
}

while (1) {
if (pthread_mutex_lock(&mutex) != 0) {
perror(“pthread_mutex_lock()”);
pthread_exit(NULL);
}
while (count == 0) {
if (pthread_cond_wait(&cond, &mutex) != 0) {
perror(“pthread_cond_wait()”);
pthread_exit(NULL);
}
}
if (count == -1) {
if (pthread_mutex_unlock(&mutex) != 0) {
perror(“pthread_mutex_unlock()”);
pthread_exit(NULL);
}
break;
}
n = count;
count = 0;
if (pthread_cond_broadcast(&cond) != 0) {
perror(“pthread_cond_broadcast()”);
pthread_exit(NULL);
}
if (pthread_mutex_unlock(&mutex) != 0) {
perror(“pthread_mutex_unlock()”);
pthread_exit(NULL);
}
flag = 1;
for (i=2;i<n/2;i++) {
ret = read(fd, buf, BUFSIZ);
if (ret < 0) {
perror(“read()”);
}
if (n%i == 0) {
flag = 0;
break;
}
}
if (flag == 1) {
printf(“%d is a prime form %d!\n”, n, pthread_self());
}
}

close(fd);
pthread_exit(NULL);
}

int main(void)
{
pthread_t tid[NUM];
int ret, i, num;

for (i=0;i<NUM;i++) {
ret = pthread_create(&tid[i], NULL, prime, (void *)i);
if (ret != 0) {
perror(“pthread_create()”);
exit(1);
}
}

for (i=START;i<END;i+=2) {
if (pthread_mutex_lock(&mutex) != 0) {
perror(“pthread_mutex_lock()”);
pthread_exit(NULL);
}
while (count != 0) {
if (pthread_cond_wait(&cond, &mutex) != 0) {
perror(“pthread_cond_wait()”);
pthread_exit(NULL);
}
}
count = i;
if (pthread_cond_broadcast(&cond) != 0) {
perror(“pthread_cond_broadcast()”);
pthread_exit(NULL);
}
if (pthread_mutex_unlock(&mutex) != 0) {
perror(“pthread_mutex_unlock()”);
pthread_exit(NULL);
}
}
if (pthread_mutex_lock(&mutex) != 0) {
perror(“pthread_mutex_lock()”);
pthread_exit(NULL);
}
while (count != 0) {
if (pthread_cond_wait(&cond, &mutex) != 0) {
perror(“pthread_cond_wait()”);
pthread_exit(NULL);
}
}
count = -1;
if (pthread_cond_broadcast(&cond) != 0) {
perror(“pthread_cond_broadcast()”);
pthread_exit(NULL);
}
if (pthread_mutex_unlock(&mutex) != 0) {
perror(“pthread_mutex_unlock()”);
pthread_exit(NULL);
}

for (i=0;i<NUM;i++) {
ret = pthread_join(tid[i], NULL);
if (ret != 0) {
perror(“pthread_join()”);
exit(1);
}
}

exit(0);
}

此时jerry组中的所有线程仍然是每个线程一个文件,与上一轮测试一样。测试结果如下:

shares zorro/shares jerry(核心数) cpuset realtime cpushare realtime cpuquota realtime
2000/22000(2) 2m27.402s 2m41.015s 4m37.149s
4000/20000(4) 1m18.178s 1m25.214s 2m42.455s
6000/18000(6) 0m52.592s 1m2.691s 1m48.492s
8000/16000(8) 0m43.598s 0m57.000s 1m21.044s
12000/12000(12) 0m52.182s 0m59.613s 0m58.004s
16000/8000(16) 0m50.712s 0m54.371s 0m56.911s
24000/0(24) 0m50.599s 0m50.550s 0m50.496s
shares zorro/shares jerry(核心数) cpuset systime cpushare systime cpuquota systime
2000/22000(2) 2m19.829s 2m47.706s 6m39.800s
4000/20000(4) 2m41.928s 3m6.575s 8m14.087s
6000/18000(6) 2m45.671s 3m38.722s 8m13.668s
8000/16000(8) 3m14.434s 4m54.451s 8m12.904s
12000/12000(12) 7m39.542s 9m7.751s 8m57.332s
16000/8000(16) 10m47.425s 11m41.443s 12m21.056s
24000/0(24) 17m17.661s 17m7.311s 17m14.788s

观察这轮测试的结果我们会发现,当线程同时read同一个文件时,时间的消耗并不在呈现线性下降的趋势了,而且,随着分配的资源越来越多,sys占用时间也越来越高,这种现象如何解释呢?本质上来讲,使用cgroup进行资源隔离时,内核资源仍然是共享的。如果业务使用内核资源如果没有产生冲突,那么隔离效果应该会比较理想,但是业务一旦使用了会导致内核资源冲突的逻辑时,那么业务的执行效率就会下降,此时可能所有进程在内核中处理的时候都可能会在竞争的资源上忙等(如果使用了spinlock)。自然的,如果多个cgroup的进程之间也正好使用了可能会导致内核触发竞争条件的资源时,自然也会发生所谓的cgroup之间的相互影响。可能的现象就是,当某一个业务A的cgroup正在运行着,突然B业务的cgroup有请求要处理,会导致A业务的响应速度和处理能力下降。而这种相互干扰,正是资源隔离手段想要尽量避免的。我们认为,如果出现了上述效果,那么资源隔离手段就是打了折扣的。

根据我们的实验结果可以推论,在内核资源有竞争条件的情况下,cpuset的资源隔离方式表现出了相对其他方式的优势,cpushare方式的性能折损尚可接受,而cpuquota表现出了最差的性能,或者说在cpuquota的隔离条件下,cgroup之间进程相互影响的可能性最大。

那么在内核资源存在竞争的时候,cgroup的cpu资源隔离会有相互干扰。结论就是这样了么?这个推断靠谱么?我们再来做一轮实验,这次只对比cpuset和cpuquota。这次我们不用jerry来运行干扰程序测试隔离性,我们让zorro只在单纯的隔离状态下,再有内核资源竞争的条件下进行运算效率测试,就是说这个环境没有多个cgroup可能造成的相互影响。先来看数据:

cpu比率(核心数) cpuset realtime cpuquota realtime
8.3%(2) 2m26.815s 9m4.490s
16.7%(4) 1m17.894s 4m49.167s
25%(6) 0m52.356s 3m13.144s
33.3%(8) 0m42.946s 2m23.010s
50%(12) 0m52.014s 1m33.571s
66.7%(16) 0m50.903s 1m10.553s
100%(24) 0m50.331s 0m50.304s
cpu比率(核心数) cpuset systime cpuquota systime
8.3%(2) 2m18.713s 15m27.738s
16.7%(4) 2m41.172s 16m30.741s
25%(6) 2m44.618s 16m30.964s
33.3%(8) 3m12.587s 16m18.366s
50%(12) 7m36.929s 15m55.407s
66.7%(16) 10m49.327s 16m1.463s
100%(24) 17m9.482s 17m9.533s

不知道看完这组数据之后,大家会不会困惑?cpuset的测试结果根上一轮基本一样,这可以理解。但是为什么cpuquota这轮测试反倒比刚才有jerry用户进程占用cpu进行干扰的时候的性能更差了?

如果了解了内核在这种资源竞争条件的原理的话,这个现象并不难解释。可以这样想,如果某一个资源存在竞争的话,那么是不是同时竞争的人越多,那么对于每个人来说,单次得到资源的可能性更低?比如说,老师给学生发苹果,每次只发一个,但是同时有10个人一起抢,每个人每次抢到苹果的几率是10%,如果20个人一起抢,那么每次每人强到苹果的几率就只有5%了。在内核竞争条件下,也是一样的道理,资源只有一个,当抢的进程少的时候,每个进程抢到资源的概率大,于是浪费在忙等上的时间就少。本轮测试的cpuset就可以说明这个现象,可以观察到,cpuset systime随着分配的核心数的增多而上升,就是同时跑的进程越多,sys消耗在忙等资源上的时间就越大。而cpuquota systime消耗从头到尾都基本变化不大,意味着再以quota方式分配cpu的时候,所有核心都是用得上的,所以一直都有24个进程在抢资源,大家消耗在忙等上的时间是一样的。
为什么有jerry进程同时占用cpu的情况下,cpuquota反倒效率要快些呢?这个其实也好理解。在jerry进程执行的时候,这个cgroup的相关线程打开的是不同的文件,所以从内核竞争上没有冲突。另外,jerry消耗了部分cpu,导致内核会在zorro的进程和jerry的进程之间发生调度,这意味着,同一时刻核心数只有24个,可能有18个在给jerry的线程使用,另外6个在给zorro的进程使用,这导致zorro同时争抢资源的进程个数不能始终保持24个,所以内核资源冲突反倒减小了。这导致,使用cpuquota的情况下,有其他cgroup执行的时候,还可能会使某些业务的执行效率提升,而不是下降。这种相互影响实在太让人意外了!但这确实是事实!

那么什么情况下会导致cgroup之间的相互影响使性能下降呢?也好理解,当多个cgroup的应用之间使用了相同的内核资源的时候。请大家思考一个问题:现实情况是同一种业务使用冲突资源的可能性更大还是不同业务使用冲突资源的可能性更大呢?从概率上说应该是同一种业务。从这个角度出发来看,如果我们有两台多核服务器,有两个跟我们测试逻辑类似的业务A、B,让你选择一种部署方案,你是选择让A、B两个业务分别独占一个服务器?还是让A、B业务使用资源隔离分别在两个服务器上占用50%的资源?通过这轮分析我想答案很明确了:

  1. 从容灾的角度说,让某一个业务使用多台服务器肯定会增加容灾能力。
  2. 从资源利用率的角度说,如果让一个业务放在一个服务器上,那么他在某些资源冲突的情况下并不能发挥会最大效率。然而如果使用group分布在两个不同的服务器上,无论你用cpuset,还是cpushare,又或是cpuquota,它的cpu性能表现都应该强于在一个独立的服务器上部署。况且cgroup的cpu隔离是在cfs中实现的,这种隔离几乎是不会浪费额外的计算能力的,就是说,做隔离相比不做隔离,系统本身的性能损耗都可以忽略不计。

那么,究竟还有什么会妨碍我们使用cgoup的cpu资源隔离呢?

您的支持是我持续写下去的动力,所以本文无耻的接受捐赠。如果你觉得值得,请刷下面二维码,捐赠九毛九。

mm_facetoface_collect_qrcode_1465221734716