作者:孫祚龍
愛可生南區(qū)分公司交付服務(wù)部成員,實習(xí)工程師。負(fù)責(zé)公司產(chǎn)品問題排查及日常運(yùn)維工作。
本文來源:原創(chuàng)投稿
*愛可生開源社區(qū)出品,原創(chuàng)內(nèi)容未經(jīng)授權(quán)不得隨意使用,轉(zhuǎn)載請聯(lián)系小編并注明來源。
引言
前陣子處理這樣一個案例,某客戶的實例 MySQLd 進(jìn)程內(nèi)存經(jīng)常持續(xù)增加導(dǎo)致最終被 OOM killer。作為 DBA 肯定想知道有哪些原因可能會導(dǎo)致 OOM(內(nèi)存溢出)。
此篇文章敘述個人的一些拙見~
先介紹下這位朋友:OOM-killer
OOM Killer(Out of Memory Killer) 是當(dāng)系統(tǒng)內(nèi)存嚴(yán)重不足時 linux 內(nèi)核采用的殺掉進(jìn)程,釋放內(nèi)存的機(jī)制。
OOM Killer 通過檢查所有正在運(yùn)行的進(jìn)程,然后根據(jù)自己的算法給每個進(jìn)程一個 badness 分?jǐn)?shù),擁有最高 badness 分?jǐn)?shù)的進(jìn)程將會在內(nèi)存不足時被殺掉。
它打分的算法如下:
- 某一個進(jìn)程和它所有的子進(jìn)程都占用了很多內(nèi)存的將會打一個高分。
- 為了釋放足夠的內(nèi)存來解決這種情況,將殺死最少數(shù)量的進(jìn)程(最好是一個進(jìn)程)。
- 內(nèi)核進(jìn)程和其他較重要的進(jìn)程會被打成相對較低的分。
上面打分的標(biāo)準(zhǔn)意味著,當(dāng) OOM killer 選擇殺死的進(jìn)程時,將選擇一個使用大量內(nèi)存,有很多子進(jìn)程且不是系統(tǒng)進(jìn)程的進(jìn)程。
簡單來講,oom-killer 的原則就是損失最小、收益最大,因此它會讓殺死的進(jìn)程數(shù)盡可能小、釋放的內(nèi)存盡可能大。在數(shù)據(jù)庫服務(wù)器上,MySQL 被分配的內(nèi)存一般不會小,因此容易成為 oom-killer 選擇的對象。
“既然發(fā)生了 OOM,那必然是內(nèi)存不足,內(nèi)存不足這個問題產(chǎn)生原因很多。
首先第一個就是 MySQL 自身內(nèi)存的規(guī)劃有問題,這就涉及到 mysql 相應(yīng)的配置參數(shù)。
另一個可以想到的原因就是一般部署 MySQL 的服務(wù)器,都會部署很多的監(jiān)控和定時任務(wù)腳本,而這些腳本往往缺少必要的內(nèi)存限制,導(dǎo)致在高峰期的時候占用大量的內(nèi)存,導(dǎo)致觸發(fā) Linux 的 oom-killer 機(jī)制,最終 MySQL 無辜躺槍犧牲。”
all-important:MySQL 自身內(nèi)存規(guī)劃
說到 MySQL 自身的內(nèi)存規(guī)劃,最先想到的就是 MySQL 中各種 buffer 的大小,innodb buffer pool 就是最鶴立雞群的那個。innodb_buffer_pool_size 參數(shù)的大小究竟如何設(shè)置,才能保證 MySQL 的性能呢?在官網(wǎng)文檔中可以找到這個參數(shù)的一些描述:
A larger buffer pool requires less disk I/O to access the same table data more than once. On a dedicated database server, you might set the buffer pool size to 80% of the machine's physical memory size.
意思是在專用數(shù)據(jù)庫服務(wù)器上,可以將 innodb_buffer_pool_size 設(shè)置為計算機(jī)物理內(nèi)存大小的 80%。在許許多多前輩的的經(jīng)驗中了解到,此參數(shù)的值設(shè)置為物理內(nèi)存的 50%~80% 頗為合理。
舉個栗子:

innodb buffer pool 分配 76G,每個連接線程最大可用 160M,最大有 3000 連接數(shù),最大可能使用內(nèi)存總量 545G,但是這臺實例所在服務(wù)器的物理內(nèi)存僅僅有 97G,遠(yuǎn)超物理內(nèi)存總量。結(jié)果可想而知,這個實例在運(yùn)行中經(jīng)常被 oom-killer 殺死,想必原因之一即是因為一開始 MySQL 自身的內(nèi)存規(guī)劃欠妥。
innodb buffer pool 緩存數(shù)據(jù)的作用相信大家都懂,比如這個 case 中,可以發(fā)現(xiàn)該實例為寫密集,讀請求很少,innodb buffer 對性能改善作用不大,80% 的內(nèi)存沒必要,完全可以降低到物理內(nèi)存的50%。
“ 以上是對 OOM 發(fā)生原因的一些見解,那思考一下還有沒有其他的原因會導(dǎo)致內(nèi)存溢出的情況呢?不知道大家對內(nèi)存泄漏是否了解,有沒有可能 MySQL 因為內(nèi)存泄漏堆積演變?yōu)閮?nèi)存溢出,最終 oom-killer ... ”
知識補(bǔ)給站:內(nèi)存泄漏
內(nèi)存泄漏(Memory Leak)是指程序中己動態(tài)分配的堆內(nèi)存由于某種原因程序未釋放或無法釋放,造成系統(tǒng)內(nèi)存的浪費,導(dǎo)致程序運(yùn)行速度減慢甚至系統(tǒng)崩潰等嚴(yán)重后果。
內(nèi)存泄漏缺陷具有隱蔽性、積累性的特征,比其他內(nèi)存非法訪問錯誤更難檢測。因為內(nèi)存泄漏的產(chǎn)生原因是內(nèi)存塊未被釋放,屬于遺漏型缺陷而不是過錯型缺陷。此外,內(nèi)存泄漏通常不會直接產(chǎn)生可觀察的錯誤癥狀,而是逐漸積累,降低系統(tǒng)整體性能,極端的情況下可能使系統(tǒng)崩潰。
上文說到內(nèi)存泄漏具有隱蔽性,就是不容易被發(fā)現(xiàn)唄......為之奈何?
那咱們就去找一個可以檢測內(nèi)存泄漏的工具:valgrind
關(guān)于 valgrind 工具
Valgrind 是一個用于構(gòu)建動態(tài)分析工具的工具框架。它提供了一組工具,每個工具都執(zhí)行某種調(diào)試、分析或類似的任務(wù),以幫助您改進(jìn)程序。Valgrind 的體系結(jié)構(gòu)是模塊化的,因此可以輕松地創(chuàng)建新工具,而不會影響現(xiàn)有的結(jié)構(gòu)。
標(biāo)配了許多有用的工具:
- Memcheck 是內(nèi)存錯誤檢測器。
- Cachegrind 是一個緩存和分支預(yù)測探查器。
- Callgrind 是一個生成調(diào)用圖的緩存分析器。
- Helgrind 是線程錯誤檢測器。
- DRD 還是線程錯誤檢測器。
- Massif 是堆分析器。
- DHAT 是另一種堆分析器。
- SGcheck 是一種實驗性工具,可以檢測堆棧和全局陣列的溢出。
- BBV 是一個實驗性 SimPoint 基本塊矢量生成器。
關(guān)于內(nèi)存泄漏,我們需要使用 valgrind 的默認(rèn)工具,也就是 memcheck 工具。
Memcheck 是內(nèi)存錯誤檢測器。它可以檢測以下和內(nèi)存相關(guān)的問題:
- 使用未初始化的內(nèi)存
- 讀取/寫入已釋放的內(nèi)存
- 讀取/寫入 malloc 塊的末端
- 內(nèi)存泄漏
- 對 malloc/new/new[]與free/delete/delete[] 的不匹配使用
- 雙重釋放內(nèi)存
Valgrind Memcheck 工具的用法如下:
valgrind --tool=memcheck ./a.out
從上面的命令可以清楚地看到,主要的命令是“ Valgrind”,而我們要使用的工具由選項“ --tool”指定。上面的“ a.out ” 表示我們要在其上運(yùn)行 memcheck 的可執(zhí)行文件。此外還可以使用其他的命令行選項,以滿足我們的需要。運(yùn)行的程序結(jié)束后,會生成這個進(jìn)程的內(nèi)存分析報告。
“ OK,工具有了,這就如同摸金校尉拿到了洛陽鏟,寶藏還會遠(yuǎn)嗎~ 還不快找?guī)讐K地挖掘試試?”
搞個測試找找感覺
1. 使用 valgrind 的 memcheck 工具啟動 mysql:
valgrind --tool=memcheck --leak-check=full --show-reachable=yes --log-file=/tmp/valgrind-mysql.log /usr/local/mysql/bin/mysqld --defaults-file=/etc/my.cnf --user=root
2. 利用 sysbench 模擬負(fù)載;
3. 進(jìn)程結(jié)束后查看檢測報告:
==29326== LEAK SUMMARY:==29326== definitely lost: 0 bytes in 0 blocks==29326== indirectly lost: 0 bytes in 0 blocks==29326== possibly lost: 549,072 bytes in 1,727 blocks==29326== still reachable: 446,492,944 bytes in 54 blocks==29326== suppressed: 0 bytes in 0 blocks==29326====29326== For counts of detected and suppressed errors, rerun with: -v==29326== ERROR SUMMARY: 339 errors from 339 contexts (suppressed: 0 from 0)
在報告的最后的總結(jié)中發(fā)現(xiàn)程序退出時有部分內(nèi)存未釋放,而且存在潛在的內(nèi)存泄漏。通過向上查看具體的信息,分析后發(fā)現(xiàn)主要集中在 performance_schema,偶然發(fā)現(xiàn)了一個疑點,那我們完全禁用掉 performance_schema 呢?
==9954== LEAK SUMMARY:==9954== definitely lost: 0 bytes in 0 blocks==9954== indirectly lost: 0 bytes in 0 blocks==9954== possibly lost: 0 bytes in 0 blocks==9954== still reachable: 32 bytes in 1 blocks==9954== suppressed: 0 bytes in 0 blocks==9954====9954== For counts of detected and suppressed errors, rerun with: -v==9954== ERROR SUMMARY: 0 errors from 0 contexts (suppressed: 0 from 0)
發(fā)現(xiàn)程序退出時幾乎沒有內(nèi)存未釋放,也不存在潛在的內(nèi)存泄漏。三次測試過后,發(fā)現(xiàn)結(jié)果是一致的。這是什么原因?
“ 大家都知道 MySQL 的 performance schema 用于監(jiān)控 MySQL server 在一個較低級別的運(yùn)行過程中的資源消耗、資源等待等情況,但它為什么可能會導(dǎo)致內(nèi)存泄漏呢,看來關(guān)于 ps 還有不少待挖掘的寶藏哦~ ”
最后一個小總結(jié)
1. 注意 MySQL 自身的內(nèi)存規(guī)劃,為保證 MySQL 的性能,innodb buffer pool 大小設(shè)置要合理,可以根據(jù)實例讀寫負(fù)載的情況適當(dāng)調(diào)整 buffer pool 的大小。并且 innodb buffer 與連接會話內(nèi)存的總和盡量不要超過系統(tǒng)物理內(nèi)存。
2. 調(diào)整 oom_score_adj 參數(shù)(/proc/<pid>/oom_score_adj),將 MySQL 被 oom-killer 鎖定的優(yōu)先級降低。這個參數(shù)值越小,越不容易被鎖定。
3. 加強(qiáng)內(nèi)存的監(jiān)控和報警,一旦報警,DBA 應(yīng)該迅速介入,選擇性 Kill 掉一些占用較多內(nèi)存的連接。
4. 在開啟 performance_schema 時,會有額外的內(nèi)存開銷,通過 valgrind-memcheck 內(nèi)存分析工具發(fā)現(xiàn),較大概率發(fā)生內(nèi)存泄漏。它有可能也會導(dǎo)致 OOM,在場景中若不需要 performance_schema 可以完全禁用,或需要盡量只開啟必要的 instrument。