swap空間不足導致MySQL被OOM kill案例
背景:
- 某機器記憶體256G,安裝2執行個體mysql,每個 buffer_pool各106G,總計212G;
- 某套DB晚上10:00左右遷移到該環境,第2天早上10:00左右收到OOM kill簡訊,因swap空間不足一個Mysql執行個體被強制kill;
- 該執行個體mysqld進程沒有被徹底清除,而是變成了殭屍進程,導致後續無法重啟該執行個體,最後重啟機器才解決。
調查:
為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%,具體可參看http://mysqlha.blogspot.co.uk/2008/11/innodb-memory-overhead.html。
這些只是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%左右。
本文永久更新連結地址: