一則 MySQL 從節點 hung 死問題分析

作者通過 MySQL 從節點的一個 hung 問題,對數據庫連接、日誌、innodb status 輸出等分析,再結合源碼、堆棧等最終明確爲由於 redo日誌配置不合理導致 hung 死問題根本原因。

作者:李錫超,一個愛笑的江蘇蘇商銀行 數據庫工程師,主要負責數據庫日常運維、自動化建設、DMP 平臺運維。擅長 MySQL、Python、Oracle,愛好騎行、研究技術。

愛可生開源社區出品,原創內容未經授權不得隨意使用,轉載請聯繫小編並註明來源。

本文約 3000 字,預計閱讀需要 10 分鐘。

近期,發現一個 MySQL 從節點提示同步異常。執行 show replica status 都被掛起。

重要信息

MySQL 版本 8.0.27
架構 主從模式
binlog_transaction_dependency_tracking WRITESET
replica_parallel_workers 16

初步分析

1.1 連接情況

當前連接的線程情況

當前連接中,運行的 16 個 worker 線程。其中:

  • 4 個狀態爲 Waiting for preceding transaction to commit
  • 11 個狀態爲 Applying batch of row changes
  • 1 個狀態爲 Executing event

線程等待時間 137272 秒(38 小時左右)。

同時看到執行的 show replica statusflush logs 命令都被掛起。

1.2 InnoDB Status 輸出

根據 innodb status 輸出結果:檢查 ROW OPERATIONSFILE I/O,未見幾個典型的問題,比如 innodb_thread_concurrency 配置不合理導致無法進入 InnoDB。

1.3 負載情況

檢查系統負載,線程模式查看所有線程的狀態。發現 MySQL 的 ib_log_checkpt 線程 CPU 使用率一直處於 100% 的狀態。其它線程都處於空閒狀態。

1.4 錯誤日誌

檢查錯誤日誌,未見相關的錯誤日誌記錄。

1.5 慢查詢

檢查慢查詢日誌,未見相關的慢查詢記錄。

1.6 分析小結

我們知道當前版本存在一些已知缺陷,根據主從線程的狀態,首先想到可能是由於多線程複製(MTS)的缺陷(Bug 103636 :MySQL 8.0 MTS 定時炸彈)導致。

但根據之前發現的案例,如果是該缺陷導致,在Bug觸發了這麼久,worker線程的狀態應該都是處於 Waiting for preceding transaction to commit 狀態,與此處現象不相符。

結合源碼進一步分析

結合源碼對 MTS 相關的邏輯進行梳理。關鍵邏輯如下:

// STEP-1: 啓動時,根據配置參數,分配 commit_order_mngr
|-handle_slave_sql (./sql/rpl_replica.cc:6812)
  |-Commit_order_manager *commit_order_mngr = nullptr
  |-if (opt_replica_preserve_commit_order && !rli->is_parallel_exec() && rli->opt_replica_parallel_workers > 1)
    |-commit_order_mngr = new Commit_order_manager(rli->opt_replica_parallel_workers)
  |-rli->set_commit_order_manager(commit_order_mngr) // 設置 commit_order_mngr
// STEP-2: 執行event時,分配提交請求的序列號
|-handle_slave_sql (./sql/rpl_replica.cc:7076)
  |-exec_relay_log_event (./sql/rpl_replica.cc:4905)
    |-apply_event_and_update_pos (./sql/rpl_replica.cc:4388)
      |-Log_event::apply_event (./sql/log_event.cc:3307)
        |-Log_event::get_slave_worker (./sql/log_event.cc:2770)
          |-Mts_submode_logical_clock::get_least_occupied_worker (./sql/rpl_mta_submode.cc:976)
            |-Commit_order_manager::register_trx (./sql/rpl_replica_commit_order_manager.cc:67)
              |-this->m_workers[worker->id].m_stage = cs::apply::Commit_order_queue::enum_worker_stage::REGISTERED
              |-this->m_workers.push(worker->id)
                // 此節點的 worker 正在處理的提交請求的序列號,其取值爲每次從 m_commit_sequence_generator(提交序列號計數器) 加 1 後所得
                // m_commit_sequence_generator 應該是全局的
                // 每次 start slave 之後會重新初始化
                |-this->m_workers[index].m_commit_sequence_nr->store(this->m_commit_sequence_generator->fetch_add(1))
                // 將worker id 加入到 m_commit_queue 的尾部
                |-this->m_commit_queue << index
