第一章:学习Linux内存分配策略
今天想提到的是线上一个4G的RDS实例,发生了OOM(out of memory)的问题,MySQL进程被直接Kill掉了。在解释这个问题的时候,我们首先需要从Linux系统内存分配策略讲起。
一般写C语言程序,我们习惯使用malloc动态的申请内存空间(Java由JVM负责内存管理),malloc函数会向操作系统申请一段连续的内存单元,然后返回这段空间的起始地址。如果malloc函数返回为null则表示系统没有可分配的内存空间。这是我们的一般思维,当然这在某些操作系统中确实也是正确的(Solaris)。
但是Linux不是这样的,Linux的内存分配采取的是一种更加积极的分配策略,它假设应用申请了内存空间后并不会立即去使用它,所以允许一定量的超售,当应用真的需要使用它的时候,操作系统可能已经通过回收了其他应用的内存空间而变得有能力去满足这个应用的需求,简单的说,就是允许应用申请比实际可分配空间(包括物理内存和Swap)更多的内存,这个特性称为OverCommit。
这个特性在Linux操作系统里面也是可配的,可以通过设置/proc/sys/vm/overcommit_memory为不同的值来调整OverCommit策略。
overcommit_memory可以取3个值:
0:默认值,由Linux内核通过一些启发式算法来决定是否超售和超售的大小,一般允许轻微的超售,拒绝一些明显不可能提供的请求,同时做一些规则限制,比如不同用户overcommit的大小也不一样。
1:允许,不做限制的超售,当然这个也不是无限大,还受到寻址空间的限制,32位系统最大可能只有4G,64位系统大概16T左右。
2:禁止,禁止超售,系统能够分配的内存不会超过swap+实际物理内存*overcommit_ratio,该值可以通过/proc/sys/vm/overcommit_ratio设置,默认50%。
为了验证Linux的内存分配,我们用个小程序来测试一下:
第一个程序,通过malloc()申请了内存空间以后,并没有立即去使用它。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
$ cat test_1.c #include <stdio.h> #include <stdlib.h> #define MEGABYTE 1024*1024 int main(int argc, char *argv[]) { void *myblock = NULL; int count = 0; while (1) { myblock = (void *) malloc(MEGABYTE); if (!myblock) break; printf("Currently allocating %d MB\n", ++count); } exit(0); } |
第二个程序,每次申请完都立即用1去填充。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
$ cat test2.c #include <stdio.h> #include <stdlib.h> #define MEGABYTE 1024*1024 int main(int argc, char *argv[]) { void *myblock = NULL; int count = 0; while(1) { myblock = (void *) malloc(MEGABYTE); if (!myblock) break; memset(myblock,1, MEGABYTE); printf("Currently allocating %d MB\n",++count); } exit(0); } |
我们来看看两个程序运行的结果,先编译一下。
1 2 |
$ gcc -o test_1 test_1.c $ gcc -o test_2 test_2.c |
第一个程序运行结果:
1 2 3 4 5 6 7 8 |
$ ./test_1 ............ Currently allocating 108480 MB Currently allocating 108481 MB Currently allocating 108482 MB Currently allocating 108483 MB Currently allocating 108484 MB Killed |
第二个程序运行结果:
1 2 3 4 5 6 7 8 |
$ ./test_2 ......... Currently allocating 878 MB Currently allocating 879 MB Currently allocating 880 MB Currently allocating 881 MB Currently allocating 882 MB Killed |
这是在1G的RAM的虚拟机上运行的结果,前者申请了远远超过实际内存的空间,后者并没有超过实际内存可用空间。这就验证了前面叙述的Linux的内存分配策略。
本身这是一个系统的优化,无可厚非。但是我们知道,但凡“超售”都是基于不会有大量程序同时使用资源的假设,这显然也是有风险的。所以Linux又使用了一种OOM Killer(Out Of Memory killer)的机制,在系统可用内存(包括Swap)即将使用完之前,选择性的Kill掉一些进程以求释放一些内存。下一章我们重点讨论一下Linux OOM Killer的机制。
第二章:OOM_killer机制
这里就涉及到一个问题,到底Kill掉谁呢?一般稍微了解一些Linux内核的同学第一反应是谁用的最多,就Kill掉谁。这当然是Linux内核首先考虑的一种重要因素,但是也不完全是这样的,我们查一些Linux的内核方面的资料,可以知道其实Kill谁是由/proc/<pid>/oom_score来决定的,这个值每个进程一个,是由Linux内核的oom_badness()函数负责计算的。那下面我们来仔细读一读badness()函数。
在badness()函数的注释部分,写明了badness()函数的处理思路:
1) we lose the minimum amount of work done
2) we recover a large amount of memory
3) we don’t kill anything innocent of eating tons of memory
4) we want to kill the minimum amount of processes (one)
5) we try to kill the process the user expects us to kill, this algorithm has been meticulously tuned to meet the principle of least surprise … (be careful when you change it)
总的来说就是Kill掉最小数量的进程来获取最大数量的内存,这与我们Kill掉占用内存最大的进程是吻合的。
1 2 3 4 |
/* * The memory size of the process is the basis for the badness. */ points = p->mm->total_vm; |
分数的起始是进程实际使用的RAM内存,注意这里不包括SWAP,即OOM Killer只会与进程实际的物理内存有关,与Swap是没有关系的,并且我们可以看到,进程实际使用的物理内存越多,分数就越高,分数越高就越容易被牺牲掉。
1 2 3 4 5 6 7 8 9 |
/* * Processes which fork a lot of child processes are likely * a good choice. We add the vmsize of the childs if they * have an own mm. This prevents forking servers to flood the * machine with an endless amount of childs */ ... if (chld->mm != p->mm && chld->mm) points += chld->mm->total_vm; |
这段表示子进程占用的内存都会计算到父进程上。
1 2 3 4 5 6 |
s = int_sqrt(cpu_time); if (s) points /= s; s = int_sqrt(int_sqrt(run_time)); if (s) points /= s; |
这表明进程占用的CPU时间越长或者进程运行的时间越长,分数越低,越不容易被Kill掉。
1 2 3 4 5 6 |
/* * Niced processes are most likely less important, so double * their badness points. */ if (task_nice(p) > 0) points *= 2; |
如果进程优先级低(nice值,正值低优先级,负值高优先级),则Point翻倍。
1 2 3 4 5 6 7 |
/* * Superuser processes are usually more important, so we make it * less likely that we kill those. */ if (cap_t(p->cap_effective) & CAP_TO_MASK(CAP_SYS_ADMIN) || p->uid == 0 || p->euid == 0) points /= 4; |
super用户的进程优先级较低。
1 2 |
if (cap_t(p->cap_effective) & CAP_TO_MASK(CAP_SYS_RAWIO)) points /= 4; |
直接可以访问原始设备的进程优先级较高。
1 2 3 4 5 6 |
if (p->oomkilladj) { if (p->oomkilladj > 0) points <<= p->oomkilladj; else points >>= -(p->oomkilladj); } |
每个进程有个oomkilladj 可以设置该进程被kill的优先级,这个参数看上去对Point影响还是比较大的,oomkilladj 最大+15,最小是-17,越大越容易被干掉,这个值由于是移位运算,所以影响还是比较大的。
下面我写个小程序实验一下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
#define MEGABYTE 1024*1024*1024 #include <stdio.h> #include <string.h> #include <stdlib.h> int main(int argc, char *argv[]) { void *myblock = NULL; myblock = (void *) malloc(MEGABYTE); printf("Currently allocating 1GB\n"); sleep(1); int count = 0; while( count < 10) { memset(myblock,1,100*1024*1024); myblock = myblock + 100*1024*1024; count++; printf("Currently allocating %d00 MB\n",count); sleep(10); } exit(0); } |
上面的程序先申请一个1G的内存空间,然后100M为单位,填充这些内存空间。在一个8G内存,400M Swap空间的机器上跑3个上面的进程。我们看一下运行结果:
开启3个test_3进程分别申请了1G的虚拟内存空间(VSZ),然后每隔10s,实际占用的RAM空间就增长100M(RES)。
1 2 3 4 5 |
$ ps aux | egrep "(test|USER)" USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND root 7602 0.7 1.2 1052740 103476 pts/1 S+ 16:07 0:00 ./test_3 root 7603 1.0 1.3 1052740 104400 pts/2 S+ 16:07 0:00 ./test_3 root 7604 1.5 1.2 1052740 103512 pts/3 S+ 16:07 0:00 ./test_3 |
当物理内存空间不足时,如果有SWAP,OS就会开始进行Swap。
当内存是在没有可分配的空间时,7602进程被操作系统Kill掉了。dmesg我们可以看到,7602进程被OS Kill掉,同时oom_score为1000。
1 |
[8550582.112629] Out of memory: Kill process 7602 (test_3) score 1000 or sacrifice child |
这3个进程的oom_adj全部都是默认值0,你也可以实验一下设置了oom_adj的效果,重新启动这3个进程。
1 |
$ echo 15 > /proc/PID/oom_adj |
选择一个进程设置其oomadj的值为15,如果不出意外的话,被kill的进程就会是此进程了。
为了避免自己需要的进程被kill掉,可以通过设置进程的oom_adj来实现。当然,有的人会说,这一切都是超售引起的,既然Linux提供了overcommit_memory可以禁用overcommit特性,那为什么不禁用呢。这有利也有弊,一旦禁用overcommit,就意味着MySQL根本无法申请超过实际内存的空间,而在MySQL中,存在很多动态申请内存空间的地方,如果申请不到,MySQL就会Crash,这大大增加了MySQL宕机的风险,这也是Linux为什么要overcommit的原因。
第三章:如何避免自己的进程被OOM_killer?
有了上面的分析,我们不难看出,如果在不设置oom_adj的前提下,MySQL一般都会成为OOM_Killer的首选对象,因为MySQL一般都是内存的最大占用者。那作为MySQL,我们如何尽量的去规避被Kill的风险呢,下一章我们将重点从MySQL的角度分析如何规避OOM。
我们分析了Linux内存分配的策略以及Linux通过使用OOM_Killer的机制解决了“超售”引起的风险,MySQL同其他的应用程序一样,在操作系统允许的范围内也是可以超售的,一般人理解,Innodb_buffer_pool必须小于实际物理内存,否则MySQL会启动失败。其实这是一个误区,这个不是MySQL层控制的,这个是操作系统(OS)层控制的,就是前面提到的 /proc/sys/vm/overcommit_memory 控制OS是否允许“超售”。如果允许“超售”,则Innodb_buffer_pool可以远远超过实际的内存空间大小,但是这部分空间是没有使用。
讲了这么多,现在言归正传,回到我们最早提到的RDS实例被OS Kill掉的问题上来,前面我们也提到了,一旦实例可用内存不足,MySQL一般都会成为OOM_Killer的首选目标。这里就涉及到两个问题:
1. 为什么会内存不足?
2. 如何让MySQL摆脱被Kill的厄运?
首先我们来看一下第一个问题。内存不足这个问题产生原因很多,但是主要就两个方面,第一个是MySQL自身内存的规划有问题。第二个就是一般部署MySQL的服务器,都会部署很多的监控或者定时任务脚本,而这些脚本往往缺少必要的内存限制,导致在高峰期的时候占用大量的内存,导致触发Linux OOM_Killer机制,MySQL就无辜牺牲了。
那如何才能让MySQL摆脱被Kill的厄运呢? MySQL被Kill的根源在于Linux超售的内存分配机制,前面也提到了,只要存在这种超售的机制,就不可能完全避免某一个应用程序被Kill的风险。那要使得MySQL一定不会被Kill掉,只能禁止操作系统超出实际内存空间的分配内存。但是前面我们也提过,对于部署了MySQL的服务器,我们不建议这么做,因为MySQL的很多内存都是刚开始申请了,并不是立即使用的,OS一旦禁止超售,这不仅对MySQL自身内存规划提出更苛刻的要求,同时也存在内存无法充分利用的问题。同时,MySQL的每个连接的私有内存是动态分配的,如果分配不到,就会直接导致服务器Crash,这样也会增加MySQL Crash的风险。
既然受限于操作系统,无法完全做到避免被Kill,那只能尽量降低MySQL被Kill的几率。我觉得至少可以做下面3个事情:
1) 合理的规划MySQL的内存使用。
2) 调整OOM_adj参数,将MySQL被OOM_Killer锁定的优先级降低。
3) 加强内存的监控和报警,一旦报警,DBA应该迅速介入,Kill掉一些占用较多内存的连接。