基於GTID搭建主從MySQL

賜我白日夢發表於2020-06-03

基於gtid搭建主從MySQL

一、GTID的使用

想讓主從之間使用gtid的方式同步資料,需要我們在配置檔案中開啟mysql對gtid相關的配置資訊

找到my.cnf ,在mysqld模組中加入如下的配置。(主庫從庫都這樣)

# on表示開啟,OFF表示關閉
gtid-mode = ON
# 下面的兩個變數必須開啟,否則MySQL拒絕啟動
log-slave-updates = 1
log-bin = MySQL-bin
# 必須開啟,否則MySQL不啟動,因為MySQL的SQL和gtid
# 許多MySQL的SQL和GTID是不相容的。比如開啟ROW 格式時,CREATE TABLE … SELECT
# 在binlog中會形成2個不同的事務,GTID無法唯一。
# 另外在事務中更新MyISAM表也是不允許的。
enforce_gtid_consistency = 1  #
log-bin-index = MySQL-bin.index

對主庫來說依然需要建立一個用於同步資料的賬號

mysql> grant replication slave on *.* to MySQLsync@"10.123.123.213" identified by "MySQLsync123";
Query OK, 0 rows affected, 1 warning (0.00 sec)

對從庫中執行如下命令:即可完成主從同步

CHANGE MASTER TO
    MASTER_HOST='10.123.123.123',
    MASTER_USER='MySQLsync',
    MASTER_PASSWORD='MySQLsync123',
    MASTER_PORT=8882,
    MASTER_AUTO_POSITION = 1;

這種自動找點對方式,相對於之前使用bin-log+position找點就顯得及其方便了。

省去了手動檢視master執行到那個binlog,以及binlog的position。

做了如上的配置後,還是可以繼續使用fileName和position找點,但是不推薦這樣做了,如果非要這樣做,設定MASTER-AUTO-POSITION=0


假設我們想在現有的主從叢集上新加一個庫從,如果這個主庫已經執行很久了,binlog肯定曾經被purge過,所以如果主庫中原來的資料不重要,不介意主從資料不一致,在從庫中執行:

reset master; 

# 缺失的GTID集合設定為purged ,執行這個命令請確保從庫:@@global.gtid_executed為空
set global gtid_purged = ‘主庫中曾經purge過的gtid記錄’

# 然後通過MASTER_AUTO_POSITION=1完成自動找點

如果介意主從資料強一致可以考慮使用可以熱備份的工具從主庫拷貝資料到從庫,完成資料的同步再在從庫執行如上set global gtid_purged = 'xxx'

熱備份工具如:xtrabackup , 它可以拷貝物理檔案和redolog來支援熱備份


二、GTID的簡介

GTID (global transcation identifier)

GTID是MySQL5.6版本中新增進來的新特性 ,通過GTID取代同步模式1中手動查詢fileName和position, 實現了自動找點

比如一條update有語句進入MySQL之後經歷如下過程:

1. 寫undolog 
2. 寫redolog(prepare)
3. 寫binlog 
4. 寫redolog(commit)

MySQL5.6之後加入了GTID新特性後,update語句經歷如下過程

1. 寫undolog # 回滾
2. 寫redolog(prepare)# 保證提交的不會丟失
3. 寫一個特殊的Binlog Event,型別為GTID_Event,指定下一個事務的GTID 
4. 寫binlog # 主從同步事物使用
5. 寫redolog(commit)

這個GTID的作用就是用於去唯一的標示一個事物的id。

主從之間,之所以能完成資料的同步,是因為從庫會dump主庫記錄的binlog, 主庫將自己成功執行過的事物都寫在binlog用於給從庫回放。當我們在mysql的配置檔案中將上面的配置都開啟時,主庫在記錄binlog的同時在binlog中會混雜著gtid的資訊,這個gtid和當前事物唯一對應。

當從庫向主庫傳送同步資料當請求時:bin-log和gtid都會傳送到slave端,slave在回放日誌同步資料時,同樣會使用gtid寫bin-log,這樣主庫和從庫之間的資料,就通過GTID強制性的關聯並且保持同步了。

下圖中淺色的背景是一條完整的binlog,從binlog的記錄中可以發現,gtid也寫在binlog中,當前事物提交commit後,還會為下一個事物生成一個gtid待使用。

