MySQL實戰45講學習筆記----一主多從主備切換問題

在一主多從架構下,主庫故障後的主備切換問題

    

                                   圖1 一主多從基本結構                                                                  圖2主庫發生故障,主備切換後的結果

圖中,虛線箭頭表示的是主備關係,也就是A和A’互爲主備, 從庫B、C、D指向的是主庫A。一主多從的設置,一般用於讀寫分離,主庫負責所有的寫入和一部分讀,其他的讀請求則由從庫分擔。在一主多從架構下,主庫故障後的主備切換問題。相比於一主一備的切換流程,一主多從結構在切換完成後,A’會成爲新的主庫,從庫B、C、D也要改接到A’。正是由於多了從庫B、C、D重新指向的這個過程,所以主備切換的複雜性也相應增加了。

基於位點的主備切換

當我們把節點B設置成節點A’的從庫的時候,需要執行一條change master命令:

CHANGE MASTER TO 
MASTER_HOST=$host_name 
MASTER_PORT=$port 
MASTER_USER=$user_name 
MASTER_PASSWORD=$password 
MASTER_LOG_FILE=$master_log_name 
MASTER_LOG_POS=$master_log_pos  

這條命令有這麼6個參數:

  • MASTER_HOST、MASTER_PORT、MASTER_USER和MASTER_PASSWORD四個參數,分別代表了主庫A’的IP、端口、用戶名和密碼。
  • 最後兩個參數MASTER_LOG_FILE和MASTER_LOG_POS表示,要從主庫的master_log_name文件的master_log_pos這個位置的日誌繼續同步。而這個位置就是我們所說的同步位點,也就是主庫對應的文件名和日誌偏移量

節點B要設置成A’的從庫,就要執行change master命令,就不可避免地要設置位點的這兩個參數,但是這兩個參數到底應該怎麼設置呢?

原來節點B是A的從庫,本地記錄的也是A的位點。但是相同的日誌,A的位點和A’的位點是不同的。因此,從庫B要切換的時候,就需要先經過“找同步位點”這個邏輯。

考慮到切換過程中不能丟數據,所以我們找位點的時候,總是要找一個“稍微往前”的,然後再通過判斷跳過那些在從庫B上已經執行過的事務。

一種取同步位點的方法是這樣的:

  1. 等待新主庫A’把中轉日誌(relay log)全部同步完成;

  2. 在A’上執行show master status命令,得到當前A’上最新的File 和 Position;

  3. 取原主庫A故障的時刻T;

  4. 用mysqlbinlog工具解析A’的File,得到T時刻的位點。

mysqlbinlog File --stop-datetime=T --start-datetime=T

圖3 mysqlbinlog 部分輸出結果

圖中,end_log_pos後面的值“123”,表示的就是A’這個實例,在T時刻寫入新的binlog的位置。然後,我們就可以把123這個值作爲$master_log_pos ,用在節點B的change master命令裏。

當然這個值並不精確。假設在T這個時刻,主庫A已經執行完成了一個insert 語句插入了一行數據R,並且已經將binlog傳給了A’和B,然後在傳完的瞬間主庫A的主機就掉電了。

那麼,這時候系統的狀態是這樣的:

  1. 在從庫B上,由於同步了binlog, R這一行已經存在;

  2. 在新主庫A’上, R這一行也已經存在,日誌是寫在123這個位置之後的;

  3. 我們在從庫B上執行change master命令,指向A’的File文件的123位置,就會把插入R這一行數據的binlog又同步到從庫B去執行。

這時候,從庫B的同步線程就會報告 Duplicate entry ‘id_of_R’ for key ‘PRIMARY’ 錯誤,提示出現了主鍵衝突,然後停止同步。

所以,通常情況下,我們在切換任務的時候,要先主動跳過這些錯誤,有兩種常用的方法。

一種做法是,主動跳過一個事務。跳過命令的寫法是:

set global sql_slave_skip_counter=1;
start slave;

因爲切換過程中,可能會不止重複執行一個事務,所以我們需要在從庫B剛開始接到新主庫A’時,持續觀察,每次碰到這些錯誤就停下來,執行一次跳過命令,直到不再出現停下來的情況,以此來跳過可能涉及的所有事務。

另外一種方式是,通過設置slave_skip_errors參數,直接設置跳過指定的錯誤。

在執行主備切換時,有這麼兩類錯誤,是經常會遇到的:

  • 1062錯誤是插入數據時唯一鍵衝突;
  • 1032錯誤是刪除數據時找不到行。

因此,我們可以把slave_skip_errors 設置爲 “1032,1062”,這樣中間碰到這兩個錯誤時就直接跳過。

這裏需要注意的是,這種直接跳過指定錯誤的方法,針對的是主備切換時,由於找不到精確的同步位點,所以只能採用這種方法來創建從庫和新主庫的主備關係。這個背景是,我們很清楚在主備切換過程中,直接跳過1032和1062這兩類錯誤是無損的,所以纔可以這麼設置slave_skip_errors參數。等到主備間的同步關係建立完成,並穩定執行一段時間之後,我們還需要把這個參數設置爲空,以免之後真的出現了主從數據不一致,也跳過了。

GTID

通過sql_slave_skip_counter跳過事務和通過slave_skip_errors忽略錯誤的方法,雖然都最終可以建立從庫B和新主庫A’的主備關係,但這兩種操作都很複雜,而且容易出錯。所以,MySQL 5.6版本引入了GTID,徹底解決了這個困難。

GTID的全稱是Global Transaction Identifier,也就是全局事務ID,是一個事務在提交的時候生成的,是這個事務的唯一標識。它由兩部分組成,格式是:

GTID=server_uuid:gno

其中:

  • server_uuid是一個實例第一次啓動時自動生成的,是一個全局唯一的值;
  • gno是一個整數,初始值是1,每次提交事務的時候分配給這個事務,並加1。

GTID模式的啓動也很簡單,我們只需要在啓動一個MySQL實例的時候,加上參數gtid_mode=on和enforce_gtid_consistency=on就可以了。

在GTID模式下,每個事務都會跟一個GTID一一對應。這個GTID有兩種生成方式,而使用哪種方式取決於session變量gtid_next的值。

  1. 如果gtid_next=automatic,代表使用默認值。這時,MySQL就會把server_uuid:gno分配給這個事務。
    a. 記錄binlog的時候,先記錄一行 SET @@SESSION.GTID_NEXT=‘server_uuid:gno’;
    b. 把這個GTID加入本實例的GTID集合。

  2. 如果gtid_next是一個指定的GTID的值,比如通過set gtid_next='current_gtid’指定爲current_gtid,那麼就有兩種可能:
    a. 如果current_gtid已經存在於實例的GTID集合中,接下來執行的這個事務會直接被系統忽略;
    b. 如果current_gtid沒有存在於實例的GTID集合中,就將這個current_gtid分配給接下來要執行的事務,也就是說系統不需要給這個事務生成新的GTID,因此gno也不用加1。

注意,一個current_gtid只能給一個事務使用。這個事務提交後,如果要執行下一個事務,就要執行set 命令,把gtid_next設置成另外一個gtid或者automatic。這樣,每個MySQL實例都維護了一個GTID集合,用來對應“這個實例執行過的所有事務”。

在實例X中創建一個表t。

