MySQL高可用之組複製技術(2):配置單主模型的組複製

weixin_34391854發表於2018-06-20

MySQL組複製系列文章:

  1. MySQL組複製大綱
  2. MySQL組複製(1):組複製技術簡介
  3. MySQL組複製(2):配置單主模型的組複製
  4. MySQL組複製(3):配置多主模型的組複製
  5. MySQL組複製(4):組複製理論透徹分析

MySQL的組複製可以配置為單主模型多主模型兩種工作模式,它們都能保證MySQL的高可用。以下是兩種工作模式的特性簡介:

  • 單主模型:從複製組中眾多個MySQL節點中自動選舉一個master節點,只有master節點可以寫,其他節點自動設定為read only。當master節點故障時,會自動選舉一個新的master節點,選舉成功後,它將設定為可寫,其他slave將指向這個新的master。
  • 多主模型:複製組中的任何一個節點都可以寫,因此沒有master和slave的概念,只要突然故障的節點數量不太多,這個多主模型就能繼續可用。

雖然多主模型的特性很誘人,但缺點是要配置和維護這種模式,必須要深入理解組複製的理論,更重要的是,多主模型限制較多,其一致性、安全性還需要多做測試。

而使用單主模型的組複製就簡單的太多了,唯一需要知道的就是它會自動選舉master節點這個特性,因為它的維護一切都是自動進行的,甚至對於管理人員來說,完全可以不用去了解組複製的理論。

雖然單主模型比多主模型的效能要差,但它沒有資料不一致的危險,加上限制少,配置簡單,基本上沒有額外的學習成本,所以多數情況下都是配置單主模型的組複製,即使是PXC和MariaDB也如此。

1.單主模型組複製的理論基礎

雖說組複製的單主模型很簡單,但有必要了解一點和單主模型有關的理論,儘管不瞭解也沒什麼問題,畢竟一切都是自動的。

如下圖,master節點為s1,其餘為slave節點。

733013-20180619164451814-1310834102.png

組複製一切正常時,所有的寫操作都路由到s1節點上,所有的讀操作都路由到s2、s3、s4或s5上。當s1節點故障後,組複製自動選舉新的master節點。假如選舉s2為新master成功後,s3、s4和s5將指向s2,寫操作將路由到s2節點上。

至於如何改變客戶端的路由目標,這不是組複製應該考慮的事情,而是客戶端應用程式應該考慮的事情。實際上,更好的方式是使用中介軟體來做資料庫的路由,比如MySQL Router、ProxySQL、amoeba、cobar、mycat。

1.1 如何加入新節點

上面一直說,單主模型是自動選舉主節點的,那麼如何選舉?

首先,在第一個MySQL節點s1啟動時,一般會將其設定為組的引導節點,所謂引導就是在啟動組複製功能時去建立一個複製組。當然,這並非強制要求,也可以設定第二個啟動節點作為組的引導節點。因為組內沒有其他節點,所以這第一個節點會直接選為master節點。

然後,如果有第二個節點要加入組時,新節點需要徵得組的同意,因為目前只有一個節點,所以只需s1節點同意即可。新節點在加入組時,首先會聯絡s1,與s1建立非同步複製的通道,並從s1節點處獲取s2上目前缺失的資料,等到s1和s2節點上的資料同步後,s2節點就會真正成為組中的新成員。當然,實際過程要比這裡複雜一些,本文不會過多討論。

如果還有新節點(比如s3節點)繼續加入組,s3將從s1或s2中選一個,並與之建立非同步複製的通道,然後獲取缺失的資料,同步結束後,如果s1和s2都同意s3加入,那麼s3將會組中的新成員。其餘節點加入組也依次類推。

有兩點需要注意:

  1. 新節點加入組時,如何選擇聯絡物件?

    上面說加入第二個節點s2時會聯絡s1,加入s3時會聯絡s1、s2中的任意一個。實際上,新節點加入組時聯絡的物件,稱為donor,意為資料供應者。新節點會和選中的donor建立非同步複製通道,並從donor處獲取缺失的資料。

    在配置組複製時,需要指定種子節點列表。當新節點加入組時,只會聯絡種子節點,也即是說,只有種子節點列表中的節點才有機會成為donor,沒有在種子節點列表中的節點不會被新節點選中。但建議,將組中所有節點都加入到種子列表中

    當聯絡第一個donor失敗後,會向後聯絡第二個donor,再失敗將聯絡第三個donor,如果所有種子節點都聯絡失敗,在等待一段時間後再次從頭開始聯絡第一個donor。依此類推,直到加組失敗報錯。

  2. 新節點加入組時,需要徵得哪些節點的同意?

    實際上,新節點加組涉及到組的決策:是否允許它加組。在組複製中,所有的決策都需要組中大多數節點達成一致,也即是達到法定票數。所謂大多數節點,指的是N/2+1(N是組中目前節點總數),例如目前組中有5個節點,則需要3個節點才能達到大多數的要求。

1.2 如何選舉新的master

當主節點s1故障後,組複製的故障探測機制就能發現這個問題並報告給組中其他成員,組中各成員根據收集到的其他成員資訊,會比較各成員的權重值(由變數group_replication_member_weigth控制),權重值最高的優先成為新的Master。如果有多個節點具有相同的最高權重值,會按字典順序比較它們的server_uuid值,最小的(升序排序,最小值在最前面)優先成為新的master。

但需要注意,變數group_replication_member_weigth是從MySQL 5.7.20開始提供的,在MySQL 5.7.17到5.7.19之間沒有該變數。此時將根據它們的server_uuid值進行排序選舉。具體的規則可自行測試。

1.3 最多允許多少個節點故障

MySQL組複製使用Paxos分散式演算法來提供節點間的分散式協調。正因如此,它要求組中大多數節點線上才能達到法定票數,從而對一個決策做出一致的決定。

