27 | 主庫出問題了,從庫怎麼辦?

在前面的第242526篇文章中,我和你介紹了MySQL主備複製的基礎結構,但這些都是一主一備的結構。mysql

大多數的互聯網應用場景都是讀多寫少,所以你負責的業務,在發展過程當中極可能先會遇到讀性能的問題。而在數據庫層解決讀性能問題,就要涉及到接下來兩篇文章要討論的架構:一主多從。sql

今天這篇文章,咱們就先聊聊一主多從的切換正確性。而後,咱們在下一篇文章中再聊聊解決一主多從的查詢邏輯正確性的方法。數據庫

如圖1所示,就是一個基本的一主多從結構。session

圖1 一主多從基本結構

圖中,虛線箭頭表示的是主備關係,也就是A和A’互爲主備, 從庫B、C、D指向的是主庫A。一主多從的設置,通常用於讀寫分離,主庫負責全部的寫入和一部分讀,其餘的讀請求則由從庫分擔。架構

今天咱們要討論的就是,在一主多從架構下,主庫故障後的主備切換問題。工具

如圖2所示,就是主庫發生故障,主備切換後的結果。性能

圖2 一主多從基本結構--主備切換

相比於一主一備的切換流程,一主多從結構在切換完成後,A’會成爲新的主庫,從庫B、C、D也要改接到A’。正是因爲多了從庫B、C、D從新指向的這個過程,因此主備切換的複雜性也相應增長了。ui

接下來,咱們再一塊兒看看一個切換系統會怎麼完成一主多從的主備切換過程。spa

基於位點的主備切換

這裏,咱們須要先來回顧一個知識點。線程

當咱們把節點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究竟是什麼意思,又是如何解決找同步位點這個問題呢?如今,我就和你簡單介紹一下。

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

GTID=server_uuid:gno

其中:

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

這裏我須要和你說明一下,在MySQL的官方文檔裏,GTID格式是這麼定義的:

GTID=source_id:transaction_id

這裏的source_id就是server_uuid;然後面的這個transaction_id,我以爲容易形成誤導,因此我改爲了gno。爲何說使用transaction_id容易形成誤解呢?

由於,在MySQL裏面咱們說transaction_id就是指事務id,事務id是在事務執行過程當中分配的,若是這個事務回滾了,事務id也會遞增,而gno是在事務提交的時候纔會分配。

從效果上看,GTID每每是連續的,所以咱們用gno來表示更容易理解。

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集合,用來對應「這個實例執行過的全部事務」。

這樣看上去不太容易理解,接下來我就用一個簡單的例子,來和你說明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的概念,再一塊兒來看看基於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協議。能夠看到,前面讓咱們頭疼不已的MASTER_LOG_FILE和MASTER_LOG_POS參數,已經不須要指定了。

咱們把如今這個時刻,實例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

接下來,我再舉個例子幫你理解GTID。

以前在第22篇文章《MySQL有哪些「飲鴆止渴」提升性能的方法?》中,我和你提到業務高峯期的慢查詢性能問題時,分析到若是是因爲索引缺失引發的性能問題,咱們能夠經過在線加索引來解決。可是,考慮到要避免新增索引對主庫性能形成的影響,咱們能夠先在備庫加索引,而後再切換。

當時我說,在雙M結構下,備庫執行的DDL語句也會傳給主庫,爲了不傳回後對主庫形成影響,要經過set sql_log_bin=off關掉binlog。

評論區有位同窗提出了一個問題:這樣操做的話,數據庫裏面是加了索引,可是binlog並無記錄下這一個更新,是否是會致使數據和日誌不一致?

這個問題提得很是好。當時,我在留言的回覆中就引用了GTID來講明。今天,我再和你展開說明一下。

假設,這兩個互爲主備關係的庫仍是實例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上執行這條更新。

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

小結

在今天這篇文章中,我先和你介紹了一主多從的主備切換流程。在這個過程當中,從庫找新主庫的位點是一個痛點。由此,咱們引出了MySQL 5.6版本引入的GTID模式,介紹了GTID的基本概念和用法。

能夠看到,在GTID模式下,一主多從切換就很是方便了。

所以,若是你使用的MySQL版本支持GTID的話,我都建議你儘可能使用GTID模式來作一主多從的切換。

在下一篇文章中,咱們還能看到GTID模式在讀寫分離場景的應用。

最後,又到了咱們的思考題時間。

你在GTID模式下設置主從關係的時候,從庫執行start slave命令後,主庫發現須要的binlog已經被刪除掉了,致使主備建立不成功。這種狀況下,你以爲能夠怎麼處理呢?

你能夠把你的方法寫在留言區,我會在下一篇文章的末尾和你討論這個問題。感謝你的收聽,也歡迎你把這篇文章分享給更多的朋友一塊兒閱讀。

上期問題時間

上一篇文章最後,我給你留的問題是,若是主庫都是單線程壓力模式,在從庫追主庫的過程當中,binlog-transaction-dependency-tracking 應該選用什麼參數?

這個問題的答案是,應該將這個參數設置爲WRITESET。

因爲主庫是單線程壓力模式,因此每一個事務的commit_id都不一樣,那麼設置爲COMMIT_ORDER模式的話,從庫也只能單線程執行。

一樣地,因爲WRITESET_SESSION模式要求在備庫應用日誌的時候,同一個線程的日誌必須與主庫上執行的前後順序相同,也會致使主庫單線程壓力模式下退化成單線程複製。

因此,應該將binlog-transaction-dependency-tracking 設置爲WRITESET。

相關文章
相關標籤/搜索