三、GTID的構成

gtid由兩部分組成,server_uuid:transaction_id

  • server_uuid是一個只讀變數,儲存在 MySQL/var/auto.cnf, 也可以通過命令檢視show global variables like 'server_uuid' ;

    第一種檢視方式:

cat auto.cnf

第二種檢視方式:

  • transaction_id是事物id, 一般他會遞增。

四、檢視GTID的執行情況

在主庫中檢視gtid的執行情況:

如果不出意外的話,從庫中的gtid執行情況和主庫是一樣的。

這是如果我們往主庫插入一條資訊,主庫的gtid_executed則會變成:

f6b65f0b-a251-11ea-8921-b8599f2ef058:1-5

同樣去從庫中檢視,從庫的gtid資訊和主庫是一樣的。

4.1 gtid_executed

它既可以是一個global型別的變數,也可以是一個session級別的變數。是隻讀的,記錄著曾經執行過的gtid集合。比如:f6b65f0b-a251-11ea-8921-b8599f2ef058:1-5 表示曾經執行過1~5共五個事物。

global和session之間的區別:如果在global級別下,我們開啟一個會話然後設定值,關掉這個視窗,開啟一個新的視窗,依然能看在上一個被關閉的視窗設定的值。 如果在session級別下,開啟一個視窗A設定值,然後開啟一個新的視窗重新檢視是看不到在繪畫A中修改過的值的。

4.2 gtid_own

它既可以是一個global型別的變數,也可以是一個session級別的變數。是隻讀的,它記錄的是當前例項正在執行的gtid,以及對應的執行緒id。

4.3 gtid_purged

它是一個全域性的變數,purge就是丟棄的意思,而bin-log是實現主從之間的資料同步,主要是起到一箇中間者的作用,主從資料同步之後,binlog其實就可以被清除了,線上的日誌檔案被清理最勤的日誌檔案恐怕就binlog,可能每隔幾個小時或者1天清理一次。

這個gtid_purged所裝載的就是被丟棄的bin-log對應的gtid集合, gtid_purged是gtid_executed的子集,是不能被隨意更改的,只有在@@global.gtid_executed為空的情況下才能修改這個值。


認識了上面的幾個變數後:整理一下整個流程:

  • 開啟GTID模式後,我們指定MASTER_AUTO_POSITION=1。然後 start slave時,從庫會計算Retrieved_Gtid_SetExecuted_Gtid_Set的並集(通過show slave status可以檢視),然後把這個GTID並集傳送給主庫。主庫將從庫請求的GTID集合和自己的gtid_executed比較,把從庫GTID集合裡缺失的事務全都傳送給從庫。從庫再拿著這些gtid在自己本地回放事物,同步資料。

  • gtid是有冪等性的,從庫碰到原來使用過的gtid會直接跳過。

  • 如果從庫缺失的GTID已經被主庫pruge了,那麼從庫報1236錯誤,IO執行緒中斷。

此外,從庫的Retrieved_Gtid_SetExecuted_Gtid_Set在哪裡檢視呢?

通過show slave status 檢視

image-20200603191208054


再看這張圖:

這張圖是從庫的gtid相關資訊:

從張圖乍一看gtid的資訊是比較亂的:

server_uuid字尾為058結尾的是從庫同步的主庫資料時產生的。

server_uuid字尾以b38(從庫自己的server_uuid)結尾的記錄,其實是我直接在從庫insert資料產生的。

五、MySQL的冪等性

預設情況下MySQL冪等級別是:strict , 表示嚴格模式。 舉個例子,在這種情況下:假設id為主鍵,假設資料庫中已經存在了一條id=1 ,value = 1 的資料,我們重複的插入id=1 , value=2的新資料mysql會爆出 主鍵衝突的錯誤。

如果我們將冪等模式調整成:idemptent ,再往MySQL中插入一條id=1,value = 2的資料,此時不會爆出主鍵衝突,這條新資料會將原來value = 1 覆蓋成value=2。

