Linux内存控制之oom killer机制

接触kubernetes两年有余,从18年初加入kubernetes社区来算,已经一年半,或许是时候写点什么。简单文章如何写好,难点文章如何写透,或许是一种学问,打算推一个系列:《Kubernetes GO》算是对这两年的一个总结。

kubelet组件涉及oom killer相关概念,在此先作学习铺垫。


1、介绍

OOM就是out of memory的缩写,虽然linux kernel有很多的内存管理技巧(从cache中回收、swap out等)来满足各种应用空间的vm内存需求,但是,当你的系统配置不合理,让一匹小马拉大车的时候,linux kernel会运行非常缓慢并且在某个时间点分配page frame的时候遇到内存耗尽、无法分配的状况。应对这种状况首先应该是系统管理员,他需要首先给系统增加内存,不过对于kernel而言,当面对OOM的时候,要根据OOM参数来进行相应的处理。oom kiiler会在内存紧张的时候,会依次kill内存占用较高的进程,发送Sig15(SIGTERM)或Sig9(SIGKILL),取决于内核版本(可见uname -a,>= 2.6.32只会发送sigkill,并在/var/log/message中进行记录。里面会记录一些如pid,process name,cpu mask,trace等信息,通过监控可以发现类似问题。

初探

一个简单分配heap memroy的代码片段(big_mm.c):

1
2
3
4
5
6
7
8
9
10
11
12
13
#define block (1024L*1024L*MB)
#define MB 64L
unsigned long total = 0L;
for(;;) {
// malloc big block memory and ZERO it !!
char* mm = (char*) malloc(block);
usleep(100000);
if (NULL == mm)
continue;
bzero(mm,block);
total += MB;
fprintf(stdout,"alloc %lum mem\n",total);
}

这里有2个地方需要注意:

1、malloc是分配虚拟地址空间,如果不memset或者bzero,那么就不会触发physical allocate,不会映射物理地址,所以这里用bzero填充


2、每次申请的block大小比较有讲究,Linux内核分为LowMemroy和HighMemroy,LowMemory为内存紧张资源,LowMemroy有个阀值,通过free -lm和
/proc/sys/vm/lowmem_reserve_ratio来查看当前low大小和阀值low大小。低于阀值时候才会触发oom killer,所以这里block的分配小雨默认的256M,否则如果每次申请512M(大于128M),malloc可能会被底层的brk这个syscall阻塞住,内核触发page cache回写或slab回收。

测试:gcc big_mm.c -o big_mm ; ./big_mm & ./big_mm & ./big_mm &

2、oom killer机制分析

2.1 参数

panic_on_oom

当kernel遇到OOM的时候,可以有两种选择:
(1)产生kernel panic


(2)积极面对人生,选择一个或者几个最“适合”的进程,启动OOM killer,干掉那些选中的进程,释放内存,让系统勇敢的活下去。

panic_on_oom这个参数就是控制遇到OOM的时候,系统如何反应的。当该参数等于0的时候,表示选择积极面对人生,启动OOM killer。当该参数等于2的时候,表示无论是哪一种情况,都强制进入kernel panic。panic_on_oom等于其他值的时候,表示要区分具体的情况,对于某些情况可以panic,有些情况启动OOM killer。kernel的代码中,enum oom_constraint 就是一个进一步描述OOM状态的参数。系统遇到OOM总是有各种各样的情况的,kernel中定义如下:

(1)CONSTRAINT_CPUSET。cpusets是kernel中的一种机制,通过该机制可以把一组cpu和memory node资源分配给特定的一组进程。这时候,如果出现OOM,仅仅说明该进程能分配memory的那个node出现状况了,整个系统有很多的memory node,其他的node可能有充足的memory资源。

(2)CONSTRAINT_MEMORY_POLICY。memory policy是NUMA系统中如何控制分配各个memory node资源的策略模块。用户空间程序(NUMA-aware的程序)可以通过memory policy的API,针对整个系统、针对一个特定的进程,针对一个特定进程的特定的VMA来制定策略。产生了OOM也有可能是因为附加了memory policy的约束导致的,在这种情况下,如果导致整个系统panic似乎有点不太合适吧。

(3)CONSTRAINT_MEMCG。MEMCG就是memory control group,Cgroup中的memory子系统就是控制系统memory资源分配的控制器,通俗的将就是把一组进程的内存使用限定在一个范围内。当这一组的内存使用超过上限就会OOM,在这种情况下的OOM就是CONSTRAINT_MEMCG类型的OOM。

oom_adj、oom_score_adj和oom_score

准确的说这几个参数都是和具体进程相关的,因此它们位于/proc/xxx/目录下(xxx是进程ID)。假设我们选择在出现OOM状况的时候杀死进程,那么一个很自然的问题就浮现出来:到底干掉哪一个呢?内核的算法倒是非常简单,那就是打分(oom_score,注意,该参数是read only的),找到分数最高的就OK了。那么怎么来算分数呢?
我们触发了oom killer的机制,那么oom killer是计算出选择哪个进程kill呢?我们先来看一下kernel提供给用户态的/proc下的一些参数:

(1)对某一个task进行打分(oom_score)主要有两部分组成,一部分是系统打分,主要是根据该task的内存使用情况。另外一部分是用户打分,也就是oom_score_adj了,该task的实际得分需要综合考虑两方面的打分。如果用户将该task的 oom_score_adj设定成OOM_SCORE_ADJ_MIN(-1000)的话,那么实际上就是禁止了OOM killer杀死该进程。


(2)这里返回了0也就是告知OOM killer,该进程是“good process”,不要干掉它。后面我们可以看到,实际计算分数的时候最低分是1分。


(3)前面说过了,系统打分就是看物理内存消耗量,主要是三部分,RSS部分,swap file或者swap device上占用的内存情况以及页表占用的内存情况。


(4)root进程有3%的内存使用特权,因此这里要减去那些内存使用量。


(5)用户可以调整oom_score,具体如何操作呢?oom_score_adj的取值范围是-1000~1000,0表示用户不调整oom_score,负值表示要在实际打分值上减去一个折扣,正值表示要惩罚该task,也就是增加该进程的oom_score。在实际操作中,需要根据本次内存分配时候可分配内存来计算(如果没有内存分配约束,那么就是系统中的所有可用内存,如果系统支持cpuset,那么这里的可分配内存就是该cpuset的实际额度值)。oom_badness函数有一个传入参数totalpages,该参数就是当时的可分配的内存上限值。实际的分数值(points)要根据oom_score_adj进行调整,例如如果oom_score_adj设定-500,那么表示实际分数要打五折(基数是totalpages),也就是说该任务实际使用的内存要减去可分配的内存上限值的一半。

了解了oom_score_adj和oom_score之后,应该是尘埃落定了,oom_adj是一个旧的接口参数,其功能类似oom_score_adj,为了兼容,目前仍然保留这个参数,当操作这个参数的时候,kernel实际上是会换算成oom_score_adj。

1
2
/proc/[pid]/oom_adj #该pid进程被oom killer杀掉的权重,介于 [-17,15]之间,越高的权重,意味着更可能被oom killer选中,-17表示禁止被kill掉。
/proc/[pid]/oom_score #当前该pid进程的被kill的分数,越高的分数意味着越可能被kill,这个数值是根据oom_adj运算后的结果,是oom_killer的主要参考。

sysctl 下有2个可配置选项:

1
2
vm.panic_on_oom = 0       #内存不够时内核是否直接panic
vm.oom_kill_allocating_task = 1      #oom-killer是否选择当前正在申请内存的进程进行kill

后续

该文为kubelet相关内容的铺垫,涉及到kubelet对于oom killer的使用。


参考资料