原文連接:http://qing.blog.sina.com.cn/1757661907/68c3cad333002s5l.html
原文作者:淘長源
轉載注明以上信息
這是 MySQL 5.6 全局事務 ID(GTID) 系列的第三篇博客。
這里准備介紹的是全局事務 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 事件:
它里面只包含一條 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(
link)
WL#4677: Unique Server Ids for Replication Topology (UUIDs)(link)
WL#3584: Global Transaction Identifiers (GTIDs)(
link)
順便提下,
MySQL Worklog 是個好地方,你可以從這里了解 MySQL 的原始需求,開發人員的想法,還有值得關注的問題