大多數指的是N/2+1(N是組中目前節點總數),例如目前組中有5個節點,則需要3個節點才能達到大多數的要求。所以,允許出現故障的節點數量如下圖:

733013-20180619175159515-1966257287.png

1.4 單主模型組複製的要求

見:使用MySQL組複製的限制和侷限性

1.5 更多組複製的理論

若想了解更多組複製的理論以及組複製中每一個過程的細節,請參考我另一篇文章MySQL組複製理論透徹分析,或者閱讀我對MySQL官方手冊關於組複製的翻譯

2.配置單主模型

本文配置3個節點的單主模型組複製。配置很簡單,基本上就是在常規復制選項的基礎上多了幾個選項、多了幾步操作。

拓撲圖如下:

733013-20180619181103824-527250075.png

具體環境細節如下:

節點名稱 MySQL版本 客戶端介面(eth0) 組內通訊介面(eth0) 資料狀態
s1 MySQL 5.7.22 192.168.100.21 192.168.100.21 全新例項
s2 MySQL 5.7.22 192.168.100.22 192.168.100.22 全新例項
s3 MySQL 5.7.22 192.168.100.23 192.168.100.23 全新例項

發現了每個節點都給了兩個介面嗎?我這裡配置它們都使用同一個介面eth0。其中:

  1. 客戶端介面是mysqld向外提供資料庫服務的,對應埠是3306,例如php程式連線MySQL執行一個查詢語句時就使用該地址。
  2. 組內節點通訊介面用於組內各節點訊息傳遞,組內兩兩節點建立一條訊息傳遞的TCP連線。所以,3個節點需要建立的組內通訊連線為:s1<-->s2、s1<-->s3、s2<-->s3

請確保這3個節點的主機名不同,且能正確解析為客戶端介面的地址(別搞錯地址了),因為在連線donor進行資料恢復的時候,是通過主機名進行解析的。所以,所有節點都要先配置好不同的主機名,並修改/etc/hosts檔案。對於克隆出來的實驗主機,這一步驟很關鍵。以centos 7為例:

# s1上:
hostnamectl set-hostname --static s1.longshuai.com
hostnamectl -H root@192.168.100.22 set-hostname s2.longshuai.com
hostnamectl -H root@192.168.100.23 set-hostname s3.longshuai.com

# 寫/etc/hosts
# s1上:
cat >>/etc/hosts<<eof
    192.168.100.21 s1.longshuai.com
    192.168.100.22 s2.longshuai.com
    192.168.100.23 s3.longshuai.com
eof
scp /etc/hosts 192.168.100.22:/etc
scp /etc/hosts 192.168.100.23:/etc

2.1 配置組內第一個節點s1

1.先提供配置檔案。

[mysqld]
datadir=/data
socket=/data/mysql.sock

server-id=100                      # 必須
gtid_mode=on                       # 必須
enforce_gtid_consistency=on        # 必須
log-bin=/data/master-bin           # 必須
binlog_format=row                  # 必須
binlog_checksum=none               # 必須
master_info_repository=TABLE       # 必須
relay_log_info_repository=TABLE    # 必須
relay_log=/data/relay-log          # 必須,如果不給,將採用預設值
log_slave_updates=ON               # 必須
sync-binlog=1                      # 建議
log-error=/data/error.log
pid-file=/data/mysqld.pid

transaction_write_set_extraction=XXHASH64         # 必須
loose-group_replication_group_name="aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa"  # 必須
loose-group_replication_start_on_boot=off        # 建議設定為OFF
loose-group_replication_member_weigth = 40   # 非必需,mysql 5.7.20才開始支援該選項
loose-group_replication_local_address="192.168.100.21:20001"   # 必須,下一行也必須
loose-group_replication_group_seeds="192.168.100.21:20001,192.168.100.22:20002"

想要使用組複製,要求還是挺多的。分析一下上面的配置選項:

  • (1).因為組複製基於GTID,所以必須開啟gtid_modeenforce_gtid_consistency
  • (2).組複製必須開啟二進位制日誌,且必須設定為行格式的二進位制日誌,這樣才能從日誌記錄中收集資訊且保證資料一致性。所以設定log_binbinlog_format
  • (3).由於MySQL對複製事件校驗的設計缺陷,組複製不能對他們校驗,所以設定binlog_checksum=none
  • (4).組複製要將master和relay log的後設資料寫入到mysql.slave_master_infomysql.slave_relay_log_info中。
  • (5).組中的每個節點都保留了完整的資料副本,它是share-nothing的模式。所以所有節點上都必須開啟log_slave_updates,這樣新節點隨便選哪個作為donor都可以進行非同步複製。
  • (6).sync_binlog是為了保證每次事務提交都立刻將binlog刷盤,保證出現故障也不丟失日誌。
  • (7).最後的6行是組複製外掛的配置。以loose_開頭表示即使啟動組複製外掛,MySQL也繼續正常允許下去。這個字首是可選的。
  • (8).倒數第6行表示寫集合以XXHASH64的演算法進行hash。所謂寫集,是對事務中所修改的行進行的唯一標識,在後續檢測併發事務之間是否修改同一行衝突時使用。它基於主鍵生成,所以使用組複製,表中必須要有主鍵。
  • (9).倒數第5行表示這個複製組的名稱。它必須是一個有效的UUID值。嫌可以直接和上面一樣全寫字母a。在Linux下,可以使用uuidgen工具來生成UUID值。