開啟主從的模式下從庫的: slave_exec_mode 引數一般會被設定成:idemptent  , 這樣可以保證從庫中的資料和主庫中的資料保持一致。 並且不會發生這種問題:比如主庫中沒有id = 100的資料,故主庫中可以順利寫入id=100的資料, 從庫中有id=100的資料,但是因為開啟冪等模式,從庫不會爆出主鍵衝突的錯誤而中斷主從關係,而是使用主庫binlog中的新值去覆蓋當前存在的舊值。

六、擴充:

假設存在這樣一種場景:

有一對主從正常執行保持資料同步狀態。然後意外發生了:從庫在回放主庫binlog時有一個gtid對應的事物執行失敗了。具體一點,比如主庫現在 excuted_gtid = 1-20 , 然後從庫回放到第10條事物時還沒問題,但是回放到第11條事物時因為資料對不起來,執行失敗了,緊接著斷開了主從同步到關係。

如果是使用 binlog+position 實現的主從同步,我們可以設定 sql_slave_skip_counter 讓從庫跳過失敗的事物完成再恢復同步關係。

但是使用gtid構建的主從同步就不能skip事物了,它是自動找點的。

並且你通過show variables like '%gtid%' 可以看一下gtid的資訊, gtid_next是原子自增的。

mysql> show variables like '%gtid%' ;
+----------------------------------+------------------------------------------+
| Variable_name                    | Value                                    |
+----------------------------------+------------------------------------------+
| binlog_gtid_simple_recovery      | ON                                       |
| enforce_gtid_consistency         | ON                                       |
| gtid_executed_compression_period | 1000                                     |
| gtid_mode                        | ON                                       |
| gtid_next                        | AUTOMATIC                                |
| gtid_owned                       |                                          |
| gtid_purged                      | f6b65f0b-a251-11ea-8921-b8599f2ef058:1-9 |
| session_track_gtids              | OFF                                      |
+----------------------------------+------------------------------------------+
8 rows in set (0.01 sec)

檢視主庫的執行狀態:也能看到gtid的序號是連續的。


mysql> show master status;
+------------------+----------+--------------+------------------+------------------------------------------+
| File             | Position | Binlog_Do_DB | Binlog_Ignore_DB | Executed_Gtid_Set                        |
+------------------+----------+--------------+------------------+------------------------------------------+
| mysql-bin.000006 |      194 |              |                  | f6b65f0b-a251-11ea-8921-b8599f2ef058:1-9 |
+------------------+----------+--------------+------------------+------------------------------------------+

所以如果出現了上面的情景,可以像下面這樣做:

# 第gtid == 11 出問題了,就跳過它
set global sql_slave_skip_counter = 11;

# 執行begin commit 不會讓現有的gtid+1,僅僅是交一個空事物,佔據這個gtid=11的位置
BIGIN;COMMIT;

七、實驗:

小實驗1:

可以做一個小實驗:

先往主庫中寫入一條資料,然後重新整理日誌落盤

mysql> INSERT INTO `xxx`.`yyy` (`id` , `val`) VALUES (NULL , '123');
Query OK, 1 row affected (0.01 sec)

mysql> flush logs;
Query OK, 0 rows affected (0.00 sec)

mysql> show binary logs;
+------------------+-----------+
| Log_name         | File_size |
+------------------+-----------+
| mysql-bin.000005 |       506 |
| mysql-bin.000006 |       194 |
+------------------+-----------+
2 rows in set (0.00 sec)

退出mysql檢視所有的日誌:

然後我們手動執行 rm -rf mysql-bin.00000*

再登陸mysql檢視gtid_pured的變化情況:(你會發現,當我手動刪除主庫的binlog時,gtid_purged中沒有任何記錄)

mysql> show global variables like 'gtid%';
+----------------------------------+------------------------------------------+
| Variable_name                    | Value                                    |
+----------------------------------+------------------------------------------+
| gtid_executed                    | f6b65f0b-a251-11ea-8921-b8599f2ef058:1-7 |
| gtid_executed_compression_period | 1000                                     |
| gtid_mode                        | ON                                       |
| gtid_owned                       |                                          |
| gtid_purged                      |                                          |
+----------------------------------+------------------------------------------+
5 rows in set (0.00 sec)

當然上面手動rm -rf刪除日誌的方式是在開玩笑,真實的purged操作如下:

