作者:孫祚龍
愛(ài)可生南區(qū)分公司交付服務(wù)部成員,實(shí)習(xí)工程師。負(fù)責(zé)公司產(chǎn)品問(wèn)題排查及日常運(yùn)維工作。
本文來(lái)源:原創(chuàng)投稿
*愛(ài)可生開(kāi)源社區(qū)出品,原創(chuàng)內(nèi)容未經(jīng)授權(quán)不得隨意使用,轉(zhuǎn)載請(qǐng)聯(lián)系小編并注明來(lái)源。
引言
前陣子處理這樣一個(gè)案例,某客戶(hù)的實(shí)例 MySQLd 進(jìn)程內(nèi)存經(jīng)常持續(xù)增加導(dǎo)致最終被 OOM killer。作為 DBA 肯定想知道有哪些原因可能會(huì)導(dǎo)致 OOM(內(nèi)存溢出)。
此篇文章敘述個(gè)人的一些拙見(jiàn)~
先介紹下這位朋友:OOM-killer
OOM Killer(Out of Memory Killer) 是當(dāng)系統(tǒng)內(nèi)存嚴(yán)重不足時(shí) linux 內(nèi)核采用的殺掉進(jìn)程,釋放內(nèi)存的機(jī)制。
OOM Killer 通過(guò)檢查所有正在運(yùn)行的進(jìn)程,然后根據(jù)自己的算法給每個(gè)進(jìn)程一個(gè) badness 分?jǐn)?shù),擁有最高 badness 分?jǐn)?shù)的進(jìn)程將會(huì)在內(nèi)存不足時(shí)被殺掉。
它打分的算法如下:
- 某一個(gè)進(jìn)程和它所有的子進(jìn)程都占用了很多內(nèi)存的將會(huì)打一個(gè)高分。
- 為了釋放足夠的內(nèi)存來(lái)解決這種情況,將殺死最少數(shù)量的進(jìn)程(最好是一個(gè)進(jìn)程)。
- 內(nèi)核進(jìn)程和其他較重要的進(jìn)程會(huì)被打成相對(duì)較低的分。
上面打分的標(biāo)準(zhǔn)意味著,當(dāng) OOM killer 選擇殺死的進(jìn)程時(shí),將選擇一個(gè)使用大量?jī)?nèi)存,有很多子進(jìn)程且不是系統(tǒng)進(jìn)程的進(jìn)程。
簡(jiǎn)單來(lái)講,oom-killer 的原則就是損失最小、收益最大,因此它會(huì)讓殺死的進(jìn)程數(shù)盡可能小、釋放的內(nèi)存盡可能大。在數(shù)據(jù)庫(kù)服務(wù)器上,MySQL 被分配的內(nèi)存一般不會(huì)小,因此容易成為 oom-killer 選擇的對(duì)象。
“既然發(fā)生了 OOM,那必然是內(nèi)存不足,內(nèi)存不足這個(gè)問(wèn)題產(chǎn)生原因很多。
首先第一個(gè)就是 MySQL 自身內(nèi)存的規(guī)劃有問(wèn)題,這就涉及到 mysql 相應(yīng)的配置參數(shù)。
另一個(gè)可以想到的原因就是一般部署 MySQL 的服務(wù)器,都會(huì)部署很多的監(jiān)控和定時(shí)任務(wù)腳本,而這些腳本往往缺少必要的內(nèi)存限制,導(dǎo)致在高峰期的時(shí)候占用大量的內(nèi)存,導(dǎo)致觸發(fā) Linux 的 oom-killer 機(jī)制,最終 MySQL 無(wú)辜躺槍犧牲。”
all-important:MySQL 自身內(nèi)存規(guī)劃
說(shuō)到 MySQL 自身的內(nèi)存規(guī)劃,最先想到的就是 MySQL 中各種 buffer 的大小,innodb buffer pool 就是最鶴立雞群的那個(gè)。innodb_buffer_pool_size 參數(shù)的大小究竟如何設(shè)置,才能保證 MySQL 的性能呢?在官網(wǎng)文檔中可以找到這個(gè)參數(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.
意思是在專(zhuān)用數(shù)據(jù)庫(kù)服務(wù)器上,可以將 innodb_buffer_pool_size 設(shè)置為計(jì)算機(jī)物理內(nèi)存大小的 80%。在許許多多前輩的的經(jīng)驗(yàn)中了解到,此參數(shù)的值設(shè)置為物理內(nèi)存的 50%~80% 頗為合理。
舉個(gè)栗子:
innodb buffer pool 分配 76G,每個(gè)連接線(xiàn)程最大可用 160M,最大有 3000 連接數(shù),最大可能使用內(nèi)存總量 545G,但是這臺(tái)實(shí)例所在服務(wù)器的物理內(nèi)存僅僅有 97G,遠(yuǎn)超物理內(nèi)存總量。結(jié)果可想而知,這個(gè)實(shí)例在運(yùn)行中經(jīng)常被 oom-killer 殺死,想必原因之一即是因?yàn)橐婚_(kāi)始 MySQL 自身的內(nèi)存規(guī)劃欠妥。
innodb buffer pool 緩存數(shù)據(jù)的作用相信大家都懂,比如這個(gè) case 中,可以發(fā)現(xiàn)該實(shí)例為寫(xiě)密集,讀請(qǐng)求很少,innodb buffer 對(duì)性能改善作用不大,80% 的內(nèi)存沒(méi)必要,完全可以降低到物理內(nèi)存的50%。
“ 以上是對(duì) OOM 發(fā)生原因的一些見(jiàn)解,那思考一下還有沒(méi)有其他的原因會(huì)導(dǎo)致內(nèi)存溢出的情況呢?不知道大家對(duì)內(nèi)存泄漏是否了解,有沒(méi)有可能 MySQL 因?yàn)閮?nèi)存泄漏堆積演變?yōu)閮?nèi)存溢出,最終 oom-killer ... ”
知識(shí)補(bǔ)給站:內(nèi)存泄漏
內(nèi)存泄漏(Memory Leak)是指程序中己動(dòng)態(tài)分配的堆內(nèi)存由于某種原因程序未釋放或無(wú)法釋放,造成系統(tǒng)內(nèi)存的浪費(fèi),導(dǎo)致程序運(yùn)行速度減慢甚至系統(tǒng)崩潰等嚴(yán)重后果。
內(nèi)存泄漏缺陷具有隱蔽性、積累性的特征,比其他內(nèi)存非法訪(fǎng)問(wèn)錯(cuò)誤更難檢測(cè)。因?yàn)閮?nèi)存泄漏的產(chǎn)生原因是內(nèi)存塊未被釋放,屬于遺漏型缺陷而不是過(guò)錯(cuò)型缺陷。此外,內(nèi)存泄漏通常不會(huì)直接產(chǎn)生可觀察的錯(cuò)誤癥狀,而是逐漸積累,降低系統(tǒng)整體性能,極端的情況下可能使系統(tǒng)崩潰。
上文說(shuō)到內(nèi)存泄漏具有隱蔽性,就是不容易被發(fā)現(xiàn)唄......為之奈何?
那咱們就去找一個(gè)可以檢測(cè)內(nèi)存泄漏的工具:valgrind
關(guān)于 valgrind 工具
Valgrind 是一個(gè)用于構(gòu)建動(dòng)態(tài)分析工具的工具框架。它提供了一組工具,每個(gè)工具都執(zhí)行某種調(diào)試、分析或類(lèi)似的任務(wù),以幫助您改進(jìn)程序。Valgrind 的體系結(jié)構(gòu)是模塊化的,因此可以輕松地創(chuàng)建新工具,而不會(huì)影響現(xiàn)有的結(jié)構(gòu)。
標(biāo)配了許多有用的工具:
- Memcheck 是內(nèi)存錯(cuò)誤檢測(cè)器。
- Cachegrind 是一個(gè)緩存和分支預(yù)測(cè)探查器。
- Callgrind 是一個(gè)生成調(diào)用圖的緩存分析器。
- Helgrind 是線(xiàn)程錯(cuò)誤檢測(cè)器。
- DRD 還是線(xiàn)程錯(cuò)誤檢測(cè)器。
- Massif 是堆分析器。
- DHAT 是另一種堆分析器。
- SGcheck 是一種實(shí)驗(yàn)性工具,可以檢測(cè)堆棧和全局陣列的溢出。
- BBV 是一個(gè)實(shí)驗(yàn)性 SimPoint 基本塊矢量生成器。
關(guān)于內(nèi)存泄漏,我們需要使用 valgrind 的默認(rèn)工具,也就是 memcheck 工具。
Memcheck 是內(nèi)存錯(cuò)誤檢測(cè)器。它可以檢測(cè)以下和內(nèi)存相關(guān)的問(wèn)題:
- 使用未初始化的內(nèi)存
- 讀取/寫(xiě)入已釋放的內(nèi)存
- 讀取/寫(xiě)入 malloc 塊的末端
- 內(nèi)存泄漏
- 對(duì) malloc/new/new[]與free/delete/delete[] 的不匹配使用
- 雙重釋放內(nèi)存
Valgrind Memcheck 工具的用法如下:
valgrind --tool=memcheck ./a.out
從上面的命令可以清楚地看到,主要的命令是“ Valgrind”,而我們要使用的工具由選項(xiàng)“ --tool”指定。上面的“ a.out ” 表示我們要在其上運(yùn)行 memcheck 的可執(zhí)行文件。此外還可以使用其他的命令行選項(xiàng),以滿(mǎn)足我們的需要。運(yùn)行的程序結(jié)束后,會(huì)生成這個(gè)進(jìn)程的內(nèi)存分析報(bào)告。
“ OK,工具有了,這就如同摸金校尉拿到了洛陽(yáng)鏟,寶藏還會(huì)遠(yuǎn)嗎~ 還不快找?guī)讐K地挖掘試試?”
搞個(gè)測(cè)試找找感覺(jué)
1. 使用 valgrind 的 memcheck 工具啟動(dòng) 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é)束后查看檢測(cè)報(bào)告:
==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)
在報(bào)告的最后的總結(jié)中發(fā)現(xiàn)程序退出時(shí)有部分內(nèi)存未釋放,而且存在潛在的內(nèi)存泄漏。通過(guò)向上查看具體的信息,分析后發(fā)現(xiàn)主要集中在 performance_schema,偶然發(fā)現(xiàn)了一個(gè)疑點(diǎ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)程序退出時(shí)幾乎沒(méi)有內(nèi)存未釋放,也不存在潛在的內(nèi)存泄漏。三次測(cè)試過(guò)后,發(fā)現(xiàn)結(jié)果是一致的。這是什么原因?
“ 大家都知道 MySQL 的 performance schema 用于監(jiān)控 MySQL server 在一個(gè)較低級(jí)別的運(yùn)行過(guò)程中的資源消耗、資源等待等情況,但它為什么可能會(huì)導(dǎo)致內(nèi)存泄漏呢,看來(lái)關(guān)于 ps 還有不少待挖掘的寶藏哦~ ”
最后一個(gè)小總結(jié)
1. 注意 MySQL 自身的內(nèi)存規(guī)劃,為保證 MySQL 的性能,innodb buffer pool 大小設(shè)置要合理,可以根據(jù)實(shí)例讀寫(xiě)負(fù)載的情況適當(dāng)調(diào)整 buffer pool 的大小。并且 innodb buffer 與連接會(huì)話(huà)內(nèi)存的總和盡量不要超過(guò)系統(tǒng)物理內(nèi)存。
2. 調(diào)整 oom_score_adj 參數(shù)(/proc/<pid>/oom_score_adj),將 MySQL 被 oom-killer 鎖定的優(yōu)先級(jí)降低。這個(gè)參數(shù)值越小,越不容易被鎖定。
3. 加強(qiáng)內(nèi)存的監(jiān)控和報(bào)警,一旦報(bào)警,DBA 應(yīng)該迅速介入,選擇性 Kill 掉一些占用較多內(nèi)存的連接。
4. 在開(kāi)啟 performance_schema 時(shí),會(huì)有額外的內(nèi)存開(kāi)銷(xiāo),通過(guò) valgrind-memcheck 內(nèi)存分析工具發(fā)現(xiàn),較大概率發(fā)生內(nèi)存泄漏。它有可能也會(huì)導(dǎo)致 OOM,在場(chǎng)景中若不需要 performance_schema 可以完全禁用,或需要盡量只開(kāi)啟必要的 instrument。