|-...
|-handle_slave_worker (./sql/rpl_replica.cc:5891)
  |-slave_worker_exec_job_group (./sql/rpl_rli_pdb.cc:2549)
    |-Slave_worker::slave_worker_exec_event (./sql/rpl_rli_pdb.cc:1760)
      |-Xid_apply_log_event::do_apply_event_worker (./sql/log_event.cc:6179)
        |-Xid_log_event::do_commit (./sql/log_event.cc:6084)
          |-trans_commit (./sql/transaction.cc:246)
            |-ha_commit_trans (./sql/handler.cc:1765)
              |-MYSQL_BIN_LOG::commit (./sql/binlog.cc:8170)
                // STEP-3: 提交事務前,判斷worker隊列狀態和位置,使用MDL鎖,等待釋放事務鎖
                |-MYSQL_BIN_LOG::ordered_commit (./sql/binlog.cc:8749)
                  // 在該函數會控制,只有 slave 的worker 線程由於持有commit_order_manager, 纔會在此處進行 mngr->wait
                  |-Commit_order_manager::wait (./sql/rpl_replica_commit_order_manager.cc:375)
                    |-if (has_commit_order_manager(thd)): // 確認有 commit_order_manager 對象,該對象只要在開啓並行符合和從庫提交順序一致後,纔不爲nullptr
                      |-Commit_order_manager::wait (./sql/rpl_replica_commit_order_manager.cc:153)
                        |-Commit_order_manager::wait_on_graph (./sql/rpl_replica_commit_order_manager.cc:71)
                          |-this->m_workers[worker->id].m_stage = cs::apply::Commit_order_queue::enum_worker_stage::FINISHED_APPLYING
                          // Worker is first in the queue
                          |-if (this->m_workers.front() != worker->id) {}
                          // Worker is NOT first in the queue
                          |-if (this->m_workers.front() != worker->id):
                            |-this->m_workers[worker->id].m_stage = cs::apply::Commit_order_queue::enum_worker_stage::REQUESTED_GRANT
                            |-set_timespec(&abs_timeout, LONG_TIMEOUT);  // Wait for a year
                            // 等待 MDL 鎖
                            |-auto wait_status = worker_thd->mdl_context.m_wait.timed_wait(worker_thd, &abs_timeout,...)
                // STEP-4: 提交事務後,自增獲取下一個 worker、sequence_number,然後釋放對應的鎖。
                //         釋放了之後,其它worker就會在STEP-3中獲取到鎖,否則就會一直在STEP-3 等待
                |-MYSQL_BIN_LOG::ordered_commit (./sql/binlog.cc:8763)
                  |-MYSQL_BIN_LOG::change_stage (./sql/binlog.cc:8483)
                    |-Commit_stage_manager::enroll_for (./sql/rpl_commit_stage_manager.cc:209)
                      |-Commit_order_manager::finish_one (./sql/rpl_replica_commit_order_manager.cc:454)
                        |-Commit_order_manager::finish_one (./sql/rpl_replica_commit_order_manager.cc:300)
                          // 獲取 commit_queue 的第一個 worker 線程: next_worker
                          |-auto next_worker = this->m_workers.front()
                          // 確認 next_worker 線程的狀態,並比對 m_commit_sequence_nr。
                          // 如果滿足,則設置 mdl 鎖狀態爲 GRANTED,即喚醒 next_worker 執行 ordered_commit 的後續操作
                          |-if (this->m_workers[next_worker].m_stage in (FINISHED_APPLYING,REQUESTED_GRANT) and
                                /* 對比 next_worker 的 m_commit_sequence_nr 與 next_seq_nr:
                                   如果 m_commit_sequence_nr == next_seq_nr, 則 返回 true , 然後將 m_commit_sequence_nr = SEQUENCE_NR_FROZEN(1)
                                   如果 m_commit_sequence_nr != next_seq_nr, 則 返回 false
                                */
                          	    this->m_workers[next_worker].freeze_commit_sequence_nr(next_seq_nr))
                            // 如果 if 條件滿足, 設置 next_worker 爲授權狀態
                            |-this->m_workers[next_worker].m_mdl_context->m_wait.set_status(MDL_wait::GRANTED)
                            /* 對比 next_worker的 m_commit_sequence_nr 與 next_seq_nr
                               如果 m_commit_sequence_nr == 1, 則 返回 true , 然後將 m_commit_sequence_nr = next_seq_nr
                               如果 m_commit_sequence_nr != 1, 則 返回 false
                            */
                            |-this->m_workers[next_worker].unfreeze_commit_sequence_nr(next_seq_nr)
                          |-this->m_workers[this_worker].m_mdl_context->m_wait.reset_status()
                          // 標記當前狀態爲終態(FINISHED)
                          |-this->m_workers[this_worker].m_stage = cs::apply::Commit_order_queue::enum_worker_stage::FINISHED
                                             

