memory cgroup
Cgroup的memory子系统,即memory cgroup(memcg),提供了对系统中一组进程的内存行为的管理,从而对整个系统中对内存有不用需求的进程或应用程序区分管理,实现更有效的资源利用和隔离。
在实际业务场景中,为了防止一些应用程序对资源的滥用(可能因为应用本身的bug,如内存泄露),导致对同一主机上其他应用造成影响,我们往往希望可以控制应用程序的内存使用量,这是memcg提供的主要功能之一,当然它还可以做的更多。
Memcg的应用场景,往往来自一些虚拟化的业务需求,所以memcg往往作为cgroup的一个子系统与容器方案一起应用。在容器方案中,与一般的虚拟化方案不同,memcg在管理内存时,并不会在物理内存上对每个容器做区分,也就是说所有的容器使用的是同一个物理内存(有一种例外情况,如果存在多个内存节点,则可以通过cgroup中的cpuset子系统将不同的内存节点应用到不同的容器中)。对于共用的物理内存,memcg也不会对不同的容器做物理页面的预分配,也就是说同一个内存page,可能会被容器A使用,也可能被容器B使用。
所以memcg应用在容器方案中,虽然没有实现真正意义上的内存虚拟化,但是通过内核级的内存管理,依然可以实现某种意义上的虚拟化的内存管理,而且是真正的轻量级的。
memcg的优点和用途
内存控制器将一组任务的内存行为与系统的其余部分隔离开来。关于LWN[12]的文章提到了内存控制器的一些可能用途。内存控制器可以用来:
- 隔离一个应用程序或一组应用程序可以隔离需要内存的应用程序,并将其限制在较少的内存中。
- 创建一个有限内存的cgroup;这可以作为使用mem=XXXX引导的一个很好的替代方法。
- 虚拟化解决方案可以控制要分配给虚拟机实例的内存量。
- CD/DVD刻录机可以控制系统其余部分使用的内存数量,以确保刻录机不会因为缺少可用内存而失败。
- 还有其他几个用例;找到一个或者只是为了好玩而使用控制器(学习和破解VM子系统)。
特点
统计匿名页面、文件缓存、交换缓存使用并限制它们。
- 页面只链接到 per-memcg LRU,没有全局 LRU。
- 可选地,可以计算和限制内存+交换使用。
- 分级记录
- 软限制
- 在移动任务时移动(充值)帐户是可选的。
- 使用阈值通知程序
- 内存压力通知程序
- oom-killer 禁用旋钮和 oom-notifier
- Root cgroup 没有限制控制。
控制文件
1.cgroup内存限制
memory.memsw.limit_in_bytes: 内存+swap空间使用的总量限制。
memory.limit_in_bytes:内存使用量限制。
这两项的意义很清楚了,如果你决定在你的cgroup中关闭swap功能,可以把两个文件的内容设置为同样的值即可。
注意:
在调整memsw.limit_in_bytes或limit_in_bytes时,请保证任何时刻 “memsw.limit_in_bytes 都 >= limit_in_bytes”,否则可能修改失败。
比如现在
memsw.limit_in_bytes=1G
limit_in_bytes=1G
要缩小到800MB,那么应该先缩小limit_in_bytes再缩小memsw.limit_in_bytes
2. OOM控制
memory.oom_control: 内存超限之后的oom行为控制。
这个文件中有两个值:
oom_kill_disable 0
默认为0表示打开oom killer,就是说当内存超限时会触发干掉进程。
如果设置为1表示关闭oom killer,此时内存超限不会触发内核杀掉进程。而是将进程夯住(hang/sleep),实际上内核中就是将进程设置为D状态,并且将相关进程放到一个叫做OOM-waitqueue的队列中。这时的进程可以kill杀掉。如果你想继续让这些进程执行,可以选择这样几个方法:
- 增加该cgroup组的内存限制,让进程有内存可以继续申请。
- 杀掉该cgroup组内的其他一些进程,让本组内有内存可用。
- 把一些进程移到别的cgroup组中,让本cgroup内有内存可用。
- 删除一些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的值,去看容器是否处在超限状态,然后根据业务的特点决定如何处理业务。
当我们进行了内存限制之后,内存超限的发生频率要比使用实体机更多了,因为限制的内存量一般都是小于实际物理内存的。所以,使用基于内存限制的容器技术的服务应该多考虑自己内存使用的情况,尤其是内存超限之后的业务异常处理应该如何让服务受影响的程度降到更低。在系统层次和应用层次一起努力,才能使内存隔离的效果达到最好。
3. 内存资源审计
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选项(getconf -a)的话,那么可以看到当前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的状态,表示不使用交换分区。但是依旧可能会发生swapout,如果真的不想发生,建议使用mlock锁定内存。
memory.failcnt
显示内存使用命中数限制
memory.memsw.failcnt
显示内核内存数量使用率命中限制
4. 内存软限制 以及 内存超卖
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的内存使用。
根据软限制的这些特点,我们应该明白如果想要软限制生效,应该把它的值设置成小于硬限制。
5. 进程迁移时的内存charge
memory.move_charge_at_immigrate: 打开或者关闭进程迁移时的内存记录信息。
进程可以在多个cgroup之间切换,所以内存限制必须考虑当发生这样的切换时。
进程进入的新cgroup时,内存使用量是重新从0累计还是把原来cgroup中的信息迁移过来?
设置为0时,关闭这个功能,相当于不累计之前的信息.
默认是1,迁移的时候要在新的cgroup中累积(charge)原来信息,并把旧group中的信息给uncharge掉。
如果新cgroup中没有足够的空间容纳新来的进程,首先内核会在cgroup内部回收内存,如果还是不够,导致进程迁移cgroup失败。
6. 清空cgroup组的内存
memory.force