這是 MySQL 5.6 全局事務 ID(GTID) 系列的第三篇部落格。
在之前的兩篇部落格中,
第一篇 介紹了全局事務 ID 的定義與資料結構。
第二篇 介紹了 MySQL 5.6 新增的全局事務狀态(Gtid_state)。
這裡準備介紹的是全局事務 ID 如何參與 MySQL 的主備複制流程。
MySQL 5.6 引入全局事務 ID 的首要目的,是保證 Slave 在複制的時候不會重複執行相同的事務操作;其次,是用全局事務 IDs 代替由檔案名和實體偏移量組成的複制位點,定位 Slave 需要複制的 binlog 内容。
是以,MySQL 必須在寫 binlog 時記錄每個事務的全局 GTID,保證 Master / Slave 可以根據這些 GTID 忽略或者執行相應的事務。在實作上,MySQL 沒有修改舊的 binlog 事件,而是新增了兩類事件:
+----------------------------+----------------------------------------+
| 名稱 | 功能 |
| Previous_gtids_log_event | 該事件之前的全局事務 ID 集合。 |
| Gtid_log_event | 标記之後的事務對應的全局事務 ID。 |
Gtid_log_event
在 MySQL 5.6 的 binlog 檔案中,每個事務的開始不是 "BEGIN" ,而是 Gtid_log_event 事件:
(圖檔來源:
MySQL_Innovation_Day_Replication_HA.pdf)
它裡面隻包含一條 GTID,記錄結構如下:
Gtid_log_event := (commit_flag, sid, gno) // commit_flag 目前總是 true
裡面 sid 就是産生該事務的 server_uuid,gno 是順序編号的 transaction_id。
把 Gtid 記錄在事務的開頭是為了便于 MySQL 過濾 binlog:檢查到某個 Gtid 不需要時,可以直接忽略後面的整段事務。
MySQL 5.6 保證同時寫入 Gtid_log_event 和全局 logged_gtids 狀态:
第一步,在向 binlog_cache_data 寫入第一條 binlog 前,MySQL 會在緩存的 buffer 中寫入一個空的 Gtid_log_event 占位。
第二步,當 binlog_cache_data 的内容刷到 binlog 檔案時,MySQL 會把位于緩存 buffer 的 Gtid_log_event 内容替換成實際的 GTID,重新寫入緩存。
最後,MySQL 調用 Gtid_state 的 update_on_flush() 把 GTID 寫入 logged_gtids,再調用 sync_binlog_file() 保證内容更新到磁盤。
在主備複制中,Slave 不像 Master 那樣自動産生 GTID,而是直接拷貝 Gtid_log_event 中包含的 GTID。這個特性是這樣實作的 —— MySQL 5.6 維護了一個線程(Session)級别的變量
gtid_next,類型為 Gtid_specification:
Gtid_specification := (enum_group_type, Gtid)
enum_group_type :=enum(AUTOMATIC_GROUP, GTID_GROUP, ANONYMOUS_GROUP, INVALID_GROUP, UNDEFINED_GROUP)
在 Master 執行事務時,gtid_next 的類型預設是 AUTOMATIC_GROUP,表示應該調用 generate_automatic_gno() 自動産生全局事務 ID。
而在 Slave 執行事務時,先用 Gtid_log_event 内的 Gtid 覆寫 gtid_next,使它的類型為 GTID_GROUP。這樣的話,MySQL 會使用 gtid_next 内設定的 Gtid 值作為下一個全局事務 ID。
Previous_gtids_log_event
這個事件出現在 MySQL 5.6 每個 binlog 檔案的開始處。
MySQL 建立一個新的 binlog 檔案後,首先寫入一個 Format_description_log_event 描述,接着寫入一個 Previous_gtids_log_event,内容是在建立這個 binlog 檔案之前執行的全局事務 GTIDs。
事件的格式很簡單,就是字元串編碼的 Gdit_set:(編碼格式參考本文
Previous_gtids_log_event := buffer of Gtid_set
(例如:3E11FA47-71CA-11E1-9E33-C80AA9429562:1-5)
這個事件隻是作為記錄。在主備複制時,Slave 會忽略 binlog 裡的 Previous_gtids_log_event 事件。
Binlog 與持久化全局事務狀态
在
上一篇 沒有講到 MySQL 5.6 如何持久化全局事務狀态的 —— logged_gtids 和 lost_gtids 狀态裡存儲了這台資料庫 有史以來 執行的所有 GTIDs(包括删除 binlog 中的 GTIDs)—— 如果資料庫停機或崩潰前不做持久化,之後肯定丢失資訊。
MySQL 的解決方案很簡單,在啟動時掃描剩餘的 binlog 檔案,用檔案存儲的 Previous_gtids_log_event 和 Gtid_log_event 事件内容恢複全局 logged_gtids 和 lost_gtids 狀态。
具體的代碼如下:(源代碼:mysql-5.6.9-rc\sql\binlog.cc,line 2558)
第一步,找到最後一個 binlog 檔案,讀出 Previous_gtids_log_event 記錄;再周遊 binlog 檔案中所有的 Gtid_log_event, 把找到的 GTID 記錄合并起來,作為這台資料庫曆史上執行的所有 GTIDs 放入全局 logged_gtids 記錄;
第二步,找到第一個 binlog 檔案,用它的 Previous_gtids_log_event 資訊代替全局 lost_gtids 的内容。因為這是第一個未删除的 binlog 檔案,這裡記錄的就是之前已經删除的 binlog 檔案所包含的全部 GTIDs。
由于 MySQL 在送出事務中是最後才寫入真實 Gtid_log_event 資訊的,從 binlog 恢複資訊,可以保證讀到的 GTIDs 與成功執行的事務一緻。
CHANGE MASTER TO ...
MySQL 5.6 主備複制的一個改變,是新增了 COM_BINLOG_DUMP_GTID 協定,支援在 Slave 切換到新 Master 時,用 MASTER_AUTO_POSITION = 1 (auto_position 方式)代替原來的 binlog 檔案名和實體偏移量。
COM_BINLOG_DUMP_GTID 協定并不複雜,請求格式如下:
Request = { server_id, binlog_name, binlog_offset, gtids_executed }
如果采用 auto_position 方式連接配接 Master,現在 Slave 發送的 binlog_name 和 binlog_offset 都是空白,Master 隻使用 gtids_executed 定位 Slave 上需要執行的 binlog。
實作邏輯是這樣的:Master 從第一個檔案開始讀取 binlog,逐個檢查 Gdit_log_event 事件的全局事務 ID 是不是包含在 Slave 發送的 gtids_executed 集合中。如果發現這個 GTID 已經包含在 gtids_executed 集合内,就忽略後面的整段事務,不向 Slave 發送 binlog 内容。
其實這個過程還不是很優化,因為如果是正常情況,Master 需要周遊若幹 G 的 binlog 才能找到 Slave 需要複制的 binlog 内容 —— 這應該是一個改進點。
全局事務 ID 與并發複制
MySQL 5.6 主備複制的另一個改變,是實作了多線程并行複制。這個功能必須有全局事務 ID 的支援,原因是:
1) 在并行複制方式下,有些操作是不按照記錄在 binlog 中的順序執行的。這樣的話,如果按照檔案名 + 實體偏移量的方式記錄複制位點,則停止 / 恢複主備複制時,可能會有一些操作被重複執行。
2) 我們知道,即使是 Mixed / Row 模式下記錄的 binlog,仍有些 DDL 操作是用 Statement 的方式編碼的,這些 DDL 操作不能在 Slave 重複執行(因為非幂等)。一旦操作在 Slave 執行出錯,結果就是複制中斷。
是以,Slave 必須依賴 binlog 中的全局事務 ID,在停止 / 恢複主備複制時,精确的記錄哪些事務在 Slave 執行過,哪些沒有。
現在,MySQL 5.6 可以用 COM_BINLOG_DUMP_GTID 來保證這一點:在恢複主備複制時,Slave 向 Master 發送自己所有執行過的 GTIDs(logged_gtids),在上次中斷主備複制時,已經執行過的 binlog 被 Master 直接濾掉,不向 Slave 傳送。
總結
在主備複制上,MySQL 5.6 新增了三個特性:
1)使用 GTIDs 作為主備複制的位點,在寫 binlog 時用 Gtid_log_event 标記事務。
2)支援 auto_position 方式進行主備切換。在新增的協定中,使用 GTIDs 作為複制位點向主庫請求 binlog 資訊。
3)多線程并發複制,使用 GTIDs 防止事務重複執行。
全局事務 ID(GTID)可以很好的支援這幾個功能。而且,使用 GTIDs 避免了在傳送 binlog 邏輯上依賴檔案名和實體偏移量,能夠更好的支援自動容災切換。
但是個人感覺,全局事務 ID 這裡還有些待解決的問題:
1)GTID 是局部有序的,不能記錄事務的全局順序。是以在雙寫 / 快速主備切換場景下,不能根據 GTID 順序來解決更新沖突的問題。
2)容災切換時,MASTER_AUTO_POSITION 隻能解決記錄位點的問題。為了保證一緻性,停寫和等待主備 Caught up 仍然是必須的,通常這是服務無法快速恢複的主要原因。
補充:參考資料
這篇部落格用到的參考資料:
MySQL 5.6 Manual:Replication with Global Transaction Identifiers(
linkWL#4677: Unique Server Ids for Replication Topology (UUIDs)(
WL#3584: Global Transaction Identifiers (GTIDs)(
順便提下,
MySQL Worklog 是個好地方,你可以從這裡了解 MySQL 的原始需求,開發人員的想法,還有值得關注的問題。