由xtrabackup致使的MySQL從庫死鎖分析及參數深究

最近線上執行備份的從庫時出現複製卡死現象,分析之後發現是兩個死鎖,show full processlist的狀態如圖1所示,其中,數據庫版本是官方5.7.18版本,咱們內部作了些許修改,但與這次死鎖無關。mysql

先說一下結論,圖一中:sql

  • 162線程是執行innobackup執行的flush tables with read lock;數據庫

  • 144是SQL線程,並行複製中的Coordinator線程;session

  • 145/146是並行複製的worker線程,145/146worker線程隊列中的事務能夠並行執行。app

144Coordinator線程分發relaylog中事務時發現這個事務不能執行,要等待前面的事務完成提交,因此處於waiting for dependent transaction to commit的狀態。145/146線程和備份線程162造成死鎖,145線程等待162線程 global read lock 釋放,162線程佔有MDL::global read lock 全局讀鎖,申請全局commit lock的時候阻塞等待146線程,146線程佔有MDL:: commit lock,由於從庫設置slave_preserve_commit_order=1,保證從庫binlog提交順序,而146線程執行事務對應的binlog靠後面,因此等待145的事務提交。最終造成了145->162->146->145的死循環,造成死鎖。this

 

一樣的,圖二中:spa

  • 183是備份程序執行的flush tables with read lock;線程

  • 165是SQL線程,並行複製的Coordinator線程;debug

  • 166/167是並行複製的worker線程。日誌

165Coordinator線程分發的事務還不能執行,進入waiting for dependent transaction to commit的狀態,18三、16六、167三個線程造成死鎖,183佔有全局讀鎖,獲取全局commit鎖的時候進入阻塞,等待167釋放事務涉及到表的commit鎖;166,167的事務能夠並行複製,167佔有表級commit鎖,可是事務對應的binlog在後面,阻塞等待166先提交進入waiting for preceding transaction to commit的狀態;166線程事務執行時提交要得到表級commit鎖,但已經被183佔有,因此阻塞等待。這樣造成了183->167->166->183的死鎖。

 

三個線程相互造成死鎖,在個人經驗中仍是不多見的,又由於涉及的MDL鎖是服務層的鎖,死鎖檢測也不會起做用。

1、死鎖緣由分析

一、MDL鎖
參考:http://mysql.taobao.org/monthly/2015/11/04/

2、flush tables with read lock獲取兩個鎖

MDL::global read lock 和MDL::global commit lock,並且是顯示的MDL_SHARED鎖。

//Global_read_lock::lock_global_read_lock

			    MDL_REQUEST_INIT(&mdl_request,MDL_key::GLOBAL, "", "", MDL_SHARED, MDL_EXPLICIT);

			    //Global_read_lock::make_global_read_lock_block_commit

			    MDL_REQUEST_INIT(&mdl_request,MDL_key::COMMIT, "", "", MDL_SHARED, MDL_EXPLICIT);

3、事務執行中涉及兩個鎖

在全部更新數據的代碼路徑裏,除了必須的鎖外,還會額外請求MDL_key::GLOBAL鎖的MDL_INTENTION_EXCLUSIVE鎖;在事務提交前,會先請求MDL_key::COMMIT鎖的MDL_INTENTION_EXCLUSIVE鎖。對於scope鎖來講,IX鎖和S鎖是不兼容的。
4、--slave_preserve_commit_order

For multi-threaded slaves, enabling this variable ensures that 

			    transactions are externalized on theslave in the same order as they appear

			    in the slave's relay log.

slave_preserve_commit_order=1時,relay-log中事務的提交順序會嚴格按照在relay-log中出現的順序提交。
因此,事務的執行和flush tables with read lock語句得到兩個鎖都不是原子的,並行複製時模式下按如下的順序就會出現死鎖。

  • 事務A、B能夠並行複製,relay-log中A在前,slave_preserve_commit_order=1

  • 從庫回放時B事務執行較快,先執行到commit,得到commit鎖,並進入waiting for   preceding transaction to commit的狀態
  • 執行flush tables with read lock,進入waiting  for commit的狀態
  • 事務A執行。事務A若是在FTWRL語句得到global read lock鎖以後執行,那麼事務A就進入waiting for global  read lock的狀態,即第一種死鎖;若是事務A在FTWRL得到global read lock以前執行,同時FTWRL得到global commit鎖以後應用Xid_event提交事務,則進入 waiting for the commit lock的狀態,即第二種死鎖。

2、復現

理解了死鎖出現的緣由後,重現就簡單多了。重現這個死鎖步驟主要是2步:

1、在主庫構造並行複製的事務,利用debug_sync

session 1

			        SET DEBUG_SYNC='waiting_in_the_middle_of_flush_stage SIGNAL s1 WAIT_FOR f';

			        insert into test.test values(13);//事務A

			

			        //session 2

			        SET DEBUG_SYNC= 'now WAIT_FOR s1'; 

			        SET DEBUG_SYNC= 'bgc_after_enrolling_for_flush_stage SIGNAL f';   

			        insert into test.test values(16);//事務B

二、從庫執行,修改源代碼,在關鍵地方sleep若干時間,控制並行複製的worker的執行並留出足夠時間執行flush tables with read lock
修改點以下:

//Xid_apply_log_event::do_apply_event_worker

			        if(w->id==0)

			        {

			            std::cout<<"before commit"<

			            sleep(20);

			        }

			        //pop_jobs_item

			        if(worker->id==0)    

			            sleep(20);

開啓slave之後,觀察show full processlist和輸出日誌,在其中一個worker出現wait for  preceding transaction to commit之後,執行 ftwrl,出現圖1的死鎖;wait for  preceding transaction to commit之後,出現日誌before commit以後,執行 ftwrl,出現圖2的死鎖。

3、如何解決

出現死鎖之後若是不人工干預,IO線程正常,可是SQL線程一直卡住,通常須要等待lock-wait-timeout時間,這個值咱們線上設置1800秒,因此這個死鎖會產生很大影響。

那麼如何解決呢?kill !kill哪一個線程呢?

  • 對圖1的死鎖,146處於wait for  preceding transaction狀態的worker線程實際處於mysql_cond_wait的狀態,kill不起做用,因此只能kill 145線程或者備份線程,若是kill145worker線程,整個並行複製就報錯結束,show slave status顯示SQL異常退出,以後須要手動從新開啓sql線程,因此最好的辦法就是kill執行flush tables with read lock的線程,代價最小。

  • 至於圖2的死鎖,則只能kill掉執行flush tables with read lock的線程。因此出現上述死鎖時,kill執行flush tables with read lock的備份線程就恢復正常,以後擇機從新執行備份便可。

4、如何避免

設置xtrabackup的kill-long-queries-timeout參數能夠避免第一種死鎖的出現,其實不算避免,只是出現之後xtrabackup會殺掉阻塞的執行語句的線程;可是這個參數對第二種死鎖狀態則無能爲力了,由於xtrabackup選擇殺掉的線程時,會過濾Info!=NULL。

另外還有個參數safe-slave-backup,執行備份的時候加上這個參數會停掉SQL線程,這樣也確定不會出現這個死鎖,只是停掉SQL未免太暴力了,我的不提倡這樣作。

能夠設置slave_preserve_commit_order=0關閉從庫binlog的順序提交,關閉這個參數只是影響並行複製的事務在從庫的提交順序,對最終的數據一致性並沒有影響,因此若是無特別要求從庫的binlog順序必須與主庫保持一致,能夠設置slave_preserve_commit_order=0避免這個死鎖的出現。

 

關於xtrabackup  kill-long-query-type參數

首先說下```kill-long-queries-timeout,kill-long-query-type```參數,文檔介紹以下

--KILL-LONG-QUERY-TYPE=ALL|SELECT

This option specifies which types of queries should be killed to 
    unblock the global lock. Default is 「all」.

--KILL-LONG-QUERIES-TIMEOUT=SECONDS**

This option specifies the number of seconds innobackupex waits 
    between starting FLUSH TABLES WITH READ LOCK and killing those queries 
    that block it. Default is 0 seconds, which means innobackupex will not 
    attempt to kill any queries. In order to use this option xtrabackup 
    user should have PROCESS and SUPER privileges.Where supported (Percona 
    Server 5.6+) xtrabackup will automatically use Backup Locks as a 
    lightweight alternative to FLUSH TABLES WITH READ LOCK to copy non-
    InnoDB data to avoid blocking DML queries that modify InnoDB tables.

參數的做用的就是在Xtrabackup執行FLUSH TABLES WITH READ LOCK之後,得到全局讀鎖時,若是有正在執行的事務會阻塞等待,kill-long-queries-timeout參數不爲0時,xtrabackup內部建立一個線程,鏈接到數據庫執行show full processlist,若是TIME超過kill-long-queries-timeout,會kill掉線程,kill-long-query-type設置能夠kill掉的SQL類型。

官方文檔介紹kill-long-query-type默認值時all,也就是全部語句都會kill掉。但在使用中發現,只設置kill-long-queries-timeout,未設置kill-long-query-type時,參數沒起做用!最後查閱xtrabackup代碼,以下:

{"kill-long-query-type", OPT_KILL_LONG_QUERY_TYPE,

			    "This option specifies which types of queries should be killed to "

			    "unblock the global lock. Default is \"all\".",

			    (uchar*) &opt_ibx_kill_long_query_type,

			    (uchar*) &opt_ibx_kill_long_query_type, &query_type_typelib,

			    GET_ENUM, REQUIRED_ARG, QUERY_TYPE_SELECT, 0, 0, 0, 0, 0}

心中一萬頭草泥馬,也許只是筆誤,但也太坑爹了!因此使用kill-long-query-type時必定要本身指定好類型!

5、總結

回顧此次執行備份的從庫複製卡死故障,根本緣由在於flush tables with read lock語句和事務執行的過程都涉及到連個鎖,並且不是原子的,再加上並行複製以及設置了從庫binlog的順序提交,最終致使三個線程造成死鎖。在尋找問題的解決方案中,意外發現了Xtrabackup kill-long-query-type的「祕密」,告誡咱們在使用中儘可能顯示指定參數,一方面更準確,另外一方面也便於查看。

 

另外,咱們知道set global read_only=1語句執行中涉及到的鎖和flush tables with read lock涉及的鎖時同樣的,也是兩個MDL鎖,因此理論上在並行複製的從庫執行set global read_only=1語句也可能會出現上述的兩個死鎖,有興趣的能夠驗證下

相關文章
相關標籤/搜索