[root@xuexi ~]# uuidgen
09c38ef2-7d81-463e-bdb4-9459b2c0e49b
  • (10).倒數第4行表示組複製功能不隨MySQL例項啟動而啟動。雖然,可以將組複製外掛和啟動組複製功能的選項寫在配置檔案裡,但強烈建議不要如此,而是每次手動去配置。
  • (11).倒數第3行表示該節點在組中的權重為40。權重越高,自動選舉為primary節點的優先順序就越高。
  • (12).倒數第2行表示本機上用於組內各節點之間通訊的地址和埠
  • (13).最後一行,設定本組的種子節點。種子節點的意義在前文已經解釋過了。

現在配置檔案已經提供。可以啟動mysql例項了。

[root@xuexi ~]# systemctl start mysqld

2.建立複製使用者。

連上s1節點。建立用於複製的使用者。我這裡建立的使用者為repl,密碼為P@ssword1!

mysql> create user repl@'192.168.100.%' identified by 'P@ssword1!';
mysql> grant replication slave on *.* to repl@'192.168.100.%';

3.配置節點加組時的通道。這是組複製的一個關鍵。

在新節點加入組時,首先要選擇donor。新節點和donor之間的非同步複製就是通過一個名為group_replication_recovery的通道(通道名固定,不可使用自定義通道)進行資料恢復的,經過資料恢復後,新節點填充了它缺失的那部分資料,這樣就和組內其他節點的資料保持了同步。

這個恢復過程比較複雜,它是一種分散式恢復。本文不介紹這個,在MySQL組複製理論透徹分析中,我對此做了詳細的說明。

執行change master to語句設定恢復通道。

mysql> change master to 
            master_user='repl',
            master_password='P@ssword1!'
            for channel 'group_replication_recovery';

這裡的使用者名稱、密碼和通道在組複製中有一個專門的術語:通道憑據(channel credentials)。通道憑據是連線donor的關鍵。

當執行完上面的語句後,就生成了一個該通道的relay log檔案(注意稱呼:該通道的relay log,後面還有另一個通道的relay log)。如下,其中字首"relay-log"是配置檔案中"relay_log"選項配置的值。

[root@xuexi ~]# ls -1 /data/*group*
/data/relay-log-group_replication_recovery.000001
/data/relay-log-group_replication_recovery.index

group_replication_recovery通道的relay log用於新節點加入組時,當新節點聯絡上donor後,會從donor處以非同步複製的方式將其binlog複製到這個通道的relay log中,新節點將從這個recovery通道的relay log中恢復資料。

前面配置檔案中已經指定了master和relay log的後設資料資訊要記錄到表中,所以這裡可以先檢視下關於relay log的後設資料。

mysql> select * from mysql.slave_relay_log_info\G
*************************** 1. row ***************************
  Number_of_lines: 7
   Relay_log_name: /data/relay-log-group_replication_recovery.000001
    Relay_log_pos: 4
  Master_log_name: 
   Master_log_pos: 0
        Sql_delay: 0
Number_of_workers: 0
               Id: 1
     Channel_name: group_replication_recovery

如果要檢視連線master的後設資料資訊,則查詢mysql.slave_master_info表。不過現在沒必要查,因為啥都還沒做呢。

4.安裝組複製外掛,並啟動組複製功能。

一切就緒後,可以開啟mysql例項的組複製功能了。

mysql> install plugin group_replication soname 'group_replication.so';

然後開啟組複製功能。

mysql> set @@global.group_replication_bootstrap_group=on;
mysql> start group_replication;
mysql> set @@global.group_replication_bootstrap_group=off;

這裡的過程很重要,需要引起注意。在開啟組複製之前,設定全域性變數group_replication_bootstrap_group為on,這表示稍後啟動的組複製功能將引導組,也就是建立組並配置組,這些都是自動的。配置引導變數為ON後,再開啟組複製外掛功能,也就是啟動組複製。最後將引導變數設回OFF,之所以要設定回OFF,是為了避免下次重啟組複製外掛功能時再次引導建立一個組,這樣會存在兩個名稱相同實際卻不相同的組。

這幾個過程不適合放進配置檔案中,強烈建議手動執行它們的。否則下次重啟mysql例項時,會自動重新引導建立一個組。同理,除了第一個節點,其他節點啟動組複製功能時,不應該引導組,所以只需執行其中的start語句,千萬不能開啟group_replication_bootstrap_group變數引導組。

這裡的幾個過程,應該形成一個習慣,在啟動第一個節點時,這3條語句同時執行,在啟動其他節點時,只執行start語句。

當啟動組複製功能後,將生成另一個通道group_replication_applier的相關檔案。

[root@xuexi ~]# ls -1 /data/*group*
/data/relay-log-group_replication_applier.000001
/data/relay-log-group_replication_applier.000002
/data/relay-log-group_replication_applier.index
/data/relay-log-group_replication_recovery.000001
/data/relay-log-group_replication_recovery.index

是否還記得剛才用於恢復的通道group_replication_recovery?這個applier通道是幹什麼的?常規復制有兩個複製執行緒:io執行緒和sql執行緒,在組複製中,不再稱之為io_thread和sql_thread,取而代之的是receiver、certifier和applier。這裡簡單介紹一下它們的作用:

  • receiver的作用類似於io執行緒,用於接收組內個節點之間傳播的訊息和事務。也用於接收外界新發起的事務。
  • applier的作用類似於sql執行緒,用於應用relay log中的記錄。不過,組複製的relay log不再是relay log,而是這裡的組複製relay log:relay-log-group_replication_applier.00000N
  • certifier的作用在receiver接收到訊息後,驗證是否有併發事務存在衝突問題。衝突檢測通過後,這條訊息就會寫入到組複製的relay log中,等待applier去應用。

並不是說組複製中沒有io執行緒和sql執行緒,而是稱呼改變了,receiver和applier實際上就是io_therad和sql_thread。

5.驗證組中節點並測試插入不滿足組複製要求的資料。

至此,這個節點的組複製已經配置完成了。現在需要檢視這個節點是否成功加入到組中,成功加入組的標誌是被設定為"ONLINE"。只要沒有設定為ONLINE,就表示組中的這個節點是故障的。

