天天看點

真實案例:主備切換資料不一緻分析

故障複盤

基于一套主從的MHA環境,A為現主庫,B為現從庫。其中

    A的uuid是5a56.....7df

    B的uuid是6a56.....7df

(1)基于MHA的一主一從環境,演練主庫當機,主備切換

VIP目前在A上,提供給業務使用,模拟主庫當機

systemctl stop mysqld

主庫當機後,觀察到VIP正常飄移到B庫上,業務正常使用,此時,重新開機A庫,企圖将A庫重新開機後重新加入叢集,并啟動MHA。

啟動A庫後,将A庫作為從庫加入到新主B,出現1032報錯。

A庫資訊如下

真實案例:主備切換資料不一緻分析

B庫資訊如下

真實案例:主備切換資料不一緻分析

發現此刻的從庫的GTID_SET  5a開頭的那個少了一個事務,也就是說舊主A沒有将事務全部同步到舊從B,導緻現在B少了一個事務,就切換為新主了。

此刻想法就是先嘗試将A庫沒有同步到B庫的事務先拉過去,采用主從的方式,将A庫進行stop slave操作, 然後将B庫重新指向為A的從庫,這樣就将B庫缺少的事務拉過去了,

再次将B庫進行stop slave 操作,将A庫 start slave 發現依然報錯。

分析:由于B此前未開啟read_only,很有可能,A還是主的時候,B上面有寫新資料進去。

(2)備庫隻讀狀态下,演練主庫當機,主備切換

(1)将B全備用于恢複A

(2)将A的read_only打開為ON,禁止寫入 set global read_only=on;

(3)重新開啟MHA,此時B為主,A為從庫

(4)将兩個庫的配置檔案read_only均打開,修改MHA master_ip_failover腳本,切換為主的時候,才将read_only關閉。避免從庫寫入資料。

注意:此時是B主,A從,測試挂掉主庫B

現VIP在B庫上,A為從庫,測試将B庫當機,VIP飄移到A庫。業務正常使用,這個時候,将B庫重新開機

觀察此刻A庫上的gtid資訊如下圖

真實案例:主備切換資料不一緻分析

而B庫重新開機後的gtid資訊如下

真實案例:主備切換資料不一緻分析

可以看到B庫原來是主庫,uuid是6a,B的gtid在本地庫執行的gno比A多了一個26010,A庫上隻有26009。

依然出現了新主庫比舊主庫少了一個事務的情況,并且在修改從庫隻讀的情況下,将挂掉的舊主重新加入主從依然是報錯1032。

分析:可能是全備恢複的時候,從庫沒有進行purge操作,導緻gtid_executed表在導入資料的過程中被覆寫。一旦從庫再次重新開機,讀取gtid_executed表就會得到錯誤的gtid_executed變量,導緻啟動失敗。

(3)重做備庫,将備庫gtid_executed可能導緻的問題排除

此刻A是主,B是備

(1)停止主從

(2)主庫A進行reset master

(3)主庫A進行全備,并傳輸到備庫B

真實案例:主備切換資料不一緻分析

(4)将B庫reset master

(5)在B庫上執行purge操作

真實案例:主備切換資料不一緻分析

(6)在B庫上執行reset slave all

(7)重搭主從

CHANGE MASTER TO
MASTER_HOST='*******',
MASTER_USER='repl',
MASTER_PASSWORD='******',
MASTER_PORT=3306,
MASTER_AUTO_POSITION=1;
start slave ;      

(8)修複MHA配置檔案,重新開機MHA

此刻A為主庫,VIP在主庫,提供給業務使用

測試挂掉主庫A,此刻A庫挂掉,VIP飄移到B上,重新開機A庫,試圖重新加入到主從,報錯

真實案例:主備切換資料不一緻分析

分析:A庫依然比B庫多了一個事務,且報錯也是1032,解析原目前主庫B庫該位點的binlog,是一條update資訊,而該表在A庫中是空表,是以報錯:1032錯誤要更新的資料不存在

