MySQL升級WRITE_SET後的一次死鎖分析

背景

MySQL在推出MGR的時候使用了WRITE_SET, 借用這個思想, MySQL在5.7.22版本引入了基於WRITE_SET的並行複製方案[1]。在原先的主從複製技術中,同一批次的事物能進入事物的prepare階段說明那批事物沒有衝突,因此能夠併發執行。咱們都知道innodb是基於行鎖的數據庫,因此若是可以按照行級別的粒度來併發的回放數據會對性能有很大的提升。採用這套方案的性能優勢就有不少方面了,其中一個能夠簡單看到的好處就是:咱們在回放的時候就不用依賴於主上事物提交的狀況了,正所謂less is more。減小了依賴,並行從宏觀上也能按照邏輯行這樣的來回放,因此性能確定有很大的提高[2]. 故而,咱們數據庫這邊在一些實例上啓用了這個並行回放特性。html

致使咱們死鎖的現象是: 咱們發現開啓了write_set並行回放的實例從庫上死鎖的機率比之前高了很多, 而且發生死鎖的實例都是在進行xtrabackup備份。本文主要分析這些數據庫實例上發生死鎖的緣由。mysql

場景

咱們知道MySQL事物會設計到不少的鎖,好比MDL鎖,innodb的行鎖,意向鎖,latch
鎖等等。不一樣的隔離級別鎖的行爲也有不少的差別。從死鎖理論的角度:死鎖就是有向圖中存在環,從而形成相互等待。要解決死鎖只要簡單的破壞任何一條邊,來打破環行等待。固然實際的實現可能會因各個環節點的權重不一樣而有所優化,選擇代價最小的。但以前的重點確定是找出這個「環」。而這些鎖有些是運維的時候能夠看到有些是看不到的。好比latch鎖通常對用戶看不到。由於性能緣由,咱們的MDL鎖和INNODB鎖的詳細信息並未收集。若是開啓了,就能夠經過performance_schema.metadata_lock這個表來查詢MDL鎖的相關信息,經過show engine innodb status來查看詳細innodb的加鎖信息。sql

經過簡單的分析,咱們鎖定是MDL死鎖。因此在這樣的場景下,咱們只能經過show full processlist來查看到當時的狀態,以下圖:數據庫

case1:bash


photo 1

圖1架構

case2:併發


photo 2-1

圖2-1app


photo 2-2

圖2-2

===框架

爲了方便你們理解, 我畫了一個示意圖[圖3]來解釋這兩個case的死鎖狀況:less


photo 3

圖3

case1 死鎖分析:

能夠看到在work線程組中,有一個work處理的事物先到達了事物的提交狀態, 可是事物在提交前須要進行 order_commit判斷,由於咱們設置了slave_preserve_commit_order
,要保證事物是按照主庫上的提交順序來提交的。因此這個時候必須等待以前的事物要提交才能夠進行。因此看到這個線程的狀態是: "Waiting for preceding transaction to commit"。當那個"靠前"的事物準備提交的時候要去拿mdl::commit_lock這把鎖,發現要不到。造成如上的「環等待」。

經過分析能夠知道,這個時候同時執行了 FTWRL (flush table with read lock), 而這個操做會獲取到MDL的一個共享鎖。可是一樣沒有版本獲取mdl::commit_lock 而等待。這個等待會形成新來的更新請求被阻塞,由於更新的語句是排他類型的鎖。因爲篇幅的緣由,不細說MDL鎖兼容細節。這裏只給出結論,會阻塞部分更新的語句,進而會影響到業務。

===


photo 4

圖4

case2 死鎖分析:

順便提一句: 一樣能夠看到,這種狀況下新的請求被阻塞主。注意,這也正是備份的核心思想。阻塞新來的請求,阻塞同批次的提交。保證在備份的時候沒有新的數據插入

一開始一個比較"靠後"的事物獲取了mdl::commit_lock,在準備提交的時候,發現系統配置了slave_preserve_commit_order,同時該事物的前面還有事物未提交,須要等待前面的事物先執行完成後才能繼續。而後FTWRL先獲取了mdl::global_read_lock鎖,可是沒有辦法獲取mdl::commit_lock鎖。

這個時候若是這個「前面的事物」是更新操做,那麼就跟mdl::global_read_lock鎖互斥,故而造成上面的死鎖。

驗證

因爲這樣的死鎖,是機率出現的。爲了高效的復現問題,咱們打算使用mysql的測試框架來驗證.
第一個步驟是:經過上面的分析,修改內核源碼加大死鎖的機率。證實咱們的猜測確實可以出現死鎖。可是這個出現的死鎖並不必定就是線上真是環境的死鎖。故而須要咱們把修改的源碼在實際場景下面驗證。固然咱們沒有辦法在生產環境來驗證。咱們能夠經過第一步修改的源碼,而後使用備份的數據來模擬。若是使用備份的數據 + 咱們修改的源碼數據庫實例復現了,才能客觀的判斷咱們的死鎖研判。固然讀者可能說咱們修改源碼破壞了以前的環境,這裏固然是有前提的。這個前提就是:只修改並行回放線程組中的某一個線程,不改變原有邏輯,只是單純的讓它支持慢一點來提升死鎖的機率,做證咱們的死鎖研判。

首先咱們的第一步就是要:在主庫上產生兩個事物(固然咱們也可使用蠻力,循環,不過可能效果差,甚至可能沒法復現),使用MySQL的測試框架,祥見以下的代碼:

57 # ===========================
58 # 在master上建立兩個連接master和master1
59 --source include/rpl_connection_master.inc
60 send SET DEBUG_SYNC='waiting_in_the_middle_of_flush_stage SIGNAL w WAIT_FOR b';
61
62 --source include/rpl_connection_master1.inc
63 send SET DEBUG_SYNC= 'now WAIT_FOR w';
64
65 --source include/rpl_connection_master.inc
66 --reap
67 show master status;
68 send insert into test.t1 values(1);
69
70 --source include/rpl_connection_master1.inc
71 --reap
72 SET DEBUG_SYNC= 'bgc_after_enrolling_for_flush_stage SIGNAL b';
73 insert into test.t1 values(1000);  

複製代碼

如何驗證咱們的主庫上這兩個事物屬於同一個批次呢?固然是binlog啦。結果以下:

show master status;
File    Position    Binlog_Do_DB    Binlog_Ignore_DB    Executed_Gtid_Set
master-bin.000001   849
#200107 9:26:14 server id 1 end_log_pos 219 CRC32 0x059fa77a Anonymous_GTID last_committed=0 sequence_number=1 rbr_only=no
#200107 9:26:24 server id 1 end_log_pos 408 CRC32 0xa1a6ea99 Anonymous_GTID last_committed=1 sequence_number=2
   rbr_only=yes
#200107 9:26:24 server id 1 end_log_pos 661 CRC32 0x2b0fc8a5 Anonymous_GTID last_committed=1 sequence_number=3 rbr_only=yes

複製代碼

能夠看到last_commit這個字段咱們一共產生了兩組binlog, 一個是0 這裏是create table 語句。另一個是1, 就是咱們上面的兩條insert 語句。

接下來就是就是要修改MySQL的源代碼了,這裏主要是要考慮到MTS的並行複製邏輯。由於咱們在主庫上經過DEBUG_SYNC讓大的事物先執行,因此好比是大的事物先分配到woker線程組中的第一個。因此咱們在binlog回放的關鍵路徑上: Xid_apply_log_event::do_apply_event_worker 這個函數中讓第一個worker sleep足夠多的時間讓咱們執行FTWRL。

直接修改源代碼編譯須要來回的編譯,咱們這邊使用systemstap 這個工具,JIT在運行時注入一段代碼來改變某些worker的行爲。在執行注入前先執行腳本驗證下可否注入:

41 --exec sudo stap -L 'process("$MYSQLD").function("pop_jobs_item")'
42 --exec sudo stap -L 'process("$MYSQLD").function("*Xid_apply_log_event::do_apply_event_worker")'
複製代碼

須要注意的是,由於stap的架構原理的緣由,詳細可參考下面的連接[3],須要root權限。下面是注入的代碼:

stap -v -g -d $MYSQLD --ldd -e 'probe process($server_pid).function("Xid_apply_log_event ::Xid_apply_log_event ") {printf("hit in do_apply_log_event\n") if ($w->id ==0) { mdelay(30000)} }'
stap -v -g -d $MYSQLD --ldd -e 'probe process($server_pid).function("pop_jobs_item") { printf("hit in pop_jobs_item") if ($worker->id == 0) { mdelay(3000)} }'

複製代碼

大體的意思就是: 讓複製線程組的第一個線程sleep 3s。這樣有足夠的時間來運行FTWRL。最終的執行結果:

show full processlist;
Id User    Host    db  Command Time    State   Info
3  root    localhost:10868 test    Sleep   83      NULL
4  root    localhost:10870 test    Sleep   84      NULL
7  root    localhost:10922 test    Query   61  Waiting for commit lock flush table with read lock
8  root    localhost:10926 test    Query   0   starting    show full processlist
9  system user     NULL    Connect 82  Waiting for master to send event    NULL
10 system user     NULL    Connect 61  Slave has read all relay log; waiting for more updates  NULL
11 system user     NULL    Connect 71  Waiting for global read lock    NULL
12 system user     NULL    Connect 71  Waiting for preceding transaction to commit NULL
13 system user     NULL    Connect 82  Waiting for an event from Coordinator   NULL
14 system user     NULL    Connect 81  Waiting for an event from Coordinator   NULL
複製代碼

能夠看到,咱們的猜測完整的復現了死鎖。大體解釋下:

咱們在構造這個死鎖的時候,由於咱們控制 的worker會sleep 3s。故而咱們能夠查詢worker的狀態,當worker處於 Waiting for preceding transaction to commit 這個狀態的時候,立馬執行FTWRL。而後能夠看到FTWRL會block在commit_lock。而後另一個更新天然是要等待: global read lock, 而造成死鎖。

總結

首先對於不太理解備份原理的同窗,應該能夠從這兩個死鎖等待圖中清楚的看到FTWRL的做用。它是經過兩把GLOBAL READ LOCK 和COMMIT_LOCK鎖來控制備份的一致性。這裏不詳細討論。
解決死鎖問題,經過死鎖理論,確定是要打破有向圖中的環。在咱們的這個死鎖case中經過分析能夠知道能夠操做的兩條邊只有:
1. slave_preserve_commit_order
2. FTWRL
顯然:對於那些能夠接受在從庫上事物的提交能夠「亂序」的,咱們只要關閉這個配置選項就能夠解除死鎖

而若是是要強制要求有序的,那麼咱們只能關閉備份的線程(圖中的節點,及相關的邊) 一樣能夠破解死鎖。在死鎖出現的時候,我的以爲關閉備份線程代碼是更小的。若是關閉worker線程的話,從庫複製會出錯誤。

參考

  1. dev.mysql.com/doc/relnote…
  2. mysqlhighavailability.com/improving-t…
  3. sourceware.org/systemtap/l…

做者:龍利劍

相關文章
相關標籤/搜索