1. 程式人生 > 實用技巧 >X偵探所事件薄 | 一次記憶體溢位之謎

X偵探所事件薄 | 一次記憶體溢位之謎

作者 姜宇祥,曾就職於達夢和攜程,目前在CDB/CynosDB資料庫核心團隊擔任TXSQL雲資料庫核心研發,多年深耕資料庫領域,為國內早期一批資料庫核心研發人員。過去曾在達夢經歷了新一代達夢從零開始的整個研發過程,並參與多個版本的迭代與架構調整;還曾在攜程率先開啟MySQL的定製開發,為線上業務提供支援。另一方面,他也積極參與MySQL開源社群在中國成長過程,通過技術宣講與文章編寫助力MySQL在中國的傳播。

引言

在數字領域,TX王國是一個統御著“成T上P”資料子民的大國,這裡的T和P是極大極大的數,用成千上萬來形容資料量之多並不為過。X偵探事務所就是TX王國中負責MySQL領域管理資料子民的有關部門,而事務所中探員們就是專門負責解決各種各樣突發事件的戰鬥精英。

我們將要講述的是關於這些探員的偵探故事,他們擅長在海量的資料中追尋蛛絲馬跡,屢破奇案。這次,我們將要講述的是一個連環宕機血案的偵破故事。

案發現場

一天,探員T因遇到了一個棘手的MySQL例項宕機問題而頭疼不已,通過內部的監控系統發現一個MySQL資料服務使用的記憶體就像坐了加了速的小汽車一樣飛速上漲。作業系統為了保證整個系統的執行,不得不將該MySQL服務殺死,以釋放足夠的資源用於系統正常運轉。這是一個很嚴重的問題,任何服務的宕機以及記憶體不正常現象都是要優先進行排查並處理。

記憶體溢位(Out Of Memory)

一般是由於程式編寫者對記憶體使用不當,如沒有及時釋放申請的記憶體資源,導致該記憶體一直不能被再次使用而使計算機記憶體被耗盡的現象。殺死程序或重啟計算機可從作業系統層面解決問題,但根本解決辦法還是對程式碼進行改進。

案件經過

面對這種緊急情況,經驗豐富的探員T迅速登入伺服器檢視情況。首先懷疑的是開啟的表太多,導致大量的表物件佔用了記憶體空間。經過對frm檔案和ibd檔案的底層粗略查詢,該MySQL例項上有20多萬張的表。那麼,大量的表物件佔用了記憶體空間的必要條件就成立了。於是進一步檢視,限制表開啟數目的變數“table_definition_cache”是否設定的過大,導致佔用的記憶體過多。

但是,該變數並未如預期中設定的過大,屬於合理範圍。那麼為什麼記憶體還會佔用如此之多?探員T此刻陷入了深深的思考。現在案件似乎走入了一個死衚衕,也就是存在大量的表但是對開啟表的資源限制在了一個合理的範圍內,這似乎是一個悖論。關鍵問題來了,到底是哪裡佔用了大量的資源呢?

作為一個優秀的探員,探員T立刻意識到事件發生的現場應該還會存有大量的案發資訊,於是他立刻又回到案發現場,努力嘗試重現該事件發生的整個過程。這是一個很重要的環節,很多問題的定位都是通過還原重現場景來完成的。經過對線上管控人員的細緻調查,發現了一條可疑地SQL語句,每當執行該語句的時候,記憶體使用就會不可遏制的向上增長,這條語句就是:


SELECT table_schema, table_name, partition_name, table_rows

FROM information_schema.partitions

WHERE partition_name IS NOT NULL	

ORDER BY table_schema, table_name;

通過對該語句的跟蹤,發現該語句主要完成兩件事情:

(1)遍歷開啟MySQL例項的所有表並獲取這些表資訊

(2)現在將這些資訊寫入臨時建立的表中

