mysql gtid 还是pxc_记一次 PXC 集群拆分引发的思考
原標題:記一次 PXC 集群拆分引發的思考
作者簡介
冷正磊
2018年2月加入去哪兒網 DBA 團隊,主要負責機票業務的 MySQL 和 Redis 數據庫的運維管理工作,以及數據庫自動化運維平臺部分功能的開發工作,對數據庫技術具有濃厚興趣,具有多年 MySQL 和 Redis 運維管理和性能優化經驗。
1. 內容摘要
眾所周知,MySQL 基于 GTID 復制功能的出現,極大地簡化了 MySQL 復制拓撲初始化配置和變更以及高可用的切換。在去哪兒網,我們大量使用 PXC(Percona XtraDB Cluster)集群,然而 PXC 中用于記錄事務的 Galera GTID 與普通的 MySQL GTID 還是有一點差異,運維過程中如果不加注意,可能會引發一些問題。本文通過記錄一次 PXC 集群拆分的過程中由于未深刻理解這兩者的差別而導致的問題與原因分析,總結了 Galera GTID 與 MySQL GTID 的異同點以及運維過程中應該注意的事項。
2. 背景
Qunar 機票核心業務某個 PXC 集群 C1 由于運行時間比較久,隨著業務的持續發展,集群單個節點的實例數據大小已達到 5T 以上,對數據量如此大的 MySQL 集群進行日常維護(備份、集群節點水平擴容、實例遷移等)以及實例故障恢復都是一項比較耗時費力的工作。經過與研發討論后決定將集群 C1 中比較大的兩個庫 DB1 和 DB2 拆分出來,組成一個新的 PXC 集群 C2。
集群拆分前后示意圖如下(正常每個集群有三個節點,為簡單起見,每個集群只畫了一個節點):
3. 方案簡要說明
PXC 集群進行庫的拆分,大致流程是使用當前集群 C1 的任意一個節點做一個全量副本,利用這個副本再做2個節點的數據,組建一個三節點的新集群 C2。同時為了保持數據的一致性,新集群 C2 的寫節點作為原有集群 C1 某個節點的從庫,不斷同步集群 C1 的數據更新。
原計劃是第一步先遷移 DB1,主要流程為:
業務方下線集群 C1 上與 DB1 相關的應用服務,停止對該庫中所有表的寫入。
為了防止遺漏的應用服務對 DB1 進行寫入,DBA 將該庫里面所有的表進行改名,即加一個統一的后綴(需提前準備好腳本)。
DBA 確認兩個集群直接主從同步無延遲后,在新集群 C2 上恢復 DB1 所有表的名稱,即去掉第2步中添加的后綴(需提前準備好腳本)。
業務方發布新的應用服務(業務方已提前修改好代碼中的數據源配置),開始訪問集群 C2 中的 DB1,各系統驗證業務是否正常。
第二步是在完成第一步之后,仍然保持兩個集群之間主從同步關系,等使用 C2 中 DB1 相關的業務確認無問題后以同樣的方式遷移 DB2。
全部遷移完后觀察一段時間,確認各業務流程正常,最后刪除兩個集群中不需要的 DB。
其中第一步操作過程如下:
不過在順利完成第一步后出現了意外,原本應該正常同步數據的兩個集群出現了復制中斷,根據報錯信息發現大量的數據(除 DB1 之外的庫)在集群 C2 上找不到對應的記錄,由于兩個集群中 DB2 的數據沒法保證一致性,導致不得不中止后續的遷移計劃,以至于集群 C2 上只完成了庫 DB1 的遷移。
4. 問題分析與復現4.1 問題分析
正常來說,集群 C1 已經徹底停止(表名已改)了對 DB1 中表的寫入,而集群 C2 上只會對 DB1 中表進行寫入,其他庫的寫入不受影響,應該正常復制才對。
既然復制出現了問題,那么原有的“理所當然”的想法肯定存在不合理的地方。經過排查,我們發現了一個令人匪夷所思的問題,兩個集群用作復制的兩個節點,從庫和主庫的 GTID 的 部分竟然是一樣的,導致從庫在對 DB1 進行寫入后,生成的 GTID 的 值比主庫上大,當接收主庫推送過來的 binlog 數據時,發現主庫事務的 GTID 的 值比自己的小,于是從庫直接選擇了跳過該事務,并沒有重放這部分 binlog,從而出現了主從數據不一樣的情況。
4.2 復現過程
為什么新建的從庫生成的 GTID 的 會和主庫一樣呢?為了找到問題的原因,我們在測試環境用同樣的流程對出現的問題進行復現。
首先我們復盤了下搭建主從復制的過程,大致如下:
1、使用 Xtrabackup 備份集群 C1 某個節點的全量數據,并將數據傳送到目的服務器,用于新建集群 C2 的第一個節點。
2、在目的服務器 apply 備份日志后,根據生成的文件 xtrabackup_ binlog_info 中的內容找到復制信息。
# catxtrabackup_binlog_info
mysql-bin.000015997 401 cdbc9-e228-ee17-496f-5c53bc36ae5b:1-1123,
c05582e9-dc11-ee14-6b06-c041b8b7ff2d:1-4,
da5e0de8-dc13-ee14-76e6-f074e061cc69:1-2
3、新建文件 grastate.dat,并根據 apply 后生成的文件 xtrabackup_galera_info中的內容填寫 grastate.dat 文件信息。
# cat xtrabackup_galera_info
3faa7d16-23ee-11eb-94f9-3fbe474800d2:4
# vim grastate.dat# GALERA saved stateversion: 2.1uuid: 3faa7d16-23ee-11eb-94f9-3fbe474800d2seqno: 4safe_to_bootstrap: 1
4、 以 bootstrap-pxc 方式啟動該實例,作為集群 C2 的第一個節點,并與老集群 C1 建立復制關系。
# 啟動實例/etc/init.d/mysql.server -P 3311 bootstrap-pxcmysql> reset slave all;Query OK, 0 rows affected (0.00 sec)
# 建立新的復制mysql> set wsrep_on = 0;Query OK, 0 rows affected (0.00 sec)
mysql> reset master;Query OK, 0 rows affected (0.00 sec)
mysql> set wsrep_on = 1;Query OK, 0 rows affected (0.00 sec)
mysql> SET GLOBAL gtid_purged='401cdbc9-e228-ee17-496f-5c53bc36ae5b:1-1123,c05582e9-dc11-ee14-6b06-c041b8b7ff2d:1-4,da5e0de8-dc13-ee14-76e6-f074e061cc69:1-2';Query OK, 0 rows affected (0.01 sec)
mysql> change master to master_host='10.86.41.xxx',master_port=3306,master_user='replication',master_password='xxxxxxxxxx',master_auto_position=1;Query OK, 0 rows affected, 2 warnings (0.02 sec)
mysql> start slave;Query OK, 0 rows affected (0.00 sec)
# 集群C2此時的master信息mysql> show master statusG*************************** 1. row ***************************File: mysql-bin.000002Position: 271Binlog_Do_DB:Binlog_Ignore_DB:Executed_Gtid_Set: 401cdbc9-e228-ee17-496f-5c53bc36ae5b:1-1123,c05582e9-dc11-ee14-6b06-c041b8b7ff2d:1-4,da5e0de8-dc13-ee14-76e6-f074e061cc69:1-21 row in set (0.00 sec)
5、 向集群 C1 中未遷移的庫 test2 中正常寫入數據,觀察主從 master 信息。
# 寫入前集群C1的master狀態mysql> show master statusG*************************** 1. row ***************************File: mysql-bin.000015Position: 997Binlog_Do_DB:Binlog_Ignore_DB:Executed_Gtid_Set: 401cdbc9-e228-ee17-496f-5c53bc36ae5b:1-1123,c05582e9-dc11-ee14-6b06-c041b8b7ff2d:1-4,da5e0de8-dc13-ee14-76e6-f074e061cc69:1-21 row in set (0.00 sec# 向集群C1的庫test2中寫入兩個事務的數據后master狀態mysql> use test2;mysql> insert into t values(13);Query OK, 1 row affected (0.00 sec)
mysql> insert into t values(14);Query OK, 1 row affected (0.00 sec)
mysql> show master statusG*************************** 1. row ***************************File: mysql-bin.000015Position: 1481Binlog_Do_DB:Binlog_Ignore_DB:Executed_Gtid_Set: 401cdbc9-e228-ee17-496f-5c53bc36ae5b:1-1123,c05582e9-dc11-ee14-6b06-c041b8b7ff2d:1-6, # 發生變化的GTIDda5e0de8-dc13-ee14-76e6-f074e061cc69:1-21 row in set (0.00 sec)
# 此時集群C2的master信息mysql> show master statusG*************************** 1. row ***************************File: mysql-bin.000002Position: 735Binlog_Do_DB:Binlog_Ignore_DB:Executed_Gtid_Set: 401cdbc9-e228-ee17-496f-5c53bc36ae5b:1-1123,c05582e9-dc11-ee14-6b06-c041b8b7ff2d:1-6, # 發生變化的GTID,同步正常da5e0de8-dc13-ee14-76e6-f074e061cc69:1-21 row in set (0.00 sec)
此時復制沒有問題,數據也是正常的。
6、向集群 C2 中已遷移的庫 test1 中寫入兩個事務的數據,觀察主從 master 信 息。
mysql> insert into t values(7);Query OK, 1 row affected (0.00 sec)mysql> insert into t values(8);Query OK, 1 row affected (0.00 sec)
mysql> show master statusG*************************** 1. row ***************************File: mysql-bin.000002Position: 1209Binlog_Do_DB:Binlog_Ignore_DB:Executed_Gtid_Set: 401cdbc9-e228-ee17-496f-5c53bc36ae5b:1-1123,c05582e9-dc11-ee14-6b06-c041b8b7ff2d:1-8, # 寫入后,從節點發生變化的GTIDda5e0de8-dc13-ee14-76e6-f074e061cc69:1-21 row in set (0.00 sec)
7、 此后如果集群 C1 上繼續寫入一個事務。
mysql> delete from t where id = 13; # 刪除test2庫t表中id=13的記錄Query OK, 1 row affected (0.00 sec)# 集群C1的master信息mysql> show master statusG*************************** 1. row ***************************File: mysql-bin.000015Position: 1723Binlog_Do_DB:Binlog_Ignore_DB:Executed_Gtid_Set: 401cdbc9-e228-ee17-496f-5c53bc36ae5b:1-1123,c05582e9-dc11-ee14-6b06-c041b8b7ff2d:1-7, # GTID的gno增加1da5e0de8-dc13-ee14-76e6-f074e061cc69:1-21 row in set (0.00 sec)
# 集群C2的master信息mysql> show master statusG*************************** 1. row ***************************File: mysql-bin.000002Position: 1209Binlog_Do_DB:Binlog_Ignore_DB:Executed_Gtid_Set: 401cdbc9-e228-ee17-496f-5c53bc36ae5b:1-1123,c05582e9-dc11-ee14-6b06-c041b8b7ff2d:1-8, # GTID的gno沒有發生變化,因為同步過來的事務的GTID的gno值比自己小,選擇跳過da5e0de8-dc13-ee14-76e6-f074e061cc69:1-21 row in set (0.00 sec)
# 此時C1集群已經被刪掉的記錄,在C2集群仍然存在mysql> use test2;Database changedmysql> select * from t where id = 13;+----+| id |+----+| 13 |+----+1 row in set (0.00 sec)
從測試過程中發現,確實通過以上的方式搭建主從節點后,存在主從節點寫入后 GTID 值的部分相同的情況,此時如果主從節點同時發生寫入(針對不同的庫),就會導致主從節點數據不一致。
4.3 問題原因
對問題復現過程進行分析后,發現整個過程有一步是多余的,即步驟3(新建 grastate.dat 文件),因為我們的目的是通過搭建從庫的方式組建一個新的集群,而不是對原有集群擴增節點,所以在該 slave 節點(針對原集群而言)以 bootstrap 方式啟動時,不需要指定原來的集群信息。
當以 bootstrap 方式啟動 PXC 實例時,如果 grastate.dat 文件存在,那么該實例會從該文件中獲取 uuid 參數的值,賦值給參數 wsrep_ cluster_state_uuid,同時該參數的值也決定了事務的 Galera GTID 中的 部分,所以導致這個實例的與原集群中節點的 相同,當作為主從的兩個節點都有寫入時,從庫在應用 binlog 時就會出現沖突或者忽略的情況,導致主從數據不一致。
5. 如何改進
通過此次集群拆分過程中出現的問題,總結下原因以及改進措施,避免后續工作中出現類似情況:
5.1 原因
延用了前期類似經驗的慣性思維。因為在此之前有過兩次類似的遷移操作,只不過當時只需遷移一個庫,在原集群停止該庫的寫入后,等從庫復制無延遲后就馬上斷開了復制,所以沒有出現后續復制的問題。
操作流程不夠精細。操作過程中沒有嚴格區分 PXC 集群新增節點和拆分集群的流程差異,細節之處欠缺考慮,也反映出個人在對 PXC 的使用和原理方面研究不夠深入。5.2 改進措施
分別制定 PXC 集群新增節點和拆分集群的操作規范,在運維操作時嚴格按照規范執行。
優化集群拆分方案。比如可建立一個中間節點,對要遷移的數據庫進行過濾復制,然后再同步到新集群中,可節省組建新集群的時間,同時可避免后續在新集群上刪除多余的庫。
3. 在標準規范的基礎上,實現運維操作自動化,避免人為主觀因素造成影響。
6. 關于 Galera GTID 與 MySQL GTID 的比較6.1 GTID 的概念
GTID 特性是 MySQL5.6加入的一個強大的特性,全稱是 Global Transaction Identifier。MySQL 會為每一個 DML/DDL 操作增加一個唯一標記叫做 GTID,這個標記在整個復制環境中都是唯一的,格式為 。
GTID 相關的幾個常見術語:
server_ uuid:單個 GTID 的前半部分,即 部分,是一個32字節+1字節(/0)的字符串。
gno:單個 GTID 的后半部分,即 部分,表示事務的序號,gno 的值從全局計數器 next_ free_ gno 中獲取的。
GTID SET:表示一個 GTID 的集合,可以包含多個 server_ uuid,如 executed_ gtid、gtid_ purged。
GTID SET Interval:GTID SET 中某個 server_uuid 可能包含多個區間,比如 GTID 為“23d45aa2-3d1f-11e6-a16b-c81f66e1165d:1-99:110-200”的字符串中,GTID SET Interval 分別是“1-99”和“110-200”。6.2 GTID 的生成
GTID 是在 SQL 的 commit 命令發起后,order commit 執行到 flush 階段需要生成 GTID Event 的時候才會獲取。MySQL 內部維護了一個全局的 GTID 的計數器 next_ free_gno 用于生成 gno。
可參考函數 Gtid_ state∶getautomatic_gno,部分代碼如下∶
// 定義∶Gtid next_candidate={ sidno,sidno == get_server_sidno? next_free_gno: 1};// 賦值∶while( true){constGtid_set::Interval *iv= ivit.getO;// 定義IntervaL指針指向這個鏈表指針開頭,如果在進行下次循環會獲得NULLrpl_gno next_interval_start=iv != NULL? iv->start: MAX_GNO;// 正常情況下不會為NULL,因此 next_interval_start 等于第一個interval的start,當然如果初始化會為NULL,如果Interval->next =NULL 則標示設有區間了。while(next_candidate.gno < next_interval_start &&DBUG_EVALUATE_IF( "simulate_gno_exhausted", false, true))// 如果next_candidate.gno正常不會小于next_intervalL_start// 如果Interval->next =NULL或者初始化next_interval_start會被置為MAX_GNO,那么條件成立DBUG_RETURN(next_candidate.gno);// 返回了這個gno 則GTID生成{// 返回gno,GTID生成if(owned_gtids.get_ownernext_candidate)==O)DBUG_RETURN(next_candidate.gno)// 如果本GTID已經被其他線程占用,則next_candidate.gno++ 繼續判斷next_candidate.gno++;}......}
6.3 server_uuid 的生成
MySQL 在啟動的時候會調用 init_ server_auto_ options 來讀取 auto.cnf 文件。如果 auto.cnf 文件不存在,則會調用函數 generate_server_ uuid 來生成一個新的 server_uuid,這時 GTID 會發生改變。
當 auto.cnf 文件不存在時,調用函數 generate_ serve_ruid 生成 server_ uuid 的過程中可以看出,server_uuid 的生成至少和下面部分有關∶
數據庫的啟動時間。
線程的 LWP ID。LWP 是輕量級進程(light-weight process)的簡稱。
一個隨機的內存地址。
下面是部分代碼供參考∶
// 獲取MySqL啟動時間consttime_t save_server_start_time=server_start_time;// 加入LWP號運算server_start_time+=((ulonglong)current_pid << 48)+current_pid;// 一個內存指針,即線程結構體的內存地址thd->status_var.bytes_sent=(ulonglong)thd;// 具體的運算過程lex_start(thd);func_uuid= new(thd->mem_root)Item_func_uuid;func_uuid->fixed= 1;func_uid->vaL_str(&uuid);
6.4 Galera GTID
PXC 集群記錄事務的 Galera GTID 中 的生成邏輯與 MySQL GTID 的不太一樣,而且也不是 PXC 集群的 wsrep_ cluster_state_uuid,還是以上面的 PXC 集群 C2 為例,看下這個幾個參數的值:
mysql> select @@server_uuid;+--------------------------------------+| @@server_uuid |+--------------------------------------+| 4fd32e4d-249f-11eb-8fd9-fa163e05f092 |+--------------------------------------+1row inset ( 0. 00sec)mysql> select @@global.gtid_executed;+---------------------------------------------------------------------------------------------------------------------------------+| @@global.gtid_executed |+---------------------------------------------------------------------------------------------------------------------------------+| 401cdbc9-e228-ee17-496f-5c53bc36ae5b:1-1123,c05582e9-dc11-ee14-6b06-c041b8b7ff2d:1-8,da5e0de8-dc13-ee14-76e6-f074e061cc69:1-2 |+---------------------------------------------------------------------------------------------------------------------------------+1row inset ( 0. 00sec)
mysql> show status like 'wsrep_%_uuid';+--------------------------+--------------------------------------+| Variable_name |Value |+--------------------------+--------------------------------------+|wsrep_local_state_uuid | 3faa7d16-23ee-11eb-94f9-3fbe474800d2 || wsrep_gcomm_uuid |4f807d05- 249f- 11eb-a679-ea70d2c3575a ||wsrep_cluster_state_uuid | 3faa7d16-23ee-11eb-94f9-3fbe474800d2 |+--------------------------+--------------------------------------+3rows inset ( 0. 00sec)
可以看到,PXC 集群中實例的 server_ uuid 并不在它的 GTID SET 中,當 PXC 集群寫入數據時,生成的 MySQL GTID 的,也不是服務器的 server_uuid。
實際上在 PXC 集群中這么設計合理的,因為 PXC 是一個分布式可多寫的集群架構,所有節點共享相同的 ,當在不同的節點寫入數據時,將產生同樣的 GTID SET,看起來不同的事務像是在同一個服務器上執行的。
6.5 Galera GTID vs MySQL GTID
兩種 GTID 使用的格式相同,即 。
對于 Galera 來說,在集群以 bootstrap 啟動時會生成 ,且集群中的所有節點共享此 。
所以說 PXC 集群中各節點之間用作同步的 Galera GTID 和 MySQL GTID 之間并沒有直接關系,在運維過程中切記不要搞混淆。
參考資料:
1、簡書專欄《深入理解主從原理32講》 作者:重慶八怪2、《MySQL 運維內參》 作者:周彥偉、王竹峰、強昌金
責任編輯:
創作挑戰賽新人創作獎勵來咯,堅持創作打卡瓜分現金大獎總結
以上是生活随笔為你收集整理的mysql gtid 还是pxc_记一次 PXC 集群拆分引发的思考的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: java pdf转ofd
- 下一篇: 数字图像处理——图像去雾技术的对比