檢視的方式是通過查詢performance_schema架構下的replication_group_members表。在這個架構下,有幾張對於維護組複製來說非常重要的表,這裡的replication_group_members是其中一張。關於其他的表,我會在有需要的地方或者其他文章中解釋。

mysql> select * from performance_schema.replication_group_members;
+---------------------------+--------------------------------------+---------------------+-------------+--------------+
| CHANNEL_NAME              | MEMBER_ID                            | MEMBER_HOST         | MEMBER_PORT | MEMBER_STATE |
+---------------------------+--------------------------------------+---------------------+-------------+--------------+
| group_replication_applier | a659234f-6aea-11e8-a361-000c29ed4cf4 | xuexi.longshuai.com |        3306 | ONLINE       |
+---------------------------+--------------------------------------+---------------------+-------------+--------------+

如果不方便觀看,換一種顯示方式:

mysql> select * from performance_schema.replication_group_members\G
*************************** 1. row ***************************
CHANNEL_NAME: group_replication_applier
   MEMBER_ID: a659234f-6aea-11e8-a361-000c29ed4cf4
 MEMBER_HOST: xuexi.longshuai.com
 MEMBER_PORT: 3306
MEMBER_STATE: ONLINE

請注意這裡的每一行,包括member_host,它是對外連線的地址,所以應該設定它的DNS解析為提供MySQL資料庫服務的介面地址。這很重要,如果你不想去修改DNS解析,可以在啟動組複製之前,設定report_host變數為對外的IP地址,或者將其寫入到配置檔案中。

現在,組中的這個節點已經是ONLINE了,表示可以對外提供組複製服務了。

稍後,將向組中加入第二個節點s2和第三個節點s3,但在加入新節點之前,先向s1節點寫入一些資料,順便測試一下開啟組複製後,必須使用InnoDB、表中必須有主鍵的限制。

下面建立4個表:t1和t4是InnoDB表,t3和t4具有主鍵。

create table t1(id int);
create table t2(id int)engine=myisam;
create table t3(id int primary key)engine=myisam;
create table t4(id int primary key);

雖說組複製對這些有限制,但是建立時是不會報錯的。

向這4張表中插入資料:

insert into t1 values(1);
insert into t2 values(1);
insert into t3 values(1);
insert into t4 values(1);

會發現只有t4能插入成功,t1、t2、t3都插入失敗,報錯資訊如下:

ERROR 3098 (HY000): The table does not comply with the requirements by an external plugin.

意思是該表不遵從外部外掛(即組複製外掛)的要求。

最後,檢視下二進位制日誌中的事件。為了排版,我將顯示結果中的日誌名稱列去掉了。

mysql> SHOW BINLOG EVENTS in 'master-bin.000004';
+------+----------------+-----------+-------------+-------------------------------------------------------------------+
| Pos  | Event_type     | Server_id | End_log_pos | Info                                                              |
+------+----------------+-----------+-------------+-------------------------------------------------------------------+
|    4 | Format_desc    |       100 |         123 | Server ver: 5.7.22-log, Binlog ver: 4                             |
|  123 | Previous_gtids |       100 |         150 |                                                                   |
|  150 | Gtid           |       100 |         211 | SET @@SESSION.GTID_NEXT= 'a659234f-6aea-11e8-a361-000c29ed4cf4:1' |
|  211 | Query          |       100 |         399 | CREATE USER 'repl'@'192.168.100.%' IDENTIFIED WITH 'password'     |
|  399 | Gtid           |       100 |         460 | SET @@SESSION.GTID_NEXT= 'a659234f-6aea-11e8-a361-000c29ed4cf4:2' |
|  460 | Query          |       100 |         599 | GRANT REPLICATION SLAVE ON *.* TO 'repl'@'192.168.100.%'          |
|  599 | Gtid           |       100 |         660 | SET @@SESSION.GTID_NEXT= 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa:1' |
|  660 | Query          |       100 |         719 | BEGIN                                                             |
|  719 | View_change    |       100 |         858 | view_id=15294216022242634:1                                       |
|  858 | Query          |       100 |         923 | COMMIT                                                            |
|  923 | Gtid           |       100 |         984 | SET @@SESSION.GTID_NEXT= 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa:2' |
|  984 | Query          |       100 |        1083 | create database gr_test                                           |
| 1083 | Gtid           |       100 |        1144 | SET @@SESSION.GTID_NEXT= 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa:3' |
| 1144 | Query          |       100 |        1243 | use `gr_test`; create table t1(id int)                            |
| 1243 | Gtid           |       100 |        1304 | SET @@SESSION.GTID_NEXT= 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa:4' |
| 1304 | Query          |       100 |        1416 | use `gr_test`; create table t2(id int)engine=myisam               |
| 1416 | Gtid           |       100 |        1477 | SET @@SESSION.GTID_NEXT= 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa:5' |
| 1477 | Query          |       100 |        1601 | use `gr_test`; create table t3(id int primary key)engine=myisam   |
| 1601 | Gtid           |       100 |        1662 | SET @@SESSION.GTID_NEXT= 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa:6' |
| 1662 | Query          |       100 |        1773 | use `gr_test`; create table t4(id int primary key)                |
| 1773 | Gtid           |       100 |        1834 | SET @@SESSION.GTID_NEXT= 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa:7' |
| 1834 | Query          |       100 |        1905 | BEGIN                                                             |
| 1905 | Table_map      |       100 |        1949 | table_id: 117 (gr_test.t4)                                        |
| 1949 | Write_rows     |       100 |        1985 | table_id: 117 flags: STMT_END_F                                   |
| 1985 | Xid            |       100 |        2012 | COMMIT /* xid=63 */                                               |
+------+----------------+-----------+-------------+-------------------------------------------------------------------+