真實案例:主備切換資料不一緻分析

檢視從A庫dump出來的備份檔案,檢視該表是有在備份時候有記錄,發現備份出來的時候是有的

真實案例:主備切換資料不一緻分析

解析B庫的所有binlog資訊,并沒有發現有對該表進行drop ,detete,或者truncate 操作。

可以大概猜得到應該是主庫A備份出來的時候資料還在,而主庫A挂之前,該表被清空的操作,并沒有同步到從庫。是以出現也gtid比新主庫多了一個事務,且主從1032的錯誤,因為新主對該表的update操作無法在新從,也就是舊主A上執行。

驗證:

解析A庫binlog可以看到

真實案例:主備切換資料不一緻分析

(4)嘗試增強半同步的方式

安裝半同步的插件
主庫
INSTALL PLUGIN rpl_semi_sync_master SONAME 'semisync_master.so';
從庫
INSTALL PLUGIN rpl_semi_sync_slave SONAME 'semisync_slave.so';
show plugins;
檢視是否安裝成功

半同步需要在主從同時開啟
主庫
SET GLOBAL rpl_semi_sync_master_enabled = 1;
從庫
SET GLOBAL rpl_semi_sync_slave_enabled = 1;
以上的啟動方式是在指令行操作,也可寫在配置檔案中。

重新開機從庫的IO線程
STOP SLAVE IO_THREAD;
START SLAVE IO_THREAD;

檢視是否開啟半同步複制
mysql> show status like 'Rpl_semi_sync_master_status';
+-----------------------------+-------+
| Variable_name               | Value |
+-----------------------------+-------+
| Rpl_semi_sync_master_status | ON    |
+-----------------------------+-------+

mysql> show status like 'Rpl_semi_sync_slave_status';
+----------------------------+-------+
| Variable_name              | Value |
+----------------------------+-------+
| Rpl_semi_sync_slave_status | ON    |
+----------------------------+-------+

配置檔案
#plugin_load = "rpl_semi_sync_master=semisync_master.so;rpl_semi_sync_slave=semisync_slave.so"
rpl_semi_sync_slave_enabled = 1
rpl_semi_sync_master_enabled = 1
rpl_semi_sync_master_wait_point = after_sync
rpl_semi_sync_master_timeout =10000
rpl_semi_sync_master_wait_no_slave=0      

依然沒有得到解決

複盤結論

    主庫最後一個事務truncate操作沒有同步到從庫。 

主庫異常當機挂掉,truncate沒有同步到從庫,而當主庫重新開機,被作為新從庫,重新加入到主從中去的時候,由于該表資料已經被truncate掉,新主中對該表資料的update

操作同步過來的時候就會報錯1032,找不到要更新的資料,主從異常。為了避免主從不一緻,嘗試改為增強半同步,依然沒有解決該問題。

主從不一緻分析

主從複制

master事務的送出不需要經過slave的确認,slave是否接收到master的binlog,master并不care。slave接收到master

binlog後先寫relay log,最後異步地去執行relay log中的sql應用到自身。由于master的送出不需要確定slave

relay log是否被正确接受,當slave接受master binlog失敗或者relay log應用失敗,master無法感覺。

真實案例:主備切換資料不一緻分析

異步複制本身對于資料一緻性不做保證

半同步複制

基于傳統異步存在的缺陷,mysql在5.7版本推出增強半同步複制。可以說半同步複制是傳統異步複制的改進,在master事務的commit之前,必須確定一個slave收到relay

log并且響應給master後(從庫收到并産生 relaylog 後會向主庫發送一個 ACK 的資訊包,當主庫獲得這個包後,認為從庫已經獲得 relaylog)才能進行事務的commit。但是slave對于relay log的應用仍然是異步進行的。

真實案例:主備切換資料不一緻分析

AFTER_COMMIT 方式

MYSQL5.7 之前半同步複制采用的是 AFTER_COMMIT 方式--比 AFTER_SYNC 會有更大機率造成資料不一緻

