作者 姜宇祥,曾就職于達夢和攜程,目前在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 發布!
轉載請註明出處,本文鏈接:https://www.uj5u.com/shujuku/199918.html
標籤:其它
上一篇:oracle創建資料庫和用戶、表空間,管理表空間和資料檔案簡介
下一篇:誰是銀行核心資料庫的破局者?
