25講MySQL是怎麼保證高可用的

在上一篇文章中,我和你介紹了binlog的基本內容,在一個主備關係中,每個備庫接收主庫的binlog並執行。

正常情況下,只要主庫執行更新生成的所有binlog,都可以傳到備庫並被正確地執行,備庫就能達到跟主庫一致的狀態,這就是最終一致性。

但是,MySQL要提供高可用能力,只有最終一致性是不夠的。爲什麼這麼說呢?今天我就着重和你分析一下。

這裏,我再放一次上一篇文章中講到的雙M結構的主備切換流程圖。
圖 1 MySQL主備切換流程--雙M結構

主備延遲

主備切換可能是一個主動運維動作,比如軟件升級、主庫所在機器按計劃下線等,也可能是被動操作,比如主庫所在機器掉電。

接下來,我們先一起看看主動切換的場景。

在介紹主動切換流程的詳細步驟之前,我要先跟你說明一個概念,即“同步延遲”。與數據同步有關的時間點主要包括以下三個:

  1. 主庫A執行完成一個事務,寫入binlog,我們把這個時刻記爲T1;

  2. 之後傳給備庫B,我們把備庫B接收完這個binlog的時刻記爲T2;

  3. 備庫B執行完成這個事務,我們把這個時刻記爲T3。

所謂主備延遲,就是同一個事務,在備庫執行完成的時間和主庫執行完成的時間之間的差值,也就是T3-T1。

你可以在備庫上執行show slave status命令,它的返回結果裏面會顯示seconds_behind_master,用於表示當前備庫延遲了多少秒。

seconds_behind_master的計算方法是這樣的:

  1. 每個事務的binlog 裏面都有一個時間字段,用於記錄主庫上寫入的時間;

  2. 備庫取出當前正在執行的事務的時間字段的值,計算它與當前系統時間的差值,得到seconds_behind_master。

可以看到,其實seconds_behind_master這個參數計算的就是T3-T1。所以,我們可以用seconds_behind_master來作爲主備延遲的值,這個值的時間精度是秒。

你可能會問,如果主備庫機器的系統時間設置不一致,會不會導致主備延遲的值不準?

其實不會的。因爲,備庫連接到主庫的時候,會通過執行SELECT UNIX_TIMESTAMP()函數來獲得當前主庫的系統時間。如果這時候發現主庫的系統時間與自己不一致,備庫在執行seconds_behind_master計算的時候會自動扣掉這個差值。

需要說明的是,在網絡正常的時候,日誌從主庫傳給備庫所需的時間是很短的,即T2-T1的值是非常小的。也就是說,網絡正常情況下,主備延遲的主要來源是備庫接收完binlog和執行完這個事務之間的時間差。

所以說,主備延遲最直接的表現是,備庫消費中轉日誌(relay log)的速度,比主庫生產binlog的速度要慢。接下來,我就和你一起分析下,這可能是由哪些原因導致的。

主備延遲的來源

首先,有些部署條件下,備庫所在機器的性能要比主庫所在的機器性能差。

一般情況下,有人這麼部署時的想法是,反正備庫沒有請求,所以可以用差一點兒的機器。或者,他們會把20個主庫放在4臺機器上,而把備庫集中在一臺機器上。

其實我們都知道,更新請求對IOPS的壓力,在主庫和備庫上是無差別的。所以,做這種部署時,一般都會將備庫設置爲“非雙1”的模式。

但實際上,更新過程中也會觸發大量的讀操作。所以,當備庫主機上的多個備庫都在爭搶資源的時候,就可能會導致主備延遲了。

當然,這種部署現在比較少了。因爲主備可能發生切換,備庫隨時可能變成主庫,所以主備庫選用相同規格的機器,並且做對稱部署,是現在比較常見的情況。

追問1:但是,做了對稱部署以後,還可能會有延遲。這是爲什麼呢?

這就是第二種常見的可能了,即備庫的壓力大。一般的想法是,主庫既然提供了寫能力,那麼備庫可以提供一些讀能力。或者一些運營後臺需要的分析語句,不能影響正常業務,所以只能在備庫上跑。

我真就見過不少這樣的情況。由於主庫直接影響業務,大家使用起來會比較剋制,反而忽視了備庫的壓力控制。結果就是,備庫上的查詢耗費了大量的CPU資源,影響了同步速度,造成主備延遲。

這種情況,我們一般可以這麼處理:

  1. 一主多從。除了備庫外,可以多接幾個從庫,讓這些從庫來分擔讀的壓力。

  2. 通過binlog輸出到外部系統,比如Hadoop這類系統,讓外部系統提供統計類查詢的能力。

其中,一主多從的方式大都會被採用。因爲作爲數據庫系統,還必須保證有定期全量備份的能力。而從庫,就很適合用來做備份。

備註:這裏需要說明一下,從庫和備庫在概念上其實差不多。在我們這個專欄裏,爲了方便描述,我把會在HA過程中被選成新主庫的,稱爲備庫,其他的稱爲從庫。

追問2:採用了一主多從,保證備庫的壓力不會超過主庫,還有什麼情況可能導致主備延遲嗎?

這就是第三種可能了,即大事務

大事務這種情況很好理解。因爲主庫上必須等事務執行完成纔會寫入binlog,再傳給備庫。所以,如果一個主庫上的語句執行10分鐘,那這個事務很可能就會導致從庫延遲10分鐘。

不知道你所在公司的DBA有沒有跟你這麼說過:不要一次性地用delete語句刪除太多數據。其實,這就是一個典型的大事務場景。

比如,一些歸檔類的數據,平時沒有注意刪除歷史數據,等到空間快滿了,業務開發人員要一次性地刪掉大量歷史數據。同時,又因爲要避免在高峯期操作會影響業務(至少有這個意識還是很不錯的),所以會在晚上執行這些大量數據的刪除操作。

結果,負責的DBA同學半夜就會收到延遲報警。然後,DBA團隊就要求你後續再刪除數據的時候,要控制每個事務刪除的數據量,分成多次刪除。

另一種典型的大事務場景,就是大表DDL。這個場景,我在前面的文章中介紹過。處理方案就是,計劃內的DDL,建議使用gh-ost方案(這裏,你可以再回顧下第13篇文章《爲什麼表數據刪掉一半,表文件大小不變?》中的相關內容)。

追問3:如果主庫上也不做大事務了,還有什麼原因會導致主備延遲嗎?

造成主備延遲還有一個大方向的原因,就是備庫的並行複製能力。這個話題,我會留在下一篇文章再和你詳細介紹。

其實還是有不少其他情況會導致主備延遲,如果你還碰到過其他場景,歡迎你在評論區給我留言,我來和你一起分析、討論。

由於主備延遲的存在,所以在主備切換的時候,就相應的有不同的策略。

可靠性優先策略

在圖1的雙M結構下,從狀態1到狀態2切換的詳細過程是這樣的:

  1. 判斷備庫B現在的seconds_behind_master,如果小於某個值(比如5秒)繼續下一步,否則持續重試這一步;

  2. 把主庫A改成只讀狀態,即把readonly設置爲true;

  3. 判斷備庫B的seconds_behind_master的值,直到這個值變成0爲止;

  4. 把備庫B改成可讀寫狀態,也就是把readonly 設置爲false;

  5. 把業務請求切到備庫B。

這個切換流程,一般是由專門的HA系統來完成的,我們暫時稱之爲可靠性優先流程。
圖2 MySQL可靠性優先主備切換流程
備註:圖中的SBM,是seconds_behind_master參數的簡寫。

可以看到,這個切換流程中是有不可用時間的。因爲在步驟2之後,主庫A和備庫B都處於readonly狀態,也就是說這時系統處於不可寫狀態,直到步驟5完成後才能恢復。

在這個不可用狀態中,比較耗費時間的是步驟3,可能需要耗費好幾秒的時間。這也是爲什麼需要在步驟1先做判斷,確保seconds_behind_master的值足夠小。