除了正常的事務對應的事件,需要關注的三行是:

BEGIN
View_change <----> view_id=15294216022242634:1
COMMIT

組複製中,每個決策都需要組中大多數節點達成一致,包括新節點加組、離組的決定。實際上,組複製中內建了一個組成員服務,這個服務負責組成員的配置以及動態捕獲組中成員列表,這個成員列表成為成員檢視。每個檢視都有一個view id,view id的第一部分是建立組時隨機生成的,只要組不停止,這部分就不改變,第二部分是從1開始的單調遞增的數值。每當有成員加組、離組時,都會觸發這個服務對組成員進行重新配置,每次組成員的重新配置,view id的第二部分都會單調遞增地加1,表示這是新的成員檢視,新的組成員檢視需要得到組中大多數節點的同意,所以這個訊息要在組中進行傳播。如何傳播?就是通過將檢視更改的事件作為一個事務寫進binlog中,然後在組中到處複製,這樣每個節點都可收到檢視變化的訊息,並對此做出回應,同意之後再commit這個事務。如果足夠細心,會發現這個事務的提交和下面插入資料的提交(COMMIT /* xid=63 */)方式不一樣。如果不理解也沒關係,這個理論並不影響組複製的使用。

再仔細一看,還可以發現MySQL中的DDL語句是沒有事務的。所以,絕不允許不同節點上對同一個物件併發執行"DDL+DML"和"DDL+DDL",衝突檢測機制會探測到這樣的衝突。

2.2 向組中新增新節點

當組中已有第一個節點後,需要做的是向組中新增新的節點。這裡以新增s2和s3為例。

2.2.1 新增新節點前要做什麼

前面多次提到,新節點在加入組的時候,會先選擇一個donor,並通過非同步複製的方式從這個donor處獲取缺失的資料,以便在成功加入組的時候它的資料和組中已有的節點是完全同步的,這樣才能向外界客戶端提供查詢。

這裡的重點在於非同步複製,既然是複製,它就需要複製binlog,並通過應用binlog中的記錄來寫資料。如果在加入組之前,組中的資料量已經非常大,那麼這個非同步複製的過程會很慢,而且還會影響donor的效能,畢竟它要傳輸大量資料出去。

本來加入新節點的目的就是對組複製進行擴充套件,提高它的均衡能力,現在因為非同步複製慢,反而導致效能稍有下降,新節點短期內還無法上線向外提供服務。這有點背離原本的目標。

再者,如果組中的節點purge過日誌,那麼新節點將無法從donor上獲取完整的資料。這時新節點上的恢復過程會讓它重新選擇下一個donor。但很可能還是會失敗,因為實際環境中,既然purge了某節點上的一段日誌,很可能同時會去所有節點上也Purge。(注意,purge不是事件,不會寫入到binlog中,所以不會複製到其它節點上,換句話說,某節點Purge後,那麼它的binlog和其它節點的binlog是不一致的)。

所以,在新節點加入組之前,應該先通過備份恢復的方式,從組中某節點上備份目前的資料到新節點上,然後再讓新節點去加組,這樣加組的過程將非常快,且能保證不會因為purge的原因而加組失敗。至於如何備份恢復,參見我的另一篇文章:將slave恢復到master指定的座標

我這裡做實驗的環境,所有節點都是剛安裝好的全新例項,資料量小,也沒purge過日誌,所以直接加入到組中就可以。

2.2.2 新增第二個節點

仍然先是提供配置檔案。配置檔案和第一個節點基本相同,除了幾個需要保持唯一性的選項。

配置檔案內容如下:

[mysqld]
datadir=/data
socket=/data/mysql.sock

server-id=110                      # 必須,每個節點都不能相同
gtid_mode=on                       # 必須
enforce_gtid_consistency=on        # 必須
log-bin=/data/master-bin           # 必須
binlog_format=row                  # 必須
binlog_checksum=none               # 必須
master_info_repository=TABLE       # 必須
relay_log_info_repository=TABLE    # 必須
relay_log=/data/relay-log          # 必須,如果不給,將採用預設值
log_slave_updates=ON               # 必須
sync-binlog=1                      # 建議
log-error=/data/error.log
pid-file=/data/mysqld.pid

transaction_write_set_extraction=XXHASH64         # 必須
loose-group_replication_group_name="aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa"  # 必須
loose-group_replication_start_on_boot=off        # 建議設定為OFF
loose-group_replication_member_weigth = 20   # 非必需,mysql 5.7.20才開始支援該選項
loose-group_replication_local_address="192.168.100.22:20002"   # 必須,下一行也必須
loose-group_replication_group_seeds="192.168.100.21:20001,192.168.100.22:20002"

這裡和s1的配置檔案相比,只修改了server-idgroup_replication_local_address以及權重值。

然後執行change master to,選擇一個donor(此刻只有s1能選),並和donor建立通道連線。

mysql> change master to 
            master_user='repl',
            master_password='P@ssword1!'
            for channel 'group_replication_recovery';

這時,就已經選擇好donor,並和donor建立通道連線了。如果去s1上檢視,可以看到這個通道的連線。下面的查詢結果中,第二行就是和s2建立的連線,通道為group_replication_recovery

mysql> select * from mysql.slave_master_info\G
*************************** 1. row ***************************
       Number_of_lines: 25
       Master_log_name: 
        Master_log_pos: 4
                  Host: <NULL>
             User_name: 
         User_password: 
                  Port: 0
         Connect_retry: 60
           Enabled_ssl: 0
                Ssl_ca: 
            Ssl_capath: 
              Ssl_cert: 
            Ssl_cipher: 
               Ssl_key: 