mysql> show binary logs;
+------------------+-----------+
| Log_name         | File_size |
+------------------+-----------+
| mysql-bin.000005 |       506 |
| mysql-bin.000006 |       194 |
+------------------+-----------+
2 rows in set (0.00 sec)

mysql> purge binary logs to 'mysql-bin.000006';
Query OK, 0 rows affected (0.00 sec)

mysql> show binary logs;
+------------------+-----------+
| Log_name         | File_size |
+------------------+-----------+
| mysql-bin.000006 |       194 |
+------------------+-----------+
1 row in set (0.00 sec)

mysql> show global variables like 'gtid%';
+----------------------------------+------------------------------------------+
| Variable_name                    | Value                                    |
+----------------------------------+------------------------------------------+
| gtid_executed                    | f6b65f0b-a251-11ea-8921-b8599f2ef058:1-9 |
| gtid_executed_compression_period | 1000                                     |
| gtid_mode                        | ON                                       |
| gtid_owned                       |                                          |
| gtid_purged                      | f6b65f0b-a251-11ea-8921-b8599f2ef058:1-9 |
+----------------------------------+------------------------------------------+
5 rows in set (0.00 sec)

命令中的purge binary logs to 'xxx' 是通過mysql的機制去刪除binlog。刪除的範圍是 mysql-bin00000x之前的所有binlog,而不包含 mysql-bin00000x。

小實驗2:

假設我現在有AB兩臺主從MySQL,兩臺都開啟gtid, 我先通過gtid 完成主從之間的同步關係,插入幾條資料後,主從的gtid值都為 1-10 。

然後我開始搞事情:斷開主從,先往主庫寫入一條資料(gtid=11)。

mysql> INSERT INTO `dktest00`.`testaaa00` (`id` , `val`)VALUES (NULL , '123');
Query OK, 1 row affected (10.00 sec)

mysql> show master status;
+------------------+----------+--------------+------------------+-------------------------------------------+
| File             | Position | Binlog_Do_DB | Binlog_Ignore_DB | Executed_Gtid_Set                         |
+------------------+----------+--------------+------------------+-------------------------------------------+
| mysql-bin.000006 |      732 |              |                  | f6b65f0b-a251-11ea-8921-b8599f2ef058:1-11 |
+------------------+----------+--------------+------------------+-------------------------------------------+

**再通過 binlog + position 完成兩者都同步關係。再往主庫中插入一條資料:

# set position = 732 
# binlog = f6b65f0b-a251-11ea-8921-b8599f2ef058:1-11

mysql> INSERT INTO `dktest00`.`testaaa00` (`id` ,`val`)VALUES (NULL , '123');
Query OK, 1 row affected (0.00 sec)

然後我去檢視從庫的gtid資訊:

mysql> show  global variables like 'gtid%';
+----------------------------------+----------------------------------------------+
| Variable_name                    | Value                                        |
+----------------------------------+----------------------------------------------+
| gtid_executed                    | f6b65f0b-a251-11ea-8921-b8599f2ef058:1-10:12 |
| gtid_executed_compression_period | 1000                                         |
| gtid_mode                        | ON                                           |
| gtid_owned                       |                                              |
| gtid_purged                      |                                              |
+----------------------------------+----------------------------------------------+
5 rows in set (0.00 sec)

我們會發現,雖然是使用position+binlog同步資料,但是隻要gtid開啟了,gtid就會記錄曾經執行的事物的資訊

那並且如果我們定位potion = 732 ,從庫回放的事物就不會包含 gtid = 11, 這一點我們可以通過檢視binlog作證

mysqlbinlog --no-defaults -vv /binlog的絕對路徑。

image-20200603200056676

這說明啥事呢? 比如現在主庫中有1,2,3條資料,然後你執行show master status; 看到這個position = 732

那這個732其實不是不包含第三條資料的。換句話說從庫從732同步資料,不會同步上第3條資料。

然後我們在此基礎上繼續搞事情:我們斷開主從,重新設定從庫position 為 732的上一個事物的位點(459),然後觀察從庫的 slave staus 情況。

image-20200603212632898

開始同步後檢視從庫中的資料,可以發現剛才跳過的gtid=11對應的資料已經被同步回來了。

相關文章