試想如果一開始主備延遲就長達30分鐘,而不先做判斷直接切換的話,系統的不可用時間就會長達30分鐘,這種情況一般業務都是不可接受的。

當然,系統的不可用時間,是由這個數據可靠性優先的策略決定的。你也可以選擇可用性優先的策略,來把這個不可用時間幾乎降爲0。

可用性優先策略

如果我強行把步驟4、5調整到最開始執行,也就是說不等主備數據同步,直接把連接切到備庫B,並且讓備庫B可以讀寫,那麼系統幾乎就沒有不可用時間了。

我們把這個切換流程,暫時稱作可用性優先流程。這個切換流程的代價,就是可能出現數據不一致的情況。

接下來,我就和你分享一個可用性優先流程產生數據不一致的例子。假設有一個表 t:

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

insert into t(c) values(1),(2),(3);

這個表定義了一個自增主鍵id,初始化數據後,主庫和備庫上都是3行數據。接下來,業務人員要繼續在表t上執行兩條插入語句的命令,依次是:

insert into t© values(4);
insert into t© values(5);
假設,現在主庫上其他的數據表有大量的更新,導致主備延遲達到5秒。在插入一條c=4的語句後,發起了主備切換。

圖3是可用性優先策略,且binlog_format=mixed時的切換流程和數據結果
圖3 可用性優先策略,且binlog_format=mixed
現在,我們一起分析下這個切換流程:

  1. 步驟2中,主庫A執行完insert語句,插入了一行數據(4,4),之後開始進行主備切換。

  2. 步驟3中,由於主備之間有5秒的延遲,所以備庫B還沒來得及應用“插入c=4”這個中轉日誌,就開始接收客戶端“插入 c=5”的命令。

  3. 步驟4中,備庫B插入了一行數據(4,5),並且把這個binlog發給主庫A。

  4. 步驟5中,備庫B執行“插入c=4”這個中轉日誌,插入了一行數據(5,4)。而直接在備庫B執行的“插入c=5”這個語句,傳到主庫A,就插入了一行新數據(5,5)。

最後的結果就是,主庫A和備庫B上出現了兩行不一致的數據。可以看到,這個數據不一致,是由可用性優先流程導致的。

那麼,如果我還是用可用性優先策略,但設置binlog_format=row,情況又會怎樣呢?

因爲row格式在記錄binlog的時候,會記錄新插入的行的所有字段值,所以最後只會有一行不一致。而且,兩邊的主備同步的應用線程會報錯duplicate key error並停止。也就是說,這種情況下,備庫B的(5,4)和主庫A的(5,5)這兩行數據,都不會被對方執行。

圖4中我畫出了詳細過程,你可以自己再分析一下。
圖4 可用性優先策略,且binlog_format=row
從上面的分析中,你可以看到一些結論:

  1. 使用row格式的binlog時,數據不一致的問題更容易被發現。而使用mixed或者statement格式的binlog時,數據很可能悄悄地就不一致了。如果你過了很久才發現數據不一致的問題,很可能這時的數據不一致已經不可查,或者連帶造成了更多的數據邏輯不一致。

  2. 主備切換的可用性優先策略會導致數據不一致。因此,大多數情況下,我都建議你使用可靠性優先策略。畢竟對數據服務來說的話,數據的可靠性一般還是要優於可用性的。

但事無絕對,有沒有哪種情況數據的可用性優先級更高呢?

答案是,有的。

我曾經碰到過這樣的一個場景:

  1. 有一個庫的作用是記錄操作日誌。這時候,如果數據不一致可以通過binlog來修補,而這個短暫的不一致也不會引發業務問題。
  2. 同時,業務系統依賴於這個日誌寫入邏輯,如果這個庫不可寫,會導致線上的業務操作無法執行。

這時候,你可能就需要選擇先強行切換,事後再補數據的策略。

當然,事後覆盤的時候,我們想到了一個改進措施就是,讓業務邏輯不要依賴於這類日誌的寫入。也就是說,日誌寫入這個邏輯模塊應該可以降級,比如寫到本地文件,或者寫到另外一個臨時庫裏面。