Ssl_verify_server_cert: 0
             Heartbeat: 30
                  Bind: 
    Ignored_server_ids: 0
                  Uuid: 
           Retry_count: 86400
               Ssl_crl: 
           Ssl_crlpath: 
 Enabled_auto_position: 1
          Channel_name: group_replication_applier
           Tls_version: 
*************************** 2. row ***************************
       Number_of_lines: 25
       Master_log_name: 
        Master_log_pos: 4
                  Host: 
             User_name: repl
         User_password: P@ssword1!
                  Port: 3306
         Connect_retry: 60
           Enabled_ssl: 0
                Ssl_ca: 
            Ssl_capath: 
              Ssl_cert: 
            Ssl_cipher: 
               Ssl_key: 
Ssl_verify_server_cert: 0
             Heartbeat: 0
                  Bind: 
    Ignored_server_ids: 0
                  Uuid: 
           Retry_count: 86400
               Ssl_crl: 
           Ssl_crlpath: 
 Enabled_auto_position: 0
          Channel_name: group_replication_recovery
           Tls_version: 
2 rows in set (0.00 sec)

然後回到s2節點上,安裝組複製外掛,並開啟組複製功能。

mysql> install plugin group_replication soname 'group_replication.so';
mysql> start group_replication;

組複製啟動成功後,檢視是否處於online狀態。(請無視我這裡的Member_host欄位,這是我設定了report_host變數的結果)

mysql> select * from performance_schema.replication_group_members\G 
*************************** 1. row ***************************
CHANNEL_NAME: group_replication_applier
   MEMBER_ID: a5165443-6aec-11e8-a8f6-000c29827955
 MEMBER_HOST: 192.168.100.22
 MEMBER_PORT: 3306
MEMBER_STATE: ONLINE
*************************** 2. row ***************************
CHANNEL_NAME: group_replication_applier
   MEMBER_ID: a659234f-6aea-11e8-a361-000c29ed4cf4
 MEMBER_HOST: xuexi.longshuai.com
 MEMBER_PORT: 3306
MEMBER_STATE: ONLINE

再檢視資料是否已經同步到s2節點。其實顯示了ONLINE,就一定已經同步。

mysql> show tables from gr_test;
+-------------------+
| Tables_in_gr_test |
+-------------------+
| t1                |
| t2                |
| t3                |
| t4                |
+-------------------+
4 rows in set (0.00 sec)

mysql> select * from gr_test.t4;
+----+
| id |
+----+
|  1 |
+----+

檢視binlog事件。會發現內容已經複製,且view id又發生了一次變化。

mysql> show binlog events in 'master-bin.000002';
+------+----------------+-----------+-------------+-------------------------------------------------------------------+
| Pos  | Event_type     | Server_id | End_log_pos | Info                                                              |
+------+----------------+-----------+-------------+-------------------------------------------------------------------+
|    4 | Format_desc    |       110 |         123 | Server ver: 5.7.22-log, Binlog ver: 4                             |
|  123 | Previous_gtids |       110 |         150 |                                                                   |
|  150 | Gtid           |       100 |         211 | SET @@SESSION.GTID_NEXT= 'a659234f-6aea-11e8-a361-000c29ed4cf4:1' |
|  211 | Query          |       100 |         399 | CREATE USER 'repl'@'192.168.100.%' IDENTIFIED WITH 'password'     |
|  399 | Gtid           |       100 |         460 | SET @@SESSION.GTID_NEXT= 'a659234f-6aea-11e8-a361-000c29ed4cf4:2' |
|  460 | Query          |       100 |         599 | GRANT REPLICATION SLAVE ON *.* TO 'repl'@'192.168.100.%'          |
|  599 | Gtid           |       100 |         660 | SET @@SESSION.GTID_NEXT= 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa:1' |
|  660 | Query          |       100 |         719 | BEGIN                                                             |
|  719 | View_change    |       100 |         858 | view_id=15294216022242634:1                                       |
|  858 | Query          |       100 |         923 | COMMIT                                                            |
|  923 | Gtid           |       100 |         984 | SET @@SESSION.GTID_NEXT= 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa:2' |
|  984 | Query          |       100 |        1083 | create database gr_test                                           |
| 1083 | Gtid           |       100 |        1144 | SET @@SESSION.GTID_NEXT= 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa:3' |
| 1144 | Query          |       100 |        1243 | use `gr_test`; create table t1(id int)                            |
| 1243 | Gtid           |       100 |        1304 | SET @@SESSION.GTID_NEXT= 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa:4' |
| 1304 | Query          |       100 |        1416 | use `gr_test`; create table t2(id int)engine=myisam               |
| 1416 | Gtid           |       100 |        1477 | SET @@SESSION.GTID_NEXT= 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa:5' |
| 1477 | Query          |       100 |        1601 | use `gr_test`; create table t3(id int primary key)engine=myisam   |
| 1601 | Gtid           |       100 |        1662 | SET @@SESSION.GTID_NEXT= 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa:6' |
| 1662 | Query          |       100 |        1773 | use `gr_test`; create table t4(id int primary key)                |
| 1773 | Gtid           |       100 |        1834 | SET @@SESSION.GTID_NEXT= 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa:7' |
| 1834 | Query          |       100 |        1893 | BEGIN                                                             |
| 1893 | Table_map      |       100 |        1937 | table_id: 112 (gr_test.t4)                                        |
| 1937 | Write_rows     |       100 |        1973 | table_id: 112 flags: STMT_END_F                                   |
| 1973 | Xid            |       100 |        2000 | COMMIT /* xid=31 */                                               |
| 2000 | Gtid           |       100 |        2061 | SET @@SESSION.GTID_NEXT= 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa:8' |
| 2061 | Query          |       100 |        2120 | BEGIN                                                             |
| 2120 | View_change    |       100 |        2299 | view_id=15294216022242634:2                                       |
| 2299 | Query          |       100 |        2364 | COMMIT                                                            |
+------+----------------+-----------+-------------+-------------------------------------------------------------------+

