[TOC] # Cgroups 之CPU资源限制 基准测试 @(Cgroups) ## 测试环境 | 测试机器ip | CPU型号 | 机器型号 | | :--------: | :--------:| :------: | | 10.200.53.74/ 10.200.53.75 | Intel(R) Xeon(R) CPU E5-2630 v4 @ 2.20GHz | ProLiant BL460c Gen9 (HP) | ``` cat /proc/cpuinfo physical id: 0 表示这个核心所在的物理cpu是哪个 core id: 12 表示每个核心的id ``` | processor | physical id | core id | | :--------: | :--------:| :------: | | 0 ~ 4 | 0 | 0~4 | | 20 ~ 24 | 0 | 0~4 | | 5 ~ 9 | 0 | 8~12 | | 25 ~ 29 | 0 | 8~12 | | 10 ~ 14 | 1 | 0~4 | | 30 ~ 34 | 1 | 0~4 | | 15 ~ 19 | 1 | 8~12 | | 35 ~ 39 | 1 | 8~12 | ``` 查看的cpu physical id: 0 , core id: 12 出现两次,说明 这个物理CPU上的12号核心在系统看来出现了2个,意味着肯定开了超线程。 ``` ## 测试用例 ### `demo`: 多线程并发进行筛选质数 打印从100010001 到 100020000数字范围内的质数有哪些,并发48个工作线程 从一个共享的count 整型变量中 取数进行计算。 测试代码:https://git-sa.nie.netease.com/whale/containers\_benchmark/tree/master 可以通过查看/proc/cgroups(since Linux 2.6.24)知道当前系统支持哪些subsystem ``` root@cld-dnode14-37:/home/gzzhangyi2015/benchmark_test/cpu# cat /proc/cgroups #subsys_name hierarchy num_cgroups enabled cpuset 4 9 1 cpu 2 9 1 cpuacct 3 9 1 blkio 1 9 1 memory 7 114 1 devices 5 9 1 freezer 6 9 1 net_cls 8 9 1 perf_event 10 9 1 net_prio 9 9 1 pids 11 9 1 ``` 从左到右,字段的含义分别是: ``` 1. subsystem的名字 2. subsystem所关联到的cgroup树的ID,如果多个subsystem关联到同一颗cgroup树,那么他们的这个字段将一样,比如这里的cpu和cpuacct就一样,表示他们绑定到了同一颗树。如果出现下面的情况,这个字段将为0: * 当前subsystem没有和任何cgroup树绑定 * 当前subsystem已经和cgroup v2的树绑定 * 当前subsystem没有被内核开启 3. subsystem所关联的cgroup树中进程组的个数,也即树上节点的个数 4. 1表示开启,0表示没有被开启(可以通过设置内核的启动参数“cgroup_disable”来控制subsystem的开启). ``` ## 针对CPU核心进行资源隔离 ### cpuset子系统: 绑定cgroup到指定CPUs和NUMA节点。 其中cpuset主要用于设置CPU的亲和性,可以限制cgroup中的进程只能在指定的CPU上运行,或者不能在指定的CPU上运行,同时cpuset还能设置内存的亲和性。 ### 超线程对cpu性能的影响,进而验证对cpuset的影响 #### 测试用例1:核心数减半,对比cpu时间消耗 1. 绑定0-19核心测试 和 0-39核心测试结果, 验证是否user时间少一倍,但real时间不变。 tips: 以下测试数据已取多次测量的平均值。 ``` docker run --cpuset-cpus 0-19 dockerhub.nie.netease.com/whale/containers\_benchmark:cpu root@cld-dnode14-37:/sys/fs/cgroup/cpuset/docker/6527ef2594677b77b76355c911336c5972995ee8cfdfd77a909ae6f35531132d# cat cpuset.cpus 0-19 real 0m6.225s user 2m3.060s sys 0m0.024s ``` ![](https://img.kancloud.cn/c8/7f/c87fdf9cbc972a2e0818a61dc469100e_1102x1116.png) ``` docker run --cpuset-cpus 0-39 dockerhub.nie.netease.com/whale/containers\_benchmark:cpu root@cld-dnode14-37:/sys/fs/cgroup/cpuset/docker/6527ef2594677b77b76355c911336c5972995ee8cfdfd77a909ae6f35531132d# cat cpuset.cpus 0-39 real 0m5.922s user 3m47.180s sys 0m0.044s ``` ![](https://img.kancloud.cn/c8/7f/c87fdf9cbc972a2e0818a61dc469100e_1102x1116.png) 总结:如上所示,执行的real时间变化不大,大概慢了0.3秒,但是user时间和sys时间下降了将近一半。 #### 测试用例2:分别使用相同个数的物理核心和超线程核心,对比cpu时间消耗 2\. 绑定0-9 20-29 核心 和 10- 19 30-39 核心 测试结果,验证 是否 user时间一样,但real时间增加一倍。 ``` docker run --cpuset-cpus 0-9,20-29 dockerhub.nie.netease.com/whale/containers\_benchmark:cpu real 0m11.467s user 3m47.828s sys 0m0.040s root@cld-dnode14-37:/sys/fs/cgroup/cpuset/docker/6527ef2594677b77b76355c911336c5972995ee8cfdfd77a909ae6f35531132d# cat cpuset.cpus 0-9,20-29 ``` ![](https://img.kancloud.cn/d7/15/d7157c9acc611f87e3ee81b7db7e005a_1102x1124.png) ``` docker run --cpuset-cpus 10-19,30-39 dockerhub.nie.netease.com/whale/containers\_benchmark:cpu real 0m11.487s user 3m47.928s sys 0m0.024s root@cld-dnode14-37:/sys/fs/cgroup/cpuset/docker/6527ef2594677b77b76355c911336c5972995ee8cfdfd77a909ae6f35531132d# cat cpuset.cpus 10-19,30-39 ``` ![](https://img.kancloud.cn/85/47/85472bea471ee80c8c69b0d843ff7f34_1098x1122.png) Tips: 设置cpuset.mems = “0-1” ``` root@cld-dnode14-37:/sys/fs/cgroup/cpuset/docker/6527ef2594677b77b76355c911336c5972995ee8cfdfd77a909ae6f35531132d# cat cpuset.mems 0-1 ``` 看上去cpu核心少了一半,于是执行sys时间增加了几乎一倍。那么是什么原因导致我们绑定到0-19核心的时候看上去性能没有下降呢? (这里省略了绑定到0-19核心的测试结果截图) ### 结论: 我们的测试应用并不能充分利用超线程之后的运算资源,所以,从我们的测试用例角度看来,只要选择了合适核心,20核跟40核的效果几本差别不大。了解了超线程的这个问题,我们后续的测试过程就要注意对比的环境。 ### 分析: 0-9核心是属于物理cpu0的10个实际核心,10-19是属于物理cpu1的10个实际核心,当我们使用这20个核心的时候,运算覆盖了两个物理cpu的所有真实核心。而20-29核心是对应0-9核心超线程出来的10个核心,30-39则是对应10-19核心超线程出来的10个。 从本轮测试看来,我们应该用绑定0-9,20-29的测试结果来参考绑定一半cpu核心的效果,而不是绑定到“0-19”上的结果。从测试结果看,减少一半核心之后,确实让运算时间增加了一倍。 ## 针对CPU时间进行资源隔离 ### cpu.cfs\_period\_us & cpu.cfs\_quota\_us cfs\_period\_us用来配置时间周期长度,完全公平调度器的调整时间配额的周期。 cfs\_quota\_us用来配置当前cgroup在设置的周期长度内所能使用的CPU时间数, 两个文件配合起来设置CPU的使用上限。 两个文件的单位都是微秒(us),cfs\_period\_us的取值范围为1毫秒(ms)到1秒(s),cfs\_quota\_us的取值大于1ms即可, 如果cfs\_quota\_us的值为-1(默认值),表示不受cpu时间的限制。 ``` root@cld-dnode14-37:/sys/fs/cgroup/cpu/docker# cat cpu.cfs_quota_us -1 ``` 如下:让测试的进程在40核机器上**不绑定核心的情况下**占用所有核心的50%的cpu时间,则设置cfs\_quota\_us=”2000000”。 ``` docker run --cpu-quota 2000000 dockerhub.nie.netease.com/whale/containers\_benchmark:cpu root@cld-dnode14-37:/sys/fs/cgroup/cpu/docker/e4b6540cf9ce211d1d0f41e8b69b112795afb33fbff1ac419e9b8d7c8dc65336# cat cpu.cfs_period_us 100000 root@cld-dnode14-37:/sys/fs/cgroup/cpu/docker/e4b6540cf9ce211d1d0f41e8b69b112795afb33fbff1ac419e9b8d7c8dc65336# cat cpu.cfs_quota_us 2000000 real 0m17.124s user 5m40.904s sys 0m0.096s ``` ![](https://img.kancloud.cn/9d/44/9d4436f78ee64ee48010a40fdc271d1b_1100x1068.png) ### 结论: 基本跟绑定一半的cpu核心数的效果一样,从这个简单的对比来看,使用cpu核心数绑定的方法和使用cpu分配时间的方法,在隔离上效果是有差异的。 ## 针对CPU权重进行资源隔离 ### cpu.shares shares用来设置CPU的相对值,并且是针对所有的CPU(内核),K8S默认1 vcore 的值是1024。 假如系统中有两个cgroup,分别是A和B,A的shares值是1024,B的shares值是512,那么A将获得1024/(1204+512)=66%的CPU资源,而B将获得33%的CPU资源。 shares有两个特点: 如果A不忙,没有使用到66%的CPU时间,那么剩余的CPU时间将会被系统分配给B,即B的CPU使用率可以超过33% 如果添加了一个新的cgroup C,且它的shares值是1024,那么A的限额变成了1024/(1204+512+1024)=40%,B的变成了20% 从上面两个特点可以看出: 在闲的时候,shares基本上不起作用,只有在CPU忙的时候起作用,这是一个优点。 由于shares是一个绝对值,需要和其它cgroup的值进行比较才能得到自己的相对限额,而在一个部署很多容器的机器上,cgroup的数量是变化的,所以这个限额也是变化的,自己设置了一个高的值,但别人可能设置了一个更高的值,所以这个功能没法精确的控制CPU使用率。 ``` root@cld-dnode14-37:/sys/fs/cgroup/cpu# docker run --name docker1 --cpu-shares 1000 dockerhub.nie.netease.com/whale/containers\_benchmark:cpu root@cld-dnode14-37:/sys/fs/cgroup/cpu# docker run --name docker2 --cpu-shares 1000 dockerhub.nie.netease.com/whale/containers\_benchmark:cpu root@cld-dnode14-37:/sys/fs/cgroup/cpu/docker# cat 07acc0da18c8311f9aa85386a9c85647f524f8ca7ae4f35c0c7500e1777c6d6b/cpu.shares 1000 root@cld-dnode14-37:/sys/fs/cgroup/cpu/docker# cat 80722a5437326b6469154d1b0ecd5a06118f4c723b30e825520e91a867bf087a/cpu.shares 1000 real 0m11.683s user 3m47.568s sys 0m0.024s real 0m11.241s user 3m47.604s sys 0m0.044s 当只运行一个容器时,数据如下: real 0m5.990s user 3m47.964s sys 0m0.044s ``` ![](https://img.kancloud.cn/20/41/2041df4dbcd75ae330059ae4336e1229_1096x1112.png) ### 结论: 1\. 三种cpu资源隔离的效果基本相同,在资源分配比率相同的情况下,它们都提供了差不多相同的计算能力。 2\. cpuset隔离方式是以分配核心的方式进行资源隔离,可以提供的资源分配最小粒度是核心,不能提供更细粒度的资源隔离,但是隔离之后运算的相互影响最低。需要注意的是在服务器开启了超线程的情况下,要小心选择分配的核心,否则不同cgroup间的性能差距会比较大。 3\. cpuquota给我们提供了一种比cpuset可以更细粒度的分配资源的方式,并且保证了cgroup使用cpu比率的上限,相当于对cpu资源的硬限制。 4\. cpushares给我们提供了一种可以按权重比率弹性分配cpu时间资源的手段:当cpu空闲的时候,某一个要占用cpu的cgroup可以完全占用剩余cpu时间,充分利用资源。而当其他cgroup需要占用的时候,每个cgroup都能保证其最低占用时间比率,达到资源隔离的效果。 ## CPU资源隔离在sys较高的情况下的表现 我们不可能针对所有场景得出结论,想要找到适用于自己场景的隔离方式,还是需要在自己的环境中进行充分测试。在此只能介绍方法,以及针对一个场景的参考数据,仅此而已。单就这一个测试来说,它仍然不够全面,无法体现出内核cpu资源隔离的真正面目。 众所周知,cpu使用主要分两个部分,user和sys。上面这个测试,由于测试用例的选择,只关注了user的使用。那么如果我们的sys占用较多会变成什么样呢? 让sys部分的cpu占用变高。基于筛质数进行改造即可,我们这次让每个筛质数的线程,在做运算之前都用非阻塞方式open()打开一个文件,每次拿到一个数运算的时候,循环中都用系统调用read()读一下文件。以此来增加sys占用时间的比率。 ### 内核资源不冲突的情况 基于筛质数进行改造即可,我们这次让每个筛质数的线程,在做运算之前都用非阻塞方式open()打开一个文件,每次拿到一个数运算的时候,循环中都用系统调用read()读一下文件。以此来增加sys占用时间的比率。 测试方法: 创建两个cgroup A, B ,A中有进程在不断循环使用cpu,然后分别看在不同资源分配比率下的B 在筛质数程序运行的时间。 ![](https://img.kancloud.cn/44/c5/44c5415a082281ccc1bd8057624b7926_1102x1126.png) | Shares A/ shares B | cpuset realtime | cpushare realtime | cpuquota realtime | | :-------: | :--------:| :------: |:------: | | 4000/36000 | | | | | Shares A/ shares B | cpuset systime | cpushare systime | cpuquota systime | | 4000/36000 | | | | ### 结论: 对比循环测试,三种隔离方式都呈现出随着资源的增加进程是执行的总时间线性下降,并且隔离效果区别不大。由于调用read的次数一样,systime的使用基本都稳定在一个固定的时间范围内。这说明,在sys占用较高的情况下,各种cpu资源隔离手段都表现出比较理想的效果。 ### 内核资源冲突的情况 上一轮测试中,每个cgroup中的线程打开的文件都不是同一个文件,内核在处理这种场景的时候,并不需要使用内核中的一些互斥资源(比如自旋锁或者屏障)进行竞争条件的处理。如果环境变成大家read的是同一个文件,那么情况就可能有很大不同了。 | Shares A/ shares B | cpuset realtime | cpushare realtime | cpuquota realtime | | :-------: | :--------:| :------: |:------: | | 4000/36000 | | | | | Shares A/ shares B | cpuset systime | cpushare systime | cpuquota systime | | 4000/36000 | | | | ### 结论: 当线程同时read同一个文件时,时间的消耗并不在呈现线性下降的趋势了,而且,随着分配的资源越来越多,sys占用时间也越来越高,这种现象如何解释呢? 本质上来讲,使用cgroup进行资源隔离时,内核资源仍然是共享的。 如果业务使用内核资源如果没有产生冲突,那么隔离效果应该会比较理想,但是业务一旦使用了会导致内核资源冲突的逻辑时,那么业务的执行效率就会下降,此时可能所有进程在内核中处理的时候都可能会在竞争的资源上忙等(如果使用了spinlock)。自然的,如果多个cgroup的进程之间也正好使用了可能会导致内核触发竞争条件的资源时,自然也会发生所谓的cgroup之间的相互影响。可能的现象就是,当某一个业务A的cgroup正在运行着,突然B业务的cgroup有请求要处理,会导致A业务的响应速度和处理能力下降。而这种相互干扰,正是资源隔离手段想要尽量避免的。我们认为,如果出现了上述效果,那么资源隔离手段就是打了折扣的。 根据我们的实验结果可以推论,在内核资源有竞争条件的情况下,cpuset的资源隔离方式表现出了相对其他方式的优势,cpushare方式的性能折损尚可接受,而cpuquota表现出了最差的性能,或者说在cpuquota的隔离条件下,cgroup之间进程相互影响的可能性最大。 ## 参考文章: 1\. http://liwei.life/2016/01/22/cgroup\_cpu/ 2\. https://segmentfault.com/a/1190000009732550 3\. https://github.com/digoal/blog/blob/master/201606/20160613\_01.md 4\. https://blog.selectel.com/containerization-mechanisms-cgroups/ 5. [https://v2ex.com/member/jerry017cn/topics](https://v2ex.com/member/jerry017cn/topics)