詳解 binlog 時間戳與 exec_time 的關係。
作者:李錫超,蘇商銀行DBA,負責數據庫和中間件運維和建設。擅長 MySQL、Python、Oracle,愛好騎行、技術研究和分享。
愛可生開源社區出品,原創內容未經授權不得隨意使用,轉載請聯繫小編並註明來源。
本文約 2000 字,預計閱讀需要 8 分鐘。
概述
近期,某系統進行測試時,發現主從同步存在延遲,隨即通過 binlog 確認延遲原因。當使用 mysqlbinlog 命令解析後,發現其中的信息“似懂非懂”。
例如,對於如下 binlog 片段:
# at 449880
#240430 18:38:49 server id 345 end_log_pos 449967 CRC32 0xb3e8a02a GTID last_committed=13 sequence_number=14 rbr_only=yes original_committed_timestamp=1714473533138376 immediate_commit_timestamp=1714473539246294 transaction_length=446792
/*!50718 SET TRANSACTION ISOLATION LEVEL READ COMMITTED*//*!*/;
# original_commit_timestamp=1714473533138376 (2024-04-30 18:38:53.138376 CST)
# immediate_commit_timestamp=1714473539246294 (2024-04-30 18:38:59.246294 CST)
/*!80001 SET @@session.original_commit_timestamp=1714473533138376*//*!*/;
/*!80014 SET @@session.original_server_version=80027*//*!*/;
/*!80014 SET @@session.immediate_server_version=80027*//*!*/;
SET @@SESSION.GTID_NEXT= 'c0ac4587-6046-11ee-9fa7-001c42c92a7b:44'/*!*/;
# at 449967
#240430 18:38:16 server id 345 end_log_pos 450039 CRC32 0x0c7cb74e Query thread_id=16 exec_time=37 error_code=0
SET TIMESTAMP=1714473496/*!*/;
BEGIN
/*!*/;
/*!*/;
# at 450039
#240430 18:38:16 server id 345 end_log_pos 450098 CRC32 0xf9a84808 Table_map: `testdb`.`tb3` mapped to number 110
# at 450098
#240430 18:38:16 server id 345 end_log_pos 458309 CRC32 0xad84e9b0 Write_rows: table id 110
...
# at 896439
#240430 18:38:46 server id 345 end_log_pos 896498 CRC32 0x5cd7cd3b Table_map: `testdb`.`tb3` mapped to number 110
# at 896498
#240430 18:38:46 server id 345 end_log_pos 896540 CRC32 0x21b77031 Write_rows: table id 110 flags: STMT_END_F
...
### INSERT INTO `testdb`.`tb3`
### SET
### @1=131060 /* INT meta=0 nullable=0 is_null=0 */
### @2='c' /* VARSTRING(80) meta=80 nullable=1 is_null=0 */
# at 896540
#240430 18:38:49 server id 345 end_log_pos 896599 CRC32 0x6d6bf911 Table_map: `testdb`.`tb3` mapped to number 110
# at 896599
#240430 18:38:49 server id 345 end_log_pos 896641 CRC32 0xccd2fbb1 Write_rows: table id 110 flags: STMT_END_F
...
### INSERT INTO `testdb`.`tb3`
### SET
### @1=131061 /* INT meta=0 nullable=0 is_null=0 */
### @2='c' /* VARSTRING(80) meta=80 nullable=1 is_null=0 */
# at 896641
#240430 18:38:49 server id 345 end_log_pos 896672 CRC32 0xadb14b9d Xid = 85
通過以上 binlog 可知(P1):
#240430 18:38:16 執行 begin 開啓了事務 (爲便於表述,將時間字段名爲timestamp)
#240430 18:38:16 執行了 tb3的insert 操作
#240430 18:38:46 執行了 tb3的insert 操作
#240430 18:38:49 執行了 tb3的insert 操作
#240430 18:38:49 執行了commit操作
此外(P2):
original_commit_timestamp=2024-04-30 18:38:53
immediate_commit_timestamp=2024-04-30 18:38:59
exec_time=37
針對 P2 信息,提出如下問題:
- Q1:P2 中的字段分別表示什麼意思?是如何計算的?
- Q2:P2 的字段和 P1 看到的 timestamp 有什麼關係呢?
- Q4:P1 中的 timestamp 是如何取值的?特別是主從環境下
爲此,通過測試驗證,並結合源碼分析 binlog 中常見 Event 時間與 exec_time
的由來,並總結字段之間的關係。
以下分析基於 MySQL 8.0,不同版本字段可能不同。
主節點 binlog 日誌
1. GTID Event
timestamp
對於主節點:如沒有特殊說明,Event 的 timestamp
是在每個線程執行 dispatch_command()
初始位置獲取最新時間戳(thd->start_time
),並在生產 Event 對象時將 thd->start_time
賦值到 Log_event::common_header->when
。
主要堆棧信息如下:
|-handle_connection (./sql/conn_handler/connection_handler_per_thread.cc:302)
|-do_command (./sql/sql_parse.cc:1343)
|-dispatch_command (./sql/sql_parse.cc:1922)
// 設置 thd->start_time
|-thd->set_time()
|-my_micro_time_to_timeval(start_utime, &start_time)
|-dispatch_sql_command (./sql/sql_parse.cc:5135)
|-mysql_execute_command (./sql/sql_parse.cc:3518)
|-Sql_cmd_dml::execute (./sql/sql_select.cc:579)
……
|-Table_map_log_event the_event(this, table, table->s->table_map_id,is_transactional)
……
|-Rows_log_event *const ev = new RowsEventT(this, table, table->s->table_map_id, )
……
|-Xid_log_event end_evt(thd, xid)
immediate_commit_timestamp/original_commit_timestamp
immediate_commit_timestamp
獲取即爲提交時刻的時間戳,主節點 original_commit_timestamp
等於 immediate_commit_timestamp
。
|-error = trx_cache.flush(thd, &trx_bytes, wrote_xid)
|-Transaction_ctx *trn_ctx = thd->get_transaction()
|-trn_ctx->sequence_number = mysql_bin_log.m_dependency_tracker.step()
|-if (trn_ctx->last_committed == SEQ_UNINIT): trn_ctx->last_committed = trn_ctx->sequence_number - 1
|-if (!error): if ((error = mysql_bin_log.write_transaction(thd, this, &writer)))
|-int64 sequence_number, last_committed
|-m_dependency_tracker.get_dependency(thd, sequence_number, last_committed)
|-thd->get_transaction()->last_committed = SEQ_UNINIT
|-ulonglong immediate_commit_timestamp = my_micro_time()
//|-ulonglong original_commit_timestamp = thd->variables.original_commit_timestamp
|-ulonglong original_commit_timestamp = immediate_commit_timestamp
|-uint32_t trx_immediate_server_version = do_server_version_int(::server_version)
|-Gtid_log_event gtid_event(thd, cache_data->is_trx_cache(), last_committed, sequence_number,
cache_data->may_have_sbr_stmts(), original_commit_timestamp,
immediate_commit_timestamp, trx_original_server_version,
trx_immediate_server_version)
2. BEGIN Event
timestamp
注意:對於主節點 BEGIN event 的 timestamp
並不是執行 BEGIN 時的時間戳,而是執行第一個修改操作。在完成 InnoDB 層第一行數據修改之後,生成並寫入 Table_map event。在生成 Table_map event 之前,如果此時整個事務的 binlog 緩存是空的,纔會立即獲取該操作的 thd->start_time
,並生成真正的 BEGIN event。
exec_time
同時,對於主節點的 exec_time
就是在生成 BEGIN Event 的過程中,獲取最新的時間戳 - BEGIN Event 的 timestamp
而得。
exec_time = A - B
- A:執行第一個修改 SQL,完成第一行修改(write/update/delete)操作後,生成 BEGIN Event 的時間。
- B:第一個修改 SQL 的開始執行時間(thd->start_time)
內部堆棧與執行順序如下:
3. Table_map Event
4. Write Event
5. Xid Event
6. 主節點小結
- 除了 BEGIN Event 的
timestamp
是第一個需要寫入 binlog 操作(如:write/update/delete)的開始時間; - 其它 Event 的
timestamp
爲 SQL 語句執行時的開始時間; immediate_commit_timestamp/original_commit_timestamp
即爲提交時的時間戳;- exec_time = A - B
- A:執行第一個修改 SQL,完成第一行修改(write/update/delete)操作後,生成 BEGIN Event 的時間。
- B:第一個修改 SQL 的開始執行時間(thd->start_time)
從節點 binlog 日誌
1. GTID Event
timestamp
在從節點:對於 GTID Event,MySQL 在解析 Event 時,並不會獲取主節點 GTID/XID Event 的時間戳,因此會“繼承”該事務上一個操作的時間戳。而從節點所有修改操作的時間戳都來自於主節點執行操作時的時間戳。因此從節點的 GTID/XID Event 的時間即爲主節點最後一個修改操作的 timestamp。
immediate_commit_timestamp/original_commit_timestamp
immediate_commit_timestamp
獲取從節點提交時刻的時間戳。original_commit_timestamp
從 GTID Event 中的 original_commit_timestamp
獲取,即爲主節點提交操作的 timestamp
。
主要堆棧信息如下:
|-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)
|-MYSQL_BIN_LOG::ordered_commit (./sql/binlog.cc:8789)
|-MYSQL_BIN_LOG::process_flush_stage_queue (./sql/binlog.cc:8326)
|-MYSQL_BIN_LOG::flush_thread_caches (./sql/binlog.cc:8218)
|-binlog_cache_mngr::flush (./sql/binlog.cc:1099)
|-binlog_cache_data::flush (./sql/binlog.cc:2098)
|-MYSQL_BIN_LOG::write_transaction (./sql/binlog.cc:1586)
// 生成並寫入 GTID event
|-ulonglong immediate_commit_timestamp = my_micro_time()
|-if (original_commit_timestamp == UNDEFINED_COMMIT_TIMESTAMP){...}
|-Gtid_log_event gtid_event(thd, cache_data->is_trx_cache(), last_committed, sequence_number,
cache_data->may_have_sbr_stmts(), original_commit_timestamp, immediate_commit_timestamp, trx_original_server_version,
trx_immediate_server_version)
公式
immediate_commit_timestamp - original_commit_timestamp = A + B + C
- A = 主節點 傳輸 binlog 到 從節點 的耗時
- B = 從節點 重放 binlog 的耗時
- C = 同步延遲/中斷的耗時
2. BEGIN Event
timestamp
這裏的 timestamp
來自於主節點 BEGIN Event 的 timestamp
。其實際執行時,是會獲取 BEGIN Event 的 timestamp
將其賦值給 thd->start_time/thd->user_time
。從節點生成 Event 對象時,繼續從 thd->start_time
獲取時間戳即可。
exec_time
然後,從節點的 exec_time
依然是生成 BEGIN Event 的過程中,獲取 最新的時間戳 - timestamp
而得到(注意這裏的 timestamp
來自於主節點修改 SQL 的開始執行時間)。
主要堆棧信息如下:
|-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)
|-Log_event::do_apply_event_worker (./sql/log_event.cc:1083)
|-Query_log_event::do_apply_event (./sql/log_event.cc:4443)
|-Query_log_event::do_apply_event (./sql/log_event.cc:4606)
// 設置 user_time=start_time=ev.common_header->when
|-thd->set_time(&(common_header->when))
// query_arg="BEGIN"
|-thd->set_query(query_arg, q_len_arg)
...
公式
exec_time = A + B + C + D
- A = 主節點 整個事務的耗時
- B = binlog 傳輸耗時
- C = 同步延遲/中斷耗時(可能-主要)
- D = 從節點完成第一行數據修改
original_commit_timestamp - begin event 的 timestamp = 表示主節點整個事務的實際耗時(【主-第一個修改】 到【主- commit 開始】)。
3. Table_map Event
4. Write Event
5. Xid Event
6. 從節點小節
- 除了 GTID/XID Event,其它 Event 的時間戳均來自於主節點的 Event;
- GTID/XID Event 的
timestamp
爲主節點最後一個修改操作開始時間; - GTID Event 的
original_commit_timestamp
來自於主節點,immediate_commit_timestamp
爲最新的時間戳; - exec_time = A - B
- A = 從節點 生成 BEGIN Event 的最新時間戳
- B = 主節點 執行第一個 DML 操作的開始時間
結語
至此,關於 binlog 中的時間戳與 exec_time
已基本梳理完成,有興趣的朋友可以回到文章開頭,再看看 Q1-Q3 是否有了答案。
最後,建議讀者朋友實際模擬幾個案例,以便於更加深刻的理解相關字段,後續在利用 binlog 分析主從同步問題時,能更加得心應手。
以上信息僅供交流,作者水平有限,如有不足之處,歡迎在評論區交流。
更多技術文章,請訪問:https://opensource.actionsky.com/
關於 SQLE
SQLE 是一款全方位的 SQL 質量管理平臺,覆蓋開發至生產環境的 SQL 審覈和管理。支持主流的開源、商業、國產數據庫,爲開發和運維提供流程自動化能力,提升上線效率,提高數據質量。