2.2.3 新增第三個節點

和加入s2節點幾乎一致。所以這裡做個步驟的簡單總結:

1.配置主機名和DNS解析

2.配置單主模型

2.提供配置檔案,並啟動MySQL例項

datadir=/data
socket=/data/mysql.sock

server-id=120                      
gtid_mode=on                       
enforce_gtid_consistency=on        
log-bin=/data/master-bin           
binlog_format=row                  
binlog_checksum=none               
master_info_repository=TABLE       
relay_log_info_repository=TABLE    
relay_log=/data/relay-log          
log_slave_updates=ON               
sync-binlog=1                      
log-error=/data/error.log
pid-file=/data/mysqld.pid

transaction_write_set_extraction=XXHASH64         
loose-group_replication_group_name="aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa"  
loose-group_replication_start_on_boot=off 
loose-group_replication_member_weigth = 30
loose-group_replication_local_address="192.168.100.23:20003"
loose-group_replication_group_seeds="192.168.100.21:20001,192.168.100.22:20002"

3.連上新例項,設定恢復通道的憑據。

change master to 
            master_user='repl',
            master_password='P@ssword1!'
            for channel 'group_replication_recovery';

4.安裝組複製外掛,並啟動組複製功能。

install plugin group_replication soname 'group_replication.so';
start group_replication;

5.檢視新節點是否已經處於ONLINE。

select * from performance_schema.replication_group_members\G

3.加組失敗

3.1 新節點一直處於recoveing?

當加入一個新節點時,一切配置都正確,但是新節點死活就是不同步資料,隨便執行一個語句都卡半天,檢視performance_schema.replication_group_members表時,還發現這個新節點一直處於recovering裝態。

這時,請檢視新節點的錯誤日誌。以下是我擷取出來的一行。

[root@xuexi ~]# tail /data/error.log 
2018-06-19T17:41:22.314085Z 10 [ERROR] Plugin group_replication reported: 'There was an error when connecting to the donor server. Please check that group_replication_recovery channel credentials and all MEMBER_HOST column values of performance_schema.replication_group_members table are correct and DNS resolvable.'

很顯然,連線donor的時候出錯,讓我們檢測通道憑據,並且檢視member_host欄位的主機名是否正確解析。一切正確配置的情況下,通道憑據是沒錯的,錯就錯在member_host的主機名。

當和donor建立通道連線時,首先會通過member_host欄位的主機名去解析donor的地址。這個主機名預設採取的是作業系統預設的主機名,而非ip地址。所以,必須設定DNS解析,或者/etc/hosts檔案,將member_host對應的主機名解析為donor的ip地址。

我這裡之所以顯示錯誤,是因為我在測試環境下,所有節點的主機名都相同:xuexi.longshuai.com。所以新節點會將這個主機名解析到本機。

3.2 新節點包含了額外的gtid事務?

如果新節點中包含了額外的資料,例如,新節點上多了一個使用者,建立這個使用者是會產生gtid事務的,當這個節點要加入到組時會報錯。以下是error.log中的內容:

2018-06-24T12:56:29.300453Z 0 [ERROR] Plugin group_replication reported: 'This member has more executed transactions than those present in the gro
up. Local transactions: 48f1d8aa-7798-11e8-bf9a-000c29296408:1-2 > Group transactions: 481024ff-7798-11e8-89da-000c29ff1054:1-4,
bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb:1-6'
2018-06-24T12:56:29.300536Z 0 [ERROR] Plugin group_replication reported: 'The member contains transactions not present in the group. The member wi
ll now exit the group.'

錯誤已經指明瞭是新節點的事務和組中的事務不匹配,有多餘的事務,從而導致加組失敗。

如果你已經明確這些多餘的事務造成的資料不會影響到組中的節點,正如多了一個可能永遠也用不上的使用者,或者多了幾個和組複製完全無關的資料庫。

這時,可以將這些無關緊要的gtid給刪掉,但是想刪除這些gtid還真沒那麼容易。purge日誌不行,停掉MySQL後刪日誌檔案也不行,把binlog關掉再開啟也不行。它們都會把以前的事務記錄到Previous_gtid中。真正行之有效的方法是將全域性變數executed_gtid設定為空。方法為:

mysql> reset master;

然後,再去加組。

4.組複製維護:停止、重啟組複製功能

操作組複製的語句只有兩個。

start group_replication;
stop group_replication;

但是,和組複製相關的變數卻有好幾個。

當要停止組中的某個成員中的組複製功能時,需要在那個節點上執行stop group_replication語句。但一定要注意,在執行這個語句之前,必須要保證這個節點不會向外提供MySQL服務,否則有可能會有新資料寫入(例如主節點停止時),或者讀取到過期資料。

所以,要安全地重啟整個組,最佳方法是先停止所有非主節點的MySQL例項(不僅是停止組複製功能),然後停止主節點的MySQL例項,再先重啟主節點,在這個節點上引導組,並啟動它的組複製功能。最後再將各slave節點加入組。

如果只是想停止某單個節點,如果這個節點是主節點,那麼停止整個MySQL例項,如果是slave節點,那麼只需停止它的組複製功能即可。當它們需要再次加組時,只需執行start group_replication語句。

那麼,如何知道哪個節點是主節點?

5.查詢複製組中的主節點

只有單主模型的組複製才需要查詢主節點,多主模型沒有master/slave的概念,所以無需查詢。

mysql> SELECT VARIABLE_VALUE FROM performance_schema.global_status 
       WHERE VARIABLE_NAME='group_replication_primary_member';
+--------------------------------------+
| VARIABLE_VALUE                       |
+--------------------------------------+
| a659234f-6aea-11e8-a361-000c29ed4cf4 |
+--------------------------------------+
1 row in set (0,00 sec)

