參考自:https://dev.mysql.com/doc/refman/5.7/en/replication-gtids-lifecycle.html 筆記說明: 本文翻譯自官網,當然會根據語義做一些解釋或總結簡化,有些地方為了理解順暢也有刪減,有些地方直接翻為中文略顯生硬,如有疑問請直接參考上述 ...
參考自:https://dev.mysql.com/doc/refman/5.7/en/replication-gtids-lifecycle.html
筆記說明:
本文翻譯自官網,當然會根據語義做一些解釋或總結簡化,有些地方為了理解順暢也有刪減,有些地方直接翻為中文略顯生硬,如有疑問請直接參考上述鏈接中的原文。
本文主要介紹GTID的生成方式、基於GTID的主從同步時的工作機制,對於如何搭建GTID主從複製以及GTID主從複製為何可以實現並行複製的原理未做詳細介紹,後者原理可以參考innodb二階段日誌提交機制和組提交解析理解。
關於如何搭建基於GTID的主從複製以及在主從同步失敗時進行搶救,參考:How to create/restore a slave using GTID replication in MySQL 5.6
基於GTID的主從同步在出現問題時,除了手動的重新開啟下同步進程你能做的操作很少,相比之下原始的基於binlog pos的複製比較靈活,為了避免這種情況發生有必要探究一下基於GTID複製的工作機制,以便在主從同步異常時有效的進行修複。
本文的主要目的就是搞清GTID的生成和使用機制,搞清基於GTID複製的主要流程和核心參數,保證在GTID複製出現問題時可以通過靈活的處理相關參數來拯救主從複製。
一、GTID的生命周期如下:
1. 當事務於主庫執行時,系統會為事務分配一個由server uuid加序列號組成的GTID(當然讀事務或者被主動過濾掉的事務不會被分配GTID),寫binlog日誌時此GTID標志著一個事務的開始。GTID的格式如下所示:
GTID = source_id:transaction_id
# source_id為server的uuid
# transaction_id是一個表示事務執行順序的序列號,例如第一個執行的事務transaction_id為1,第10個為10
# GTID = <server uuid>:1-10表示從1-10的10個事務的集合,稱作gtid set
2. binlog中寫GTID的event被稱作Gtid_log_event,當binlog切換或者mysql服務關閉時,之前binlog中的所有gtid都會被加入mysql.gtid_executed表中。此表內容如下(slave中此表記錄數會有多條,取決於主從個數):
mysql> select * from mysql.gtid_executed;
+--------------------------------------+----------------+--------------+
| source_uuid | interval_start | interval_end |
+--------------------------------------+----------------+--------------+
| 71cf4b9d-8343-11e8-97f1-a0d3c1f25190 | 1 | 653948549 |
+--------------------------------------+----------------+--------------+
3. 當GTID被分配且事務被提交後,他會被迅速的以一種外部的、非原子性的方式加入@@GLOBAL.gtid_executed參數中,這個參數包含了所有被提交的GTID事務(其實他是一個GTID範圍值,例如71cf4b9d-8343-11e8-97f1-a0d3c1f25190:1-10),@@GLOBAL.gtid_executed也被用於主從複製,表示資料庫當前已經執行到了哪個事務。相比之下mysql.gtid_executed不能用於標識主庫當前事務進度,畢竟他只有在binlog切換時才會將日誌中的GTID加入(mysql服務關閉也相當於binlog切換)。
4. 在主從首次同步時(master_auto_position=1),slave會通過gtid協議將自己已經執行的gtid set(@@global.gtid_executed)發給master,master比較後從首個未被執行的GTID事務開始主從同步。
5. 當事務隨binlog被傳輸至slave後,slave每次讀到Gtid_log_event就把自己的gtid_next參數設為此GTID,需要註意的是這裡的gtid_next是在複製進程的session context中自動設置的(由binlog提供的語句),不同於show variables like 'gtid_next';這裡看到的結果預設為AUTOMATIC,是當前會話本身的gtid_next,這是個session級別的參數。
6. 當開啟並行複製時,slave會讀取並檢查事務的GTID確保當前GTID事務未被在slave執行過,且沒有並行進程在讀取並執行此事務,如果有並行複製進程正在應用此事務那麼slave server只會允許一個進程繼續執行,@@GLOBAL.gtid_owned參數展示了當前哪個並行複製進程在執行什麼事務。
7. 同樣的,在slave上如果開啟了binlog,GTID也會以Gtid_log_event事件寫入binlog,同時binlog切換或者mysql服務關閉時,當前binlog中的所有gtid都會被加入mysql.gtid_executed表中。
8. 在備庫上如果未開啟binlog,那麼GTID會被直接持久化到mysql.gtid_executed表中,在這種情況下slave的mysql.gtid_executed表包含了所有已經被執行的事務。需要註意的是在mysql5.7中,向mysql.gtid_executed表插入GTID的操作與DML操作是原子性的,對於DDL操作則不是,因此如果slave在執行DDL操作的過程中異常中斷那麼GTID機制可能會失效。在mysql8.0中這個問題已經得到解決,DDL操作的GTID插入也是原子性的。
9. 同第3條中所說的一樣,slave上的事務被執行後GTID也會被迅速的以一種外部的、非原子性的方式加入@@GLOBAL.gtid_executed參數中,在slave的binlog未開啟時mysql.gtid_executed中記載的已提交事務事實上與@@GLOBAL.gtid_executed記載的是一致的,如果slave的binlog已開啟那麼mysql.gtid_executed的GTID事務集就沒有@@GLOBAL.gtid_executed全了。
主從同步補充說明:slave會完全繼承master的GTID,因此如果slave的binlog開啟那麼即便事務在slave上什麼也沒做,還是會產生一個Gtid_log_event
,只不過之後會跟一個空事務,即begin;commit;。
這種slave空事務的可能產生場景是在master上手動設置了gtid_next並且什麼都沒做,這樣就會在binlog里產生一個空事務,雖然這個空事務什麼都沒做,slave依然要把他寫入自己的binlog中。
這樣做的好處是可以使mysql.gtid_executed和@@GLOBAL.gtid_executed記載的gtid set保持連貫。另一個好處是在主從同步中斷後重新開啟同步時可以防止再次同步那些過濾掉的GTID事務。
這可以印證一種slave跳過錯誤事務的方法,即stop slave;set gtid_next='要跳過的事務GTID';begin;commit;set gtid_next=AUTOMATIC;start slave;但是在跳過錯誤事務之前,
請使用show binlog events in 'log_name' from pos limit ...語句和mysqlbinlog工具確保你要跳過的事務不包含重要的數據更改。
並行複製的情境下,slave的GTID事務的提交順序可能與主庫不一樣,因為binlog的組提交機制允許同一組內的日誌記載的事務並行執行,其原理這裡不詳細描述,這會導致@@global.gtid_executed參數的值可能包含gtid gap,即@@global.gtid_executed中包含的事務序列號可能是不連貫的,如果使用stop slave來停止主從同步那麼複製進行會先把這些gap填上再停止,但如果主庫或從庫是異常關機的那麼這些gap可能會依然存在,這會導致你需要重新搭建主從複製,除非你自己確認這些gap事務是無影響可以跳過的。
二、一些GTID分配的其他情況:
GTID並非只會被分配給事務,一個事務也可能會被分配多個GTID。
首先解釋第一句:
除了正常的DML,DDL事務外,創建、修改、刪除一個database也會被分配一個GTID,此外procedure, function, trigger, event, view, user, role等對象的增刪改也會被分配一個GTID,此外grant操作也會被分配一個GTID。
另外對於類似myisam類型的表,雖然不涉及事務也還是會被分配GTID的,而且一旦此類不支持事務的存儲引擎的表的更改發生binlog落盤的錯誤時,binlog就會記載一次gap,對於這個binlog gap也會分配一個GTID給這個log event。
如之前所說的,master上rollback的事務不會被分配GTID,此外通過SET @@SESSION.sql_log_bin = 0;主動關閉會話binlog當然也不會為事務分配GTID了,畢竟連binlog都不會產生。
然後解釋第二句:
對於XA事務(分散式事務),一個事務會有多個GTID,而且就算其中一段事務被回滾也會被分配一個GTID。
此外在以下幾種情況下一條語句會產生多個事務,因此會被分配多個GTID:
- 一個存儲過程中包含多個事務。
- 使用一條drop table語句drop多個不同類型的表。
- CREATE TABLE ... SELECT語句,create table產生一個GTID,插入數據產生一個GTID。
三、系統參數gtid_next和
gtid_purged以及gtid_executed:
- 當gtid_next設為AUTOMATIC(預設)時,每個事務被提交時都會分配一個自增的GTID(這裡主要是說master),如果事務被回滾那麼GTID不會被分配。
- 如果將gtid_next設為一個合法的GTID值,那麼mysql server就會將此GTID設為你當前事務的GTID,即便你不作任何操作甚至設置sql_log_bin=0,此GTID也會被記錄入binlog。
需要註意的是如果你手動的將@@session.gtid_next設為一個GTID值,那麼在執行完事務後請務必重新將其設置為AUTOMATIC。
當slave的SQL thread進程應用事務時,他們會根據binlog日誌的記載將自己的@@SESSION.gtid_next設為即將要重放的事務的GTID,等到重放完畢後,還會把這個GTID加入@@global.gtid_executed。
總結下就是:此參數在事實上提供了手動跳過事務的方法,在主從同步需要跳過錯誤事務時很有用。
gtid_purged和gtid_executed:
此參數表示所有已經被提交但是在所有binlog中都找不到相關GTID的事務們,gtid_purged是gtid_executed的一個子集,其涉及到的場景主要是:
- slave上禁用了binlog,那麼所有重放的GTID事務都會被加入gtid_purged。
- 包含相應GTID事務的binlog已經被刪除,這些已提交事務會被加入gtid_purged。
通過SET @@GLOBAL.gtid_purged語句手動的將某些gtid加入gtid_purged的gtid set。
你可以通過修改@@GLOBAL.gtid_purged的值告訴slave:雖然已經無法在binlog中找到相關的GTID記錄了,但放心這些gtid set內的事務已經被應用過了,本人親自作保的!
此參數一個經典的應用場景是:你在搭建主從時使用mysqldump在slave server上恢復了備份,但是因為備份前未開啟GTID導致恢復後的資料庫並沒有gtid_executed和gtid_purged信息,因此指定gtid_mode=ON以及master_auto_position=1開啟GTID同步時slave嘗試同步master從uuid:1開始的所有GTID事務,這當然不是我們想要的也肯定會遇到錯誤。在mysql 5.7之後你可以通過只修改@@GLOBAL.gtid_purged的值來為slave同步的master_auto_position=1指明起始GTID(我認為官網這句話的意思是在5.7之前可能要連gtid_executed一併指定,具體未測試懶得做測試啦)。
gtid_executed和gtid_purged的值是在資料庫服務啟動時初始化的,每個binlog的初始event(其實是第2個啦,第一個是pos=4的Format_desc)都是Previous_gtids_log_event(通過SHOW BINLOG EVENTS [IN 'log_name'] [FROM pos] [LIMIT [offset,] row_count]查看),這個event包含了之前所有binlog files的GTID set(一般是uuid:1-<最新的事務序列號>),gtid_executed只需要看最新一個binlog的Previous_gtids_log_event的值即可,gtid_purged的值則是最新的binlog文件的Previous_gtids_log_event的值減去最老binlog文件的Previous_gtids_log_event的值。
gtid_executed的值會隨著事務的生成不斷更新,但不包含@@GLOBAL.gtid_owned的GTID,@@GLOBAL.gtid_owned表示當前資料庫正在執行的GTID事務。
在MySQL5.7.7版本之前,gtid_executed和gtid_purged的值可能會錯誤的生成,這姑且一個BUG,你可能需要將 binlog_gtid_simple_recovery 設為FALSE重新啟動DB伺服器來處理這個BUG,將此參數設為FALSE後,DB server在啟動時會遍歷所有binlog文件以便正確計算gtid_executed和gtid_purged的值,如果你有很多未開啟GTID模式時就存在的binlog,可能會導致重啟花費很長時間。
因此還是推薦在mysql5.7.8之後的版本上啟用GTID複製,以前的版本能用傳統複製就用傳統複製吧。
四、通過reset master重置GTID的自增序列號
如果你想要重置GTID的事務序列號,那麼需要執行下reset master,這會清除@@global.gtid_executed和@@global.gtid_purged的值,並且會清除以前的binlog和序列號,重新開啟一個類似於binlog.0001的binlog,如果未開啟binlog,那麼reset master至少也會清除掉@@global.gtid_executed和@@global.gtid_purged的值。
請謹慎的使用reset master以防止主從同步的事務丟失,為很好的把握此語句的使用情景需要非常瞭解他的作用和影響,以下為使用reset master時的一些註意事項:
在reset master之前,請確保你已經備份了當前資料庫的binlog文件和binlog index file,同時確保記下當前的@@global.gtid_executed和@@global.gtid_purged值。
reset master實際上做了以下操作:
- 將gtid_purged參數設為空字元
- 將gtid_executed設為空字元
- 清空mysql.gtid_executed表
- 如果DB server開啟了binlog,那麼reset master還會清除所有binlog文件和binlog index file,然後以初始的自增序列號1開啟一個新的binlog
此外要說明的是,無論是reset slave還是reset slave all都不會清除@@global.gtid_executed和@@global.gtid_purged的值。