上一篇文章說明了bug出現的原因和原理分析,要修復bug似乎已經水到渠成了,但遠沒有這麼簡單,只因為“並發”。要修復問題,首先要做的第一件事情是穩定的復現問題。由於數據庫系統是一個並發系統,並且這個bug只有一定的概率出現,更說明了多個線程在一定的執行序列情況下才會出現這個bug。在沒有用戶請求的情況下,mysql自身的線程就很多,比如主線程,IO線程,監控線程,監聽線程等;有用戶請求的情況下,還會分配工作線程為用戶服務。我們只找和bug相關的線程:purge線程,IO線程,工作線程。Purge線程主要工作是執行purge操作,產生IBUF_OP_DELETE操作;工作線程則主要執行DELETE,INSERT語句,產生IBUF_OP_INSERT和IBUF_OP_DELETE_MARK操作;IO線程則是IBUF的merge操作。為達到重現bug的目的,我們需要產生IBUF_OP_DELETE_MARK,IBUF_OP_INSERT和IBUF_OP_DELETE序列。由於這裡面涉及到purge線程和用戶線程,因此涉及一個協同問題。如何協同?
DEBUG_SYNC
DEBUG_SYNC是mysql源碼中自帶的宏,通過信號的方式實現多個線程間的同步。假設存在兩個線程thread-1(A,B),thread-2(a,b),A,B和a,b分別是thread-1和thread-2的執行順序。為了得到AaBb執行序列,可以通過DEBUG_SYNC實現。
1) 源代碼中,設置同步點
Session-1(thread-1)
Session-2(thread-2)
A
a
DEBUG_SYNC(“after_A”)
DEBUG_SYNC(“after_a”)
B
b
DEBUG_SYNC(“after_B”)
2) Session設置
Session-1(thread-1)
Session-2(thread-2)
set debug_sync="after_A signal stepA wait_for stepa";
set debug_sync=”after_B singal stepB”;
set debug_sync = "now wait_for stepA ";
set debug_sync = "after_a signal stepa wait_for stepB";
A
a
B
b
通過在代碼中設置同步點,以及在會話中設置同步關系即可以實現在並發環境下穩定地得到AaBb序列。注意到2)中的藍色字體表示同步點,紅色字體signal和 wait_for表示動作。比如“after_A signal stepA wait_for stepa”表示執行到同步點after_A後,發出信號after_A,並且等待stepa信號的到來。另外,其中now是一個特殊的同步點,表示執行到指定的位置後等待。debug_sync似乎已經完美地解決了同步問題,但問題是,通過debug_sync同步,需要會話設置等待或發出信號動作,如果需要和後台線程同步怎麼辦?而這個bug恰好又需要和後台線程purge,以及IO線程同步。別著急,有需求,就一定有解,mysql還有另外一種協同方法,DBUG_EXECUTE_IF。
DBUG_EXECUTE_IF
DBUG_EXECUTE_IF原理就很簡單了,就是在某一點執行一定的操作,至於是什麼操作則沒有限制。顯然,我們可以在某一個點執行sleep函數,雖然無法做到嚴格的同步,但也基本能滿足需求。DEBUG_EXECUTE_IF的基本格式為:DEBUG_EXECUTE_IF(key, code),比如為了得到AaBb序列,可以通過DBUG_EXECUTE_IF實現
1) 源代碼中,設置操作點
Session-1(thread-1)
Session-2(thread-2)
DEBUG_EXECUTE_IF (“before_a”,{my_sleep(1)})
A
a
DEBUG_EXECUTE_IF (“after_A”,{my_sleep(3)})
DEBUG_SYNC(“after_b”, {my_sleep(5)})
B
b
DEBUG_SYNC(“after_B”, {my_sleep(7)})
2) Session設置
這裡為了讓後台線程也起作用,可以設置全局debug變量,若只想對本session起作用,可以設置會話debug變量。設置後,執行到對應的操作點時,則會額外執行DEBUG_EXECUTE_IF裡面的代碼。通過sleep方式來同步是一種取巧的方式,在實際情況中,需要不斷調整sleep的時間,盡可能保證能按照預設的順序執行。設置如下:紅色部分是加的代碼。
set global debug=”+d,before_a, after_A, after_B, after_b”;
實踐
對於我們這個案例而言,為了得到指定的IBUF序列(IBUF_OP_DELETE_MARK,IBUF_OP_INSERT和IBUF_OP_DELETE),首先保證操作頁面不出現在buffer_pool中;其次,執行INSERT操作產生的IBUF_OP_INSERT必需先於PURGE執行,最後,在整個過程中頁面不能被MERGE,否則也將功虧一篑。對於第一點,可以通過將buffer_pool設置地比較小,然後掃描一個大表達到清理buffer_pool的目的;對於第二點,由於執行IBUF緩存的函數是IBUF_INSERT,針對類型為IBUF_OP_DELETE,強制其等待;對於第三點,在執行merge ibuf的地方,也強制其等待,並且比PURGE等待的時間要更長。具體設置點如下:
/*storage/innobase/ibuf/ibuf0ibuf.cc: ibuf_insert*/ ibuf_insert { DBUG_EXECUTE_IF("sleep_ibuf_insert",{ if (op == IBUF_OP_DELETE) my_sleep(3000000); }); ...... ibuf_insert_low(); } /* storage/innobase/ibuf/ibuf0ibuf.cc:ibuf_merge_pages*/ ibuf_merge_pages { ...... DBUG_EXECUTE_IF("sleep_ibuf_merge",{my_sleep(5000000);}); buf_read_ibuf_merge_pages(sync, space_ids, space_versions, page_nos, *n_pages);//merge操作 }
總結
在整個重現bug的過程中,其實還有很多細節,因為代碼中總是會有很多if else的判斷,以及特定標記位導致的特殊操作,因此打日志和debug很重要,通過打日志,了解大體執行流程;通過debug來了解細節,從而能掌握關鍵路徑,在關鍵路徑上設置合適的同步點,則能在並發環境下得到想要的執行序列。
參考文檔
http://www.gpfeng.com/?p=461
http://hedengcheng.com/?p=238
http://www.cnblogs.com/cchust/p/4544518.html