從以往的經驗來看,臨時建立的表不會佔用太多的資源,而且理論上二十多萬行的資料也不會佔用太多空間,於是遍歷所有表這個操作就變得愈發可疑。這也聯絡上了之前的猜測,“開啟的表太多,導致大量的表物件佔用了記憶體空間”,事情排插到了這一步,探員T 直覺推測很可能就是該操作導致的OOM。真相只有一個,那麼 探員T該如何印證這個猜想呢?

工欲善其事,必先利其器。想要快速的定位問題,探員們必須熟練掌握並使用恰當的排障工具,而MySQL就提供了這樣一個強大的實時執行工具箱——performance_schema。之所以稱之為工具箱,是因為它是很多工具的集合,今天我們要用的是這個工具箱中關於記憶體的工具,其他的工具我們將來會有專門的專題來講述。

現在我們需要在配置檔案中增加如下配置以開啟對記憶體使用的監控:

並通過該語句查詢記憶體使用情況:

Select * from performance_schema.memory_summary_by_thread_by_event_name order by CURRENT_COUNT_USED

desc limit 10;

通過上面的操作,探員T 發現確實是表物件開啟的過多,不過這些表物件不是MySQL Server層開啟的物件,而是儲存引擎層innobase開啟幾乎全部的表物件並進行快取,從而沒有及時釋放導致了大量記憶體的佔用。但對於一個成熟的程式來說不會不回收資源,那麼innodb為什麼沒有回收資源呢?原來對於表的記憶體物件回收是在下面這個後臺執行緒進行回收的

如下程式碼所示,在srv_master_thread的後臺執行緒函式中,會在active和idle兩種情況下進行資源回收。

在頻繁有操作的環境下,idle場景是不會被觸發;而在active場景下,結合如下程式碼分析,平均47秒才會有一次主動的記憶體回收。

俗話說辦法總比問題多,既然定位了問題,那麼就可以解決問題了。探員T在被動釋放記憶體物件的基礎上,innobase每次開啟表時檢測記憶體中表物件的開啟數量,當超過指定的閾值就進行釋放,從而解決了問題。

再次案發

至此,探員T已經完成了OOM問題的定位和解決,在其他相關部門相互協作下,將修改好的新版本釋出到了線上。但就在大家覺得問題已經解決,可以放鬆一下的時候,噩耗傳來,新版本竟又出現了宕機。一波未平一波又起,還未來得及好好休息的探員T又再次披掛上陣來解決問題。

首先要分析是不是新修改引進的問題,一般會用兩個方法:

(1) 快速回滾釋出到線上的版本,對於觸發頻繁的例項,該方法為首選,因為可以快速驗證;

(2) 另一個是審查修改後的原始碼,對於改動較少的版本來說,這個方法可以作為首選。

探員T 首先重新審查了修改的程式碼,這次修改只增加89行的內容,理論上可以很快就定位到問題,而且線上問題的出現頻率並不是很高。

經過反覆從程式碼層面進行分析,卻並沒有能找到引發錯誤的任何蛛絲馬跡。用於回收innobase記憶體物件的函式是經過驗證的函式,這個函式已經伴隨著MySQL釋出的很多版本,無論如何都不應該也不會出現,那麼問題的根本原因會是什麼呢?

此時在 探員T腦海中開始回想事情發生的整個經過:首先是針對innobase記憶體物件優化的修改而引發的服務崩潰,其次是通過線上例項的堆疊瞭解到問題是發生在執行前文中提到的information_schema查詢語句,最後通過分析新增程式碼的邏輯確認該改動沒有問題。

在這種情況下,就不能僅憑靜態的現場進行分析了。正所謂“紙上得來終覺淺,覺知此事要躬行”,需要能夠復現事件的發生,通過coredump或者gdb的斷點是解決這類只有靜態現場但並無思路的好辦法。很多人以為重現問題很簡單,但由於大多數時候的問題是併發造成的,併發的偶然性就造成了問題出現的偶然性。嘗試重現有兩個好處,一是能摸清楚問題發生的規律,這本身就能幫助我們將問題限定在某個範圍;二是,穩定重現可以幫助我們在不停嘗試斷點的設定,同樣會不斷縮小問題的範圍。最終通過上下文環境,進而推斷出問題原因。

