上图为oom kill后的top输出,因为该mysqld变为僵尸进程故一直没有释放内存。
mysql的BP设置为 106G,但是其RES分别达到125G和119G,加起来接近机器物理内存上限,而机器swap只有7G且被消耗完毕。
至此原因已经很清晰,解决方案也很简单,将BP调小90G。
注:自调整截至目前超过10天,没有再发生类似故障。
延伸
1 mysql内存开销
Innodb_buffer_pool_size定义了缓存池的大小,但是缓冲池本身需要额外的数据结构进行管理。
比如,缓冲池每个page都需要一个buf_block_t管理,这部分内存没有计入参数。
各种额外消耗加起来约占整个BP的8%,也有资料说是10%,具体可参看。
这些只是global buffer的开销,加上session buffer,Mysql所需的内存只会更高。
2 为什么会发生swap
首先大致说一下linux的内存管理,numa架构下linux内存被分为多个node,非numa则只有1个,由pg_data_t描述,每个node又分为3个zone,由zone_struct结构体描述。
每个zone都有active_lru和 inactive_lru,每个lru又各分为anon匿名页和file cache映射页链表,总计4个LRU;
zone同时定义了pages_low,pages_min和pages_high,当zone可用内存小于pages_low时唤醒kswapd回收内存,而当其小于pages_min时则以同步方式唤醒kswapd,直到zone可用内存达到pages_high为止;
Linux会缓存很多数据,譬如page cache和slab cache,这部分内存在回收时会先同步到磁盘然后直接重用,而对于其他内存页,诸如用户态地址空间的匿名页,以及IPC共享内存区的页,只能将其置换到swap分区,不可直接回收。
OS何时回收内存?
1 定期回收:kswapd定期唤醒,当zone空闲内存小于pages_low则进行页面回收,小于pages_min则以同步方式回收;
2 直接回收:linux为用户进程分配内存或者创建缓冲区,而当前系统又没有足够多物理内存时,则linux会进行页面回收;当OS尝试内存回收后仍无法获取足够多的页面,则调用find_bad_process并进行OOM kill;
不管哪种回收方式,最后都调用shrink_list(),对4个链表的扫描逻辑定义在vmscan.c中的get_scan_count函数内,其变量scan_balance决定了要回收哪个lru的内存,大致逻辑如下:
1. 如果系统禁用了swap或者没有swap空间,则只扫描file based的链表,即不进行匿名页链表扫描
if (!sc->may_swap || (get_nr_swap_pages() <= 0)) {
scan_balance = SCAN_FILE;
goto out;
}
2. 如果当前进行的不是全局页回收,并且swappiness=0,则不进行匿名页链表扫描
if (!global_reclaim(sc) && !vmscan_swappiness(sc)) {
scan_balance = SCAN_FILE;
goto out;
}
3. 如果是全局页回收,并且空闲内存和file based链表page数目相加都小于zone->pages_high,则进行匿名页回收,即便swappiness=0,系统也会进行swap
if (global_reclaim(sc)) {
unsigned long zonefile;
unsigned long zonefree;
zonefree = zone_page_state(zone, NR_FREE_PAGES);
zonefile = zone_page_state(zone, NR_ACTIVE_FILE) +
zone_page_state(zone, NR_INACTIVE_FILE);
if (unlikely(zonefile + zonefree <= high_wmark_pages(zone))) {
scan_balance = SCAN_ANON;
goto out;
}
}
4. 如果系统inactive file链表比较充足,则不考虑进行匿名页的回收,即不进行swap
if (!inactive_file_is_low(lruvec)) {
scan_balance = SCAN_FILE;
goto out;
}
至此,我们可以大致了解swappiness=0的作用,其并不能完全禁止swap。
何时触发OOM kill?
当系统内存被消耗殆尽,swap分区也被填满的时候,内核无法分配到新的空闲内存,便会启动OOM删除程序;
其内核调用路径为out_of_memory() – select_bad_process() – oom_kill_process()
其中select_bad_process()负责挑选待杀死的进程,其扫描系统中的每一个进程并调用oom_badness(),该API逻辑如下:
1 获取进程的oom_score_adj,如果其等于OOM_SCORE_ADJ_MIN即-1000则不Kill,该参数由/proc/NNN/ oom_score_adj记录,可手工修改
adj = (long)p->signal->oom_score_adj;
if (adj == OOM_SCORE_ADJ_MIN) {
task_unlock(p);
return 0;
}
2 根据该进程消耗的内存计算分数,如果是root进程则乘以3%,尽量避免其被Kill,最后将points返回
points = get_mm_rss(p->mm) + get_mm_counter(p->mm, MM_SWAPENTS) +
atomic_long_read(&p->mm->nr_ptes) + mm_nr_pmds(p->mm);
task_unlock(p);
if (has_capability_noaudit(p, CAP_SYS_ADMIN))
points -= (points * 3) / 100;
select_bad_process()通过比较每一个进程的oom_badness()返回值,找出得分最高且不是线程组leader的进程,将其返回给out_of_memory(),由其调用oom_kill_process()发送sigkill信号进行扑杀。
除了杀死进程,Linux可以选择在发生OOM时直接panic,当vm.panic_on_oom=1时成立
结束语
至此我们可以大致了解swappiness=0的意义,以及OOM kill发生的原因,为避免此行为应尽量留出充足的内存给OS,一般应为物理内存的20%左右。