或者:

mysql> SHOW STATUS LIKE 'group_replication_primary_member';

這樣查詢只是獲取了主節點的uuid,可以表連線的方式獲取主節點主機名。

select b.member_host the_master,a.variable_value master_uuid
    from performance_schema.global_status a
    join performance_schema.replication_group_members b
    on a.variable_value = b.member_id
    where variable_name='group_replication_primary_member';
+------------------+--------------------------------------+
| the_master       | master_uuid                          |
+------------------+--------------------------------------+
| s1.longshuai.com | a659234f-6aea-11e8-a361-000c29ed4cf4 |
+------------------+--------------------------------------+

6.測試:組複製的自動選舉和容錯

組複製中,有兩種節點離組的情況:自願離組、非自願離組。

  1. 自願離組:執行stop group_replication;語句。
    • (1).執行該語句表示該節點自願離組,它會觸發檢視自動配置,並將該檢視更改操作複製到組內所有節點,直到大多數節點都同意新的檢視配置,該節點才會離組。
    • (2).節點自願離組時,不會丟失法定票數。所以無論多少個節點自願離組,都不會出現"達不到大多數"的要求而阻塞組。
    • (3).舉個例子,5個節點的組,自願退出一個節點A後,這個組的大小為4。這個組認為節點A從來都沒有出現過。
  2. 非自願離組:除了上面自願離組的情況,所有離組的情況都是非自願離組。比如節點當機,斷網等等。
    • (1).節點非自願離組時,故障探測機制會檢測到這個問題,於是向組中報告這個問題。然後會觸發組檢視成員自動配置,需要大多數節點同意新檢視。
    • (2).非自願離組時,組的大小不會改變,無論多少個節點的組,節點非自願退出後,組大小還是5,只不過這些離組的節點被標記為非ONLINE。但注意,組的檢視配置會改變,因為離組的節點狀態需要標記為非ONLINE。
    • (3).非自願離組時,會丟失法定票數。所以,當非自願離組節點數量過多時,導致組中剩餘節點數量達不到大多數的要求,組就會被阻塞。
    • (4).舉個例子,5節點的組,非自願退出1個節點A後,這個組的大小還是5,但是節點A在新的檢視中被標記為unreachable或其他狀態。當繼續非自願退出2個節點後,組中只剩下2個ONLINE節點,這時達不到大多數的要求,組就會被阻塞。

目前,組中有3個節點:s1、s2和s3,其中s1是主節點。

現在將主節點直接關機或者斷掉網路卡,模擬非自願離組。

# s1上:
shell> ifconfig eth0 down

然後檢視s2上的錯誤日誌。可以看到選舉新主節點的過程。

[Warning] group_replication reported: 'Member with address s1.longshuai.com:3306 has become unreachable.'
[Note] group_replication reported: '[GCS] Removing members that have failed while processing new view.'
[Warning] group_replication reported: 'Members removed from the group: s1.longshuai.com:3306'
[Note] group_replication reported: 'Primary server with address s1.longshuai.com:3306 left the group. Electing new Primary.'
[Note] group_replication reported: 'A new primary with address s2.longshuai.com:3306 was elected, enabling conflict detection until the new primary applies all relay logs.'
[Note] group_replication reported: 'This server is working as primary member.'
[Note] group_replication reported: 'Group membership changed to s2.longshuai.com:3306, s3.longshuai.com:3306 on view 15294358712349771:4.'

這裡將s2選為新的主節點,且告知成員檢視中目前組中成員變為s2和s3。

可以測試下,是否能向新的主節點s2中插入資料。

# s2上:
mysql> insert into gr_test.t4 values(333);

如果再將s3停掉呢?還能繼續寫入資料嗎?

# 在s3上:
shell> ifconfig eth0 down

回到s2,插入資料看看:

# s2上:
mysql> insert into gr_test.t4 values(3333); 

發現無法插入,一直阻塞。

檢視下s2的錯誤日誌:

[Warning] group_replication reported: 'Member with address s3.longshuai.com:3306 has become unreachable.'
[ERROR] group_replication reported: 'This server is not able to reach a majority of members in the group. This server will now block all updates. The server will remain blocked until contact with the majority is restored. It is possible to use group_replication_force_members to force a new group membership.'

已經說明了,s3移除後,組中的成員無法達到大多數的要求,所以將複製組給阻塞了。如果想要修復組,可以強制生成一個新的組成員檢視。

如果這時候,將s1和s3的網路卡啟動,s1和s3還會加入到組中嗎?以下是s2上的錯誤日誌:

[Warning] group_replication reported: 'Member with address s3.longshuai.com:3306 is reachable again.'
[Warning] group_replication reported: 'The member has resumed contact with a majority of the members in the group. Regular operation is restored and transactions are unblocked.'

發現s3加入了,但s1未加入。為什麼?因為s1節點上只是停掉了網路卡,mysql例項以及組複製功能還在執行,而且它的角色還保持為主節點。這時候,s1和s2、s3已經出現了所謂的"網路分裂",對於s2和s3來說,s1被隔離,對於s1來說,s2和s3被隔離。當s1的網路卡恢復後,它仍然保留著自己的主節點執行,但因為它達不到大多數的要求,所以s1是被阻塞的,如果網路卡長時間沒有恢復,則s1會被標記為ERROR。

這種情況下的s1,要讓它重新加入到組中,應該重啟組複製,更安全的方法是重啟mysql例項,因為組可能還沒有標記為ERROR,這個組暫時還存在,它與s2、s3所屬的組同名,可能會導致腦裂問題。

如果是自願離組呢?可以測試下,無論自願退出多少個節點,只要組中還有節點,組都不會被阻塞。

相關文章