這樣的話,這種場景就又可以使用可靠性優先策略了。

接下來我們再看看,按照可靠性優先的思路,異常切換會是什麼效果

假設,主庫A和備庫B間的主備延遲是30分鐘,這時候主庫A掉電了,HA系統要切換B作爲主庫。我們在主動切換的時候,可以等到主備延遲小於5秒的時候再啓動切換,但這時候已經別無選擇了。
圖5 可靠性優先策略,主庫不可用
採用可靠性優先策略的話,你就必須得等到備庫B的seconds_behind_master=0之後,才能切換。但現在的情況比剛剛更嚴重,並不是系統只讀、不可寫的問題了,而是系統處於完全不可用的狀態。因爲,主庫A掉電後,我們的連接還沒有切到備庫B。

你可能會問,那能不能直接切換到備庫B,但是保持B只讀呢?

這樣也不行。

因爲,這段時間內,中轉日誌還沒有應用完成,如果直接發起主備切換,客戶端查詢看不到之前執行完成的事務,會認爲有“數據丟失”。

雖然隨着中轉日誌的繼續應用,這些數據會恢復回來,但是對於一些業務來說,查詢到“暫時丟失數據的狀態”也是不能被接受的。

聊到這裏你就知道了,在滿足數據可靠性的前提下,MySQL高可用系統的可用性,是依賴於主備延遲的。延遲的時間越小,在主庫故障的時候,服務恢復需要的時間就越短,可用性就越高。

小結

今天這篇文章,我先和你介紹了MySQL高可用系統的基礎,就是主備切換邏輯。緊接着,我又和你討論了幾種會導致主備延遲的情況,以及相應的改進方向。

然後,由於主備延遲的存在,切換策略就有不同的選擇。所以,我又和你一起分析了可靠性優先和可用性優先策略的區別。

在實際的應用中,我更建議使用可靠性優先的策略。畢竟保證數據準確,應該是數據庫服務的底線。在這個基礎上,通過減少主備延遲,提升系統的可用性。

最後,我給你留下一個思考題吧。

一般現在的數據庫運維繫統都有備庫延遲監控,其實就是在備庫上執行 show slave status,採集seconds_behind_master的值。

假設,現在你看到你維護的一個備庫,它的延遲監控的圖像類似圖6,是一個45°斜向上的線段,你覺得可能是什麼原因導致呢?你又會怎麼去確認這個原因呢?
圖6 備庫延遲
你可以把你的分析寫在評論區,我會在下一篇文章的末尾跟你討論這個問題。感謝你的收聽,也歡迎你把這篇文章分享給更多的朋友一起閱讀。

上期問題時間

上期我留給你的問題是,什麼情況下雙M結構會出現循環複製。

一種場景是,在一個主庫更新事務後,用命令set global server_id=x修改了server_id。等日誌再傳回來的時候,發現server_id跟自己的server_id不同,就只能執行了。

另一種場景是,有三個節點的時候,如圖7所示,trx1是在節點 B執行的,因此binlog上的server_id就是B,binlog傳給節點 A,然後A和A’搭建了雙M結構,就會出現循環複製。

圖7 三節點循環複製
這種三節點複製的場景,做數據庫遷移的時候會出現。

如果出現了循環複製,可以在A或者A’上,執行如下命令:

stop slave;
CHANGE MASTER TO IGNORE_SERVER_IDS=(server_id_of_B);
start slave;

這樣這個節點收到日誌後就不會再執行。過一段時間後,再執行下面的命令把這個值改回來。

stop slave;
CHANGE MASTER TO IGNORE_SERVER_IDS=();
start slave;

評論區留言點贊板:

@一大隻、@HuaMax 同學提到了第一個復現方法;

@Jonh同學提到了IGNORE_SERVER_IDS這個解決方法;

@React 提到,如果主備設置不同的步長,備庫是不是可以設置爲可讀寫。我的建議是,只要這個節點設計內就不會有業務直接在上面執行更新,就建議設置爲readonly。

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