[TOC] # Cgroups 之Memory资源限制 基准测试 @(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) | /proc/sys/vm/overcommit\_memory:该参数调整宿主是否接受超大内存请求。 默认为0,该设置可能造成系统中的可用内存超载。 设置为1,会增大内存超载的可能性,但也可以增强大量使用内存任务的性能。 ## VM & Container 内存带宽对比 当我们处理内存限制的时候,更多需要关心的是**当内存超限了会发生什么**和 **对边界条件的处理** 当限制内存时,我们最好先想清楚如果内存超限了会发生什么?该怎么处理?业务是否可以接受这样的状态? ## 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的影响 在默认场景下,如果你使用了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的状态。 ## 内存软限制以及内存超卖 ``` 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的内存使用。 根据软限制的这些特点,我们应该明白如果想要软限制生效,应该把它的值设置成小于硬限制。 ## 进程迁移时的内存charge ``` memory.move_charge_at_immigrate: 打开或者关闭进程迁移时的内存记账信息。 ``` 进程可以在多个cgroup之间切换,所以内存限制必须考虑当发生这样的切换时,进程进入的新cgroup中记录的内存使用量是重新从0累计还是把原来cgroup中的信息迁移过来? 当这个开关设置为0的时候是**关闭这个功能,相当于不累计之前的信息**,默认是1,**迁移的时候要在新的cgroup中累积(charge)原来信息,并把旧group中的信息给uncharge掉**。如果新cgroup中没有足够的空间容纳新来的进程,首先内核会在cgroup内部回收内存,如果还是不够,就会迁移失败。 ## 内存压力通知机制 最后,内存的资源隔离还提供了一种**压力通知机制**。当cgroup内的内存使用量达到某种压力状态的时候,内核可以通过eventfd的机制来通知用户程序,这个通知是通过**cgroup.event\_control和memory.pressure\_level**来实现的。 **使用方法**是: 使用eventfd()创建一个eventfd,假设叫做efd,然后open()打开memory.pressure\_level的文件路径,产生一个另一个fd,我们暂且叫它cfd,然后将这两个fd和我们要关注的内存压力级别告诉内核,让内核帮我们关注条件是否成立,通知方式就是把以上信息按这样的格式:” “写入cgroup.event\_control。然后就可以去等着efd是否可读了,如果能读出信息,则代表内存使用已经触发相关压力条件。 **压力级别**的level有三个: “low”:表示内存使用已经达到触发内存回收的压力级别。 “medium”:表示内存使用压力更大了,已经开始触发swap以及将活跃的cache写回文件等操作了。 “critical”:到这个级别,就意味着内存已经达到上限,内核已经触发oom killer了。 ----- 程序从efd读出的消息内容就是这三个级别的关键字。我们可以通过这个机制,建立一个内存压力管理系统,在内存达到相应级别的时候,触发响应的管理策略,来达到各种自动化管理的目的。 Linux的内存限制要说的就是这么多了,当我们限制了内存之后,相对于使用实体机,实际上对于应用来说可用内存更少了,所以业务会相对更经常地暴露在内存资源紧张的状态下。 相对于虚拟机(kvm,xen),多个cgroup之间是共享内核的,我们可以从内存限制的角度思考一些关于“容器”技术相对于虚拟机和实体机的很多特点: 1\. 内存更紧张,应用的内存泄漏会导致相对更严重的问题。 2\. 容器的生存周期时间更短,如果实体机的开机运行时间是以年计算的,那么虚拟机则是以月计算的,而容器应该跟进程的生存周期差不多,顶多以天为单位。所以,容器里面要跑的应用应该可以被经常重启。 3\. 当有多个cgroup(容器)同时运行时,我们不能再以实体机或者虚拟机对资源的使用的理解来规划整体运营方式,我们需要更细节的`理解什么是cache,什么是swap,什么是共享内存,它们会被统计到哪些资源计数中?`在内核并不冲突的环境,这些资源都是独立给某一个业务使用的,在理解上即使不是很清晰,也不会造成歧义。但是在cgroup中,我们需要彻底理解这些细节,才能对遇到的情况进行预判,并规划不同的处理策略。 ## 参考文档 1\. https://segmentfault.com/a/1190000008125359 2\. http://liwei.life/2016/01/22/cgroup\_memory/ 3. [https://toutiao.io/posts/yqmz8i/preview](https://toutiao.io/posts/yqmz8i/preview) ---- 如果页表中已经存在需要影射的内存,则检查是否要对内存进行写操作,如果不写,那就直接复用,如果要写,就发生COW写时复制,此时的COW跟上面的处理过程不完全相同,在内核中,这里主要是通过do\_wp\_page方法实现的。 如果需要申请新内存,则都会通过alloc\_page\_vma申请新内存,而这个函数的核心方法是\_\_alloc\_pages\_nodemask,也就是Linux内核著名的内存管理系统**伙伴系统**的实现。 分配过程先会检查空闲页表中有没有页可以申请,实现方法是:get\_page\_from\_freelist,我们并不关心正常情况,分到了当然一切ok。更重要的是异常处理,如果空闲中没有,则会进入\_\_alloc\_pages\_slowpath方法进行处理。 这个处理过程的主逻辑大概这样: 唤醒kswapd进程,把能换出的内存换出,让系统有内存可用。 继续检查看看空闲中是否有内存。有了就ok,没有继续下一步: 尝试清理page cache,清理的时候会将进程置为D状态。 如果还申请不到内存则: 启动oom killer干掉一些进程释放内存,如果这样还不行则: 回到步骤1再来一次!