AFTER_COMMIT 是先做 REDO COMMIT 後傳 BINLOG,做事務送出,隻是不給用戶端傳回。

AFTER_COMMIT(5.6預設值)

master将每個事務寫入binlog ,傳遞到slave 重新整理到磁盤(relay log),同時主庫送出事務。

master等待slave 回報收到relay log,隻有收到ACK後master才将commit OK結果回報給

用戶端。

實際上,主庫在等待ACK的InnoDB存儲引擎内部已經送出事務,

隻是阻塞了傳回給發起事務送出的用戶端消息而已。 該缺陷可能導緻非發起資料送出的用戶端在碰到主庫故障轉移時發生幻讀。

真實案例:主備切換資料不一緻分析

commitTrx的調用在engine層commit之後(在ordered_commit函數中process_after_commit_stage_queue調用),如上圖所示。即在等待Slave

ACK時候,雖然沒有傳回目前用戶端,但事務已經送出,其他用戶端會讀取到已送出事務。如果Slave端還沒有讀到該事務的events,同時主庫發生了crash,然後切換到備庫。那麼之前讀到的事務就不見了,出現了幻讀。

測試:

從庫上停掉IO_THREAD模拟從庫異常
stop replica io_thread;

主庫上插入一條資料,此時會HANG住(但是這條資料已經寫入了,開啟一個會話是可以查到該資料的)
insert into t1 values(3);
開啟新SESSION查詢T表
select * from t1;
傳回1,2,3
開啟另一個會話殺掉主庫MYSQLD程序pkill -9 mysqld
此時從庫中是查不到插入3這條資料的。

select * from t;
傳回1,2      

如果此時發生主從切換則主從資料發生不一緻。這也是after_commit模式複制中幻讀現象。 如圖:

真實案例:主備切換資料不一緻分析

AFTER_SYNC方式

AFTER_SYNC 是先傳 binlog 後做 REDO COMMITmaster 将每個事務寫入binlog , 傳遞到slave 重新整理到磁盤(relay log)。master

等待slave 回報接收到relay log的ack之後,再送出事務并且傳回commit OK

結果給用戶端。

即使主庫crash,所有在主庫上已經送出的事務都能保證

已經同步到slave的relay log中。

真實案例:主備切換資料不一緻分析

sync_binlog對主備的影響

參數值含義

sync_binlog= 0/1/n
0:表示每次送出事務都隻 write,不 fsync,每過一秒fsync到磁盤,每一秒刷一次磁盤
1:表示每次事務送出都刷一次磁盤,也就是每次送出事務都會執行fsync
n:(100 200 500)表示每次送出事務都 write到OS cache,但累積 N 個事務後才 fsync到磁盤      

binlog傳輸給備庫的時機

主備複制開啟的流程

1、在備庫 B 上通過 change master 指令,設定主庫 A 的 IP、端口、使用者名、密碼,以及要從哪個位置開始請求 binlog,這個位置包含檔案名和日志偏移量。

2、在備庫 B 上執行 start slave 指令,這時候備庫會啟動兩個線程,就是圖中的 io_thread 和 sql_thread。其中 io_thread 負責與主庫建立連接配接。

3、主庫 A 校驗完使用者名、密碼後,開始按照備庫 B 傳過來的位置,從本地讀取 binlog,發給 B。

4、備庫 B 拿到 binlog 後,寫到本地檔案,稱為中轉日志(relay log)。

5、sql_thread 讀取中轉日志,解析出日志裡的指令,并執行。

重點思考

需要注意的是,第3步,這裡說的主庫從本地讀取binlog,發給B,這裡是讀取的page cache還是disk裡面的呢?

對于A的線程來說,就是“讀檔案”
1. 如果這個檔案現在還在 page cache中,那就最好了,直接讀走;
2. 如果不在page cache裡,就隻好去磁盤讀。
這個行為是檔案系統控制的,MySQL隻是執行“讀檔案”這個操作      