爲便於下文說明,對於 MTS 的 16 個 worker 線程,分類如下:

  • A 類: 4 個 worker 線程的狀態爲 Waiting for preceding transaction to commit
  • B 類: 11 個 worker 線程的狀態爲 Applying batch of row changes
  • C 類: 1 個 worker 線程的狀態爲 Executing event

論點 1

假設是由於 Bug 103636 導致該問題,那麼 worker 線程應該會在執行提交操作的時候,在 ordered_commit 函數執行的開始位置,由於無法獲取 MDL 鎖而進行等待,通過 show processlist 查看 worker 線程的狀態應該爲 Waiting for preceding transaction to commit

而且通過 debug 驗證發現,一旦該問題被觸發,那麼所有的 worker(即使只有一個 worker 在執行事務),都會進入到 ordered_commit 函數,然後由於無法獲取 MDL 鎖,都會在相同的位置進行等待。即應該是所有 worker 線程處於 Waiting for preceding transaction to commit 狀態。

因此,初步判斷該問題現象和 bug 103636 不相符合。

論點 2

從因果關係來看:如果是由於 bug 103636 作爲問題的原因,結合論點 1 的分析,無法完全說明問題現象。

但相對的,可能是由於其它原因,導致以上 B 類和 C 類的 worker 線程無法順利執行事務,從而導致 A 類 worker 線程在提交的時候,由於具有較小的 commit_sequence 事務的 worker 線程還未提交執行操作,因此進行等待,導致其線程狀態爲 Waiting for preceding transaction to commit。如此更能合理的解釋當前數據庫的狀態。

那又是什麼原因,導致 B 類、C 類線程無法提交?根據採集信息,發現1個奇怪的症狀:

根據的 CPU 使用情況,問題時段的 ib_log_checkpt 一直是處於 100% 的 CPU 在運行。

根因分析

3.1 ib_log_checkpt 堆棧分析

根據 top 的線程 ID、堆棧以及 perf report 信息:

結合 log_checkpoint 的實現,我們看到該線程,主要是執行 dirty page flush 操作。具體包括:

buf_pool_get_oldest_modification_approx 實現分析,一直在循環遍歷,找 dirty page 的最小 LSN。然後根據該 LSN 進行異步 IO 刷 dirty page 操作。

由於 buf_pool_get_oldest_modification_approx 一直在跑,猜測可能是異步 IO 慢。導致檢查點無法完成,一直在尋找最小的 LSN。爲此,進一步分析系統 IO 壓力。

3.2 系統 IO 負載情況

根據以上 IO 負載情況,發現問題時段服務器 IO 壓力分析,與上述猜想不想符!那是因爲什麼原因導致該問題呢?難道是無法找到最小 LSN,或者尋找比較慢?

3.3 再次分析 InnoDB Status

根據以上分析結果,再次分析了相關採集數據。發現 innodb status 輸出包含如下信息:

---
LOG
---
Log sequence number          54848990703166
Log buffer assigned up to    54848990703166
Log buffer completed up to   54848990703166
Log written up to            54848990703166
Log flushed up to            54848990703166
Added dirty pages up to      54848990703166
Pages flushed up to          54846113541560
Last checkpoint at           54846113541560
5166640859 log i/o's done, 0.00 log i/o's/second
----------------------

發現當前 InnoDB 中的 Redo 日誌使用量=最新 lsn - checkpoint lsn=54848990703166-54846113541560=2,877,161,606=2.68GB。

而此時數據庫的 InnoDB Redo Log 配置爲:

  • innodb_log_file_size | 1073741824
  • innodb_log_files_in_group | 3

即總的日誌大小=1073741824*3=3GB

根據 MySQL 官方說明,需要確保 Redo 日誌使用量不超過 Redo 總大小的 75%,否則就會導致數據庫出現性能問題。但問題時刻我們的日誌使用率=2.68/3=89%,超過 Redo 日誌使用率的建議值 14%。

問題總結與建議

4.1 問題總結

綜合以上分析過程,導致此次故障的根本原因還是在於數據庫的 Redo 配置參數過小,在問題時段從節點的壓力下,Redo 的使用率過高,導致 InnoDB 無法完成檢查點。並進一步導致從節點的 worker 線程在執行事務時,檢查 Redo Log 是否存在有剩餘 Log 文件時,而發生等待。當前一個 worker 線程執行事務掛起後,由於從節點採用 MTS,且 slave_preserve_commit_order=on,因此其它 worker 線程需要等待之前的事務提交,最終導致所有 worker 線程掛起。

4.2 解決建議

  • 增加 Redo Log 文件的大小
  • 升級到 MySQL 8.0 的最新版本,解決 Bug 103636 等關鍵 bug
  • 增加 innodb_buffer_pool_size 內存大小

4.3 複雜問題針對數據採集建議