CREATE TABLE `t` (
  `id` int(11) NOT NULL,
  `c` int(11) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB;

insert into t values(1,1);

圖4 初始化數據的binlog

可以看到,事務的BEGIN之前有一條SET @@SESSION.GTID_NEXT命令。這時,如果實例X有從庫,那麼將CREATE TABLE和insert語句的binlog同步過去執行的話,執行事務之前就會先執行這兩個SET命令, 這樣被加入從庫的GTID集合的,就是圖中的這兩個GTID。

假設,現在這個實例X是另外一個實例Y的從庫,並且此時在實例Y上執行了下面這條插入語句:

insert into t values(1,1);

並且,這條語句在實例Y上的GTID是 “aaaaaaaa-cccc-dddd-eeee-ffffffffffff:10”。

那麼,實例X作爲Y的從庫,就要同步這個事務過來執行,顯然會出現主鍵衝突,導致實例X的同步線程停止。這時,我們應該怎麼處理呢?

處理方法就是,你可以執行下面的這個語句序列:

set gtid_next='aaaaaaaa-cccc-dddd-eeee-ffffffffffff:10';
begin;
commit;
set gtid_next=automatic;
start slave;

其中,前三條語句的作用,是通過提交一個空事務,把這個GTID加到實例X的GTID集合中。如圖5所示,就是執行完這個空事務之後的show master status的結果。

圖5 show master status結果

可以看到實例X的Executed_Gtid_set裏面,已經加入了這個GTID。

這樣再執行start slave命令讓同步線程執行起來的時候,雖然實例X上還是會繼續執行實例Y傳過來的事務,但是由於“aaaaaaaa-cccc-dddd-eeee-ffffffffffff:10”已經存在於實例X的GTID集合中了,所以實例X就會直接跳過這個事務,也就不會再出現主鍵衝突的錯誤。

在上面的這個語句序列中,start slave命令之前還有一句set gtid_next=automatic。這句話的作用是“恢復GTID的默認分配行爲”,也就是說如果之後有新的事務再執行,就還是按照原來的分配方式,繼續分配gno=3。

基於GTID的主備切換

在GTID模式下,備庫B要設置爲新主庫A’的從庫的語法如下:

CHANGE MASTER TO 
MASTER_HOST=$host_name 
MASTER_PORT=$port 
MASTER_USER=$user_name 
MASTER_PASSWORD=$password 
master_auto_position=1 

其中,master_auto_position=1就表示這個主備關係使用的是GTID協議。

把現在這個時刻,實例A’的GTID集合記爲set_a,實例B的GTID集合記爲set_b。接下來,我們就看看現在的主備切換邏輯。

我們在實例B上執行start slave命令,取binlog的邏輯是這樣的:

  1. 實例B指定主庫A’,基於主備協議建立連接。

  2. 實例B把set_b發給主庫A’。

  3. 實例A’算出set_a與set_b的差集,也就是所有存在於set_a,但是不存在於set_b的GITD的集合,判斷A’本地是否包含了這個差集需要的所有binlog事務。
    a. 如果不包含,表示A’已經把實例B需要的binlog給刪掉了,直接返回錯誤;
    b. 如果確認全部包含,A’從自己的binlog文件裏面,找出第一個不在set_b的事務,發給B;

  4. 之後就從這個事務開始,往後讀文件,按順序取binlog發給B去執行。

其實,這個邏輯裏面包含了一個設計思想:在基於GTID的主備關係裏,系統認爲只要建立主備關係,就必須保證主庫發給備庫的日誌是完整的。因此,如果實例B需要的日誌已經不存在,A’就拒絕把日誌發給B。

這跟基於位點的主備協議不同。基於位點的協議,是由備庫決定的,備庫指定哪個位點,主庫就發哪個位點,不做日誌的完整性判斷。

基於上面的介紹,我們再來看看引入GTID後,一主多從的切換場景下,主備切換是如何實現的。

由於不需要找位點了,所以從庫B、C、D只需要分別執行change master命令指向實例A’即可。

其實,嚴謹地說,主備切換不是不需要找位點了,而是找位點這個工作,在實例A’內部就已經自動完成了。但由於這個工作是自動的,所以對HA系統的開發人員來說,非常友好。

之後這個系統就由新主庫A’寫入,主庫A’的自己生成的binlog中的GTID集合格式是:server_uuid_of_A’:1-M。

如果之前從庫B的GTID集合格式是 server_uuid_of_A:1-N, 那麼切換之後GTID集合的格式就變成了server_uuid_of_A:1-N, server_uuid_of_A’:1-M。

當然,主庫A’之前也是A的備庫,因此主庫A’和從庫B的GTID集合是一樣的。這就達到了我們預期。

GTID和在線DDL

在雙M結構下,備庫執行的DDL語句也會傳給主庫,爲了避免傳回後對主庫造成影響,要通過set sql_log_bin=off關掉binlog。這樣操作的話,數據庫裏面是加了索引,但是binlog並沒有記錄下這一個更新,是不是會導致數據和日誌不一致?

假設,這兩個互爲主備關係的庫還是實例X和實例Y,且當前主庫是X,並且都打開了GTID模式。這時的主備切換流程可以變成下面這樣:

  • 在實例X上執行stop slave。

  • 在實例Y上執行DDL語句。注意,這裏並不需要關閉binlog。

  • 執行完成後,查出這個DDL語句對應的GTID,並記爲 server_uuid_of_Y:gno。

  • 到實例X上執行以下語句序列:

set GTID_NEXT="server_uuid_of_Y:gno";
begin;
commit;
set gtid_next=automatic;
start slave;

這樣做的目的在於,既可以讓實例Y的更新有binlog記錄,同時也可以確保不會在實例X上執行這條更新。

  • 接下來,執行完主備切換,然後照着上述流程再執行一遍即可。

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章