首先嚐試的是執行前文中提到的SQL語句,但在多次執行後並未觸發服務崩潰的問題,同時結合上線前跑過的MySQL基本測試,可以判定該問題為併發模式下被觸發。這裡介紹一款比較熱門的工具sysbench,因其易安裝易使用的特點,在DBA和測試人員中被廣泛的使用。首先通過sysbench建立了2萬張資料表並在每張表中插入兩條資料,然後發起壓力測試,測試期間執行上文中的SQL語句。在多次嘗試後,問題再次出現,並通過該方法穩定的重現,得到了出問題的core dump。

以下是在開啟表時出現錯誤的堆疊以及出錯時出現問題的變數。

以下是執行時出錯位點出現宕機的斷言

斷言

MySQL在執行時進行狀態檢查的一種手段,用於斷定某種情況的必然成立,所以被稱為斷言。

通過對core dump的分析,發現問題是發生在開啟表的過程中,快速獲取的資料表記憶體物件出現了記憶體訪問出錯,也就是通過如下方式獲取的記憶體物件。

為什麼會在這一步獲取的記憶體物件會出現錯誤?從這裡看,和之前修改有什麼必然關聯?探員T 又開始回顧出問題時的變數,如下圖所示:

以其豐富的經驗看,此時m_share中的index物件已經被釋放,聯絡之前的改動是innodb在開啟表達到閾值時釋放記憶體物件,那麼也就是說在釋放記憶體物件的時候沒有進行響應的保護。如果是這樣的話的,那麼也就是在innodb在進行active/idle工作時也會出錯,只是由於對於釋放操作函式srv_master_evict_from_table_cache的呼叫不夠頻繁,所以出現問題的概率降低到非常低。於是嘗試修改程式碼,提高釋放記憶體物件的頻率,程式碼修改如下:

重新執行測試驗證。Bingo,得到了同樣的結果,社群版的MySQL同樣會出現宕機的情況,至此,終於確定了問題的根本原因。那麼接踵而至的是,為什麼share物件中的表記憶體物件沒有被保護,在innodb進行active/idle工作時被釋放?此時需要進行追本溯源,對get_share/free_share和dict_table_open/dcit_table_close的過程進行分析,發現如下在innodb中開啟表的順序存在問題。如下圖,當active/idle後臺執行緒釋放了記憶體中的表物件後,事務執行緒恰好獲取了share物件則該share物件中的表記憶體物件都是無效的。

這裡就是涉及到編寫程式碼的一個原則,兩個不同資源的獲取與釋放,在獲取時,被依賴的資源需要放在前面獲取,在釋放時,先獲取資源要後釋放,如下圖所示:

按照這個原則進行程式碼修改,在進行測試驗證,記憶體問題再也沒有出現,至此對OOM問題的修改所引發的隱藏問題也得到解決。這就是編寫程式碼中經常碰到的,當我們修復了一個問題後,極有可能會觸發另外一個隱藏的問題,而D偵探事務所的 探員T,就是將兩個問題串聯起來進行分析,才能順利定位根本原因並進行修正。

後記

探員T寄語:案件終於順利解決了,希望此類案件以後不會再發生了,這種一個bugfix暗戳戳自帶了一個bug真是防不勝防啊,不過我們的探員T經驗足夠豐富所以此次有驚無險,在MySQL這個領域有X偵探所各位身懷絕技的探員們為大家保駕護航,請大家放心~接下來我們還有其他探員的故事,敬請大家期待~

本文由部落格一文多發平臺 OpenWrite 釋出!