sync_binlog=1的時候,表示每次事務送出都刷一次磁盤,也就是每次送出事務都會執行fsync,fsync其實很快的。可以了解為傳給備庫的binlog都是落盤的。

sync_binlog!=1的情況下,主庫的binlog傳輸到備庫的event是write之後就會傳過去,其實也就是主庫讀取os cache中的binlog event将其傳輸到備庫。

注:這裡的binlog write,指的就是指把日志寫入到檔案系統的 page cache,并沒有把資料持久化到磁盤,binlog落盤隻的是 fsync,才是将資料持久化到磁盤的操作。

sync_binlog=1分析

上面的案例中,sync_binlog已經配置為1,在這種設定下,主庫傳給備庫的binlog都是落盤的。如圖,主庫binlog wtrite後立即fsync落盤,傳輸給備庫,等待備庫傳回ACK。而在這個時候發生了主庫的crash。

會出現兩種情況:

真實案例:主備切換資料不一緻分析

(1)當主庫還沒來得及把日志傳輸到從庫上;主庫上在完成write binlog後crash

主庫Crash恢複後,這個事務操作資料可以被commit,這種事務可以稱為local commit或是幽靈事務,并沒有真正的完成半同步。就會出現上面所述案例中主庫比從庫多事務的情況。

這種情況下,原始的master故障恢複後,作為新master的從,1062錯誤很容易出現,因為主庫有事務沒有同步到從庫,而新主寫入很有可能與這個事務沖突。1032錯誤,就是我們遇到的這個,truncate後空表資料無法毆update操作。

是以對于after_sync複制,最好的做法是原始主庫故障後,可以對比一下最後一個GTID事務的内容

(2)日志已經傳輸到從庫上,完成了wait slave ack,此時發生crash;應用端此時并沒有接收到主庫傳回OK。

産生髒資料,是一個業務沒得到确認的事務。也可以稱為幽靈事務。

sync_binlog!=1分析

主機crash

主庫所在主機crash後,可能導緻主庫比備庫少一些gtid。在sync_binlog不等于1的情況下,在binlog還沒有sync到磁盤的時候,binlog event被同步到了從庫上。

binlog在寫檔案時先write,再sync。假設主庫在write binlog之後,sync

之前,同時備庫也拉取了這些未sync的binlog。此時主庫當機,主庫一部分 binlog

未落盤,但這部分binlog已經傳到了備庫,那麼備庫會比主庫多一些事務。是以主庫重新開機後,重新構造 gtid_executed_set

時會比備庫少一些gtid。

那些未sync的事務實際處于兩階段送出的prepare狀态,重新開機後這些處于prepare的事務由于沒有寫binlog會復原掉。

主機當機HA切換後,新主庫會比新備庫多一些事務。

而實際上新主庫會比新備庫多一些事務應該沒有影響,這些事務是使用者發出了commit指令,但主機crash了,沒有收到commit的回複,處于未知狀态。這些未決事務可以送出也可以復原!

對于以上情況,在binlog沒有purge的情況下,結合應用我們可以根據gtid來修複主備不一緻的情況,或復原備庫的修改,或者重做主庫丢失的事務。

總結:如何避免主從不一緻

​那麼使用複制如何保證資料的絕對一緻性呢?

1.複制一定是binlog row格式+gtid,同時在資料庫故障時,注意local commit問題,引入資料校驗機制。

 2. 複制環境絕對一緻性屬于僞命題,如果想要絕對的一緻目前可以考慮MySQL Group Replication。

 3. 如果一定要用複制架構,同時又要絕對的一緻性,考慮使用增強半同步after_sync結合session_track_gtids功能使用。

4. 複制推薦使用after_sync,同樣要求半同步不允許退化成為異步。

5. 深入了解複制的原理,避免不适當的操作造成複制一緻性: 大事務,較長DDL等操作。如果必須操作,可以考慮一些特殊的運維方式操作。

繼續閱讀