針對以上所有問題數據的採集,分享針對 MySQL 複雜問題的問題採集命令。具體如下:

來自:李錫超 -- <未知問題重啓前信息採集>

su - mysql
currdt=`date +%Y%m%d_%H%M%S`
echo "$currdt" > /tmp/currdt_tmp.txt
mkdir /tmp/diag_info_`hostname -i`_$currdt
cd /tmp/diag_info_`hostname -i`_$currdt

-- 1. mysql進程負載
su - 
cd /tmp/diag_info_`hostname -i`_`cat /tmp/currdt_tmp.txt`

ps -ef | grep -w mysqld
mpid=`pidof mysqld`
echo $mpid

-- b: 批量模式; n: 制定採集測試; d: 間隔時間; H: 線程模式; p: 指定進程號
echo $mpid
top -b -n 120 -d 1 -H -p $mpid > mysqld_top_`date +%Y%m%d_%H%M%S`.txt

-- 2. 信息採集步驟--- 以下窗口,建議啓動額外的窗口執行
mysql -uroot -h127.1 -p

tee var-1.txt
show global variables;

tee stat-1.txt
show global status;

tee proclist-1.txt
show full processlist\G
show full processlist;

tee slave_stat-1.txt
show slave status\G

tee threads-1.txt
select * from performance_schema.threads \G

tee innodb_trx-1.txt
select * from information_schema.innodb_trx \G

tee innodb_stat-1.txt
show engine innodb status\G

tee innodb_mutex-1.txt
SHOW ENGINE INNODB MUTEX;

-- 鎖與等待信息
tee data_locks-1.txt

-- mysql8.0
select * from performance_schema.data_locks\G
select * from performance_schema.data_lock_waits\G

-- mysql5.7
select * from information_schema.innodb_lock_waits \G
select * from information_schema.innodb_locks\G


-- 3. 堆棧信息
su - 
cd /tmp/diag_info_`hostname -i`_`cat /tmp/currdt_tmp.txt`

ps -ef | grep -w mysqld
mpid=`pidof mysqld`
echo $mpid

-- 堆棧信息
echo $mpid
pstack $mpid > mysqld_stack_`date +%Y%m%d_%H%M%S`.txt

-- 線程壓力
echo $mpid

perf top
echo $mpid
perf top -p $mpid

perf record -a -g -F 1000 -p $mpid -o pdata_1.dat
perf report -i pdata_1.dat

-- 4. 等待 30 秒
SELECT SLEEP(60);

-- 5. 信息採集步驟--- 以下窗口,建議啓動額外的窗口執行
mysql -uroot -h127.1 -p

tee var-2.txt
show global variables;

tee stat-2.txt
show global status;

tee proclist-2.txt
show full processlist\G
show full processlist;

tee slave_stat-2.txt
show slave status\G

tee threads-2.txt
select * from performance_schema.threads \G

tee innodb_trx-2.txt
select * from information_schema.innodb_trx \G

tee innodb_stat-2.txt
show engine innodb status\G

tee innodb_mutex-2.txt
SHOW ENGINE INNODB MUTEX;

-- 鎖與等待信息
tee data_locks-2.txt

-- mysql8.0
select * from performance_schema.data_locks\G
select * from performance_schema.data_lock_waits\G

-- mysql5.7
select * from information_schema.innodb_lock_waits \G
select * from information_schema.innodb_locks\G


-- 6. 堆棧信息
su - 
cd /tmp/diag_info_`hostname -i`_`cat /tmp/currdt_tmp.txt`

ps -ef | grep -w mysqld
mpid=`pidof mysqld`
echo $mpid

-- 堆棧信息
echo $mpid
pstack $mpid > mysqld_stack_`date +%Y%m%d_%H%M%S`.txt

-- 線程壓力
echo $mpid

perf top
echo $mpid
perf top -p $mpid

perf record -a -g -F 1000 -p $mpid -o pdata_2.dat
perf report -i pdata_2.dat

--- END --

以上信息僅供交流,作者水平有限,如有不足之處,歡迎在評論區交流。

更多技術文章,請訪問:https://opensource.actionsky.com/

關於 SQLE

SQLE 是一款全方位的 SQL 質量管理平臺,覆蓋開發至生產環境的 SQL 審覈和管理。支持主流的開源、商業、國產數據庫,爲開發和運維提供流程自動化能力,提升上線效率,提高數據質量。

SQLE 獲取

類型 地址
版本庫 https://github.com/actiontech/sqle
文檔 https://actiontech.github.io/sqle-docs/
發佈信息 https://github.com/actiontech/sqle/releases
數據審覈插件開發文檔 https://actiontech.github.io/sqle-docs/docs/dev-manual/plugins/howtouse
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章