在 MySQL 5.6 主從架構下,從庫使用 Percona XtraBackup (PXB) 進行物理備份時,可能觸發備份執行緒、SQL 複制執行緒、使用者執行緒三方死鎖。本文基於實際 Case 深度剖析兩種死鎖場景的形成機制、根因與解決方案,並介紹 SystemTap 復現技術。
FLUSH TABLES WITH READ LOCK (FTWRL)SHOW SLAVE STATUS、SHOW MASTER STATUSUNLOCK TABLESFTWRL 會獲取兩個 MDL 鎖:全域讀鎖 (MDL_key::GLOBAL) 與 全域 COMMIT 鎖 (MDL_key::COMMIT)。MyISAM 表多時鎖持有時間長,故建議在從庫備份。
mysql> SHOW PROCESSLIST;
+----+-------------+-----------------+------+---------+------+----------------------------------------+-------------------+
| Id | User | Host | db | Command | Time | State | Info |
+----+-------------+-----------------+------+---------+------+----------------------------------------+-------------------+
| 1 | root | 127.0.0.1:53309 | NULL | Query | 278 | init | show slave status |
| 2 | system user | | NULL | Connect | 381 | Queueing master event to the relay log | NULL |
| 3 | system user | | NULL | Connect | 311 | Waiting for commit lock | NULL |
| 4 | root | 127.0.0.1:53312 | NULL | Query | 0 | init | show processlist |
+----+-------------+-----------------+------+---------+------+----------------------------------------+-------------------+
SHOW SLAVE STATUSWaiting for commit lockSHOW SLAVE STATUS 也被阻塞,甚至 KILL 無效# 備份執行緒
#0 __lll_lock_wait
#1 _L_lock_974
#2 __GI___pthread_mutex_lock
#3 inline_mysql_mutex_lock
#4 show_slave_status <-- 等待 rli->data_lock
#5 mysql_execute_command
# SQL 執行緒
#0 pthread_cond_timedwait
#1 inline_mysql_cond_timedwait
#2 MDL_wait::timed_wait
#3 MDL_context::acquire_lock <-- 等待 COMMIT 鎖
#4 ha_commit_trans
#5 trans_commit
#6 Xid_log_event::do_commit
(gdb) p (MDL_key::enum_mdl_namespace)lock->key->m_ptr[0]
$24 = MDL_key::COMMIT
(gdb) p ((THD*)lock->m_granted.m_list.m_first->m_ctx->m_owner)->thread_id
$25 = 1 <-- 備份執行緒持有 COMMIT 鎖
SHOW SLAVE STATUS 獲取位點,請求 rli->data_lock,被 SQL 執行緒阻塞MDL 系統只能檢測 MDL 鎖 (如 COMMIT 鎖) 的循環等待。rli->data_lock 是獨立 mutex,不屬於 MDL 體系,MDL 檢測器「看不見」它,故無法發現死鎖。
SHOW SLAVE STATUS 入口有 LOCK_active_mi mutex 保護,首個死鎖的查詢未退出,後續查詢阻塞於此 mutex,且無法響應 KILL (無法檢測 thd->killed)。
SQL 執行緒在 Xid_log_event::do_commit 前持有 rli->data_lock 是多餘的。MySQL 官方在 補丁 中修復:提交前釋放此鎖。影響版本:5.6.21 前。
mysql> SHOW PROCESSLIST;
+----+-------------+-----------------+------+---------+------+----------------------------------+-------------------+
| Id | User | Host | db | Command | Time | State | Info |
+----+-------------+-----------------+------+---------+------+----------------------------------+-------------------+
| 2 | system user | | NULL | Connect | 436 | Waiting for master to send event | NULL |
| 3 | system user | | NULL | Connect | 157 | Waiting for commit lock | NULL |
| 6 | root | 127.0.0.1:42787 | NULL | Query | 86 | init | show slave status |
| 7 | root | 127.0.0.1:42788 | NULL | Query | 96 | Killing slave | stop slave |
| 8 | root | 127.0.0.1:42789 | NULL | Query | 0 | init | show processlist |
+----+-------------+-----------------+------+---------+------+----------------------------------+-------------------+
STOP SLAVE,等待 SQL 執行緒退出 (持有 LOCK_active_mi)SHOW SLAVE STATUS 請求 LOCK_active_mi,被使用者執行緒阻塞此非代碼 Bug,屬運維操作不當:當 SQL 執行緒處於 Waiting for commit lock 時,嚴禁執行 STOP SLAVE。
因備份/使用者執行緒已無響應,唯有 KILL SQL 執行緒 (Id=3) 打破循環。
| MySQL 版本 | KILL SQL 執行緒風險 | 建議 |
|---|---|---|
| 5.6.21 之前 | 高:存在 Bug,KILL 時可能跳過事務導致主從資料不一致 | KILL 後必須對比主從資料,補齊缺失事務 |
| 5.6.21 及之後 | 低:事務可正常回滾重做 | 直接 KILL 安全 |
無需修改代碼重編譯,利用 SystemTap 動態探針延遲 Xid_log_event::do_commit:
# 列出可探測函數
sudo stap -L 'process("/usr/sbin/mysqld").function("*")'
sudo stap -L 'process("/usr/sbin/mysqld").function("*Xid_log_event::*")'
# 延遲 do_commit 3 秒 (PID 16011 為 mysqld 進程)
sudo stap -v -g -d /usr/bin/mysqld --ldd -e '
probe process(16011).function("Xid_log_event::do_commit") {
printf("got it\n")
mdelay(3000)
}'
執行後,每當 SQL 執行緒提交事務會暫停 3 秒,給予足夠時間執行 FTWRL 搶佔 COMMIT 鎖,穩定復現死鎖。
SHOW SLAVE STATUS 輪詢、禁止 STOP SLAVE