深入了解MySQL主從複製的原理

歡迎微信關注「SH的全棧筆記

0. 主從複製

首先主從複製是什麼?簡單來說是讓一台MySQL伺服器去複製另一台MySQL的數據,使兩個伺服器的數據保持一致。

這種方式與Redis的主從複製的思路沒有太大的出入。如果你對Redis的主從複製感興趣可以去看看《Redis的主從複製》。那既然Redis和MySQL都採用了複製這種方式,主從複製所帶來的意義是什麼呢?

通過複製功能,構建一個或者多個從庫,可以提高資料庫的高可用性可擴展性,同時實現負載均衡。當主庫發生故障時,可以快速的切到其某一個從庫,並將該從庫提升為主庫,因為數據都一樣,所以不會影響系統的運行;當MySQL伺服器需要扛住更多的讀請求時,可以把讀請求的流量分流到各個從庫上去,寫請求則轉發給主庫,形成讀寫分離的架構,來提供更好的讀擴展和請求的負載均衡。

讀寫分離的架構應用的其實非常廣泛,就比如MySQL,還有Redis,以及我們熟悉的Zookeeper,Zookeeper的Follower收到讀請求不會自己處理,而是會將讀請求轉發給Leader,感興趣的可以自己下來了解一下,這裡就不偏題了。

1. 複製原理

MySQL的主從複製支援兩種方式:

  • 基於
  • 基於語句

基於語句的複製在MySQL3.23中就已經有了,而基於語句的方式則在5.1中才實現。其本質都是基於主庫的binlog來實現的,主庫記錄binlog,然後從庫將binlog在自己的伺服器上重放,從而保證了主、從的數據一致性。

1.1 binlog

MySQL中日誌分為兩個維度,一個是MySQL伺服器的,一個是底層存儲引擎的。而上文提到的binlog就是屬於MySQL伺服器的日誌,binlog也叫二進位日誌,記錄了所有對MySQL所做的更改。

基於行、語句的複製方式跟binlog的存儲方式有關係。 binlog有三種存儲格式,分別是Statement、Row和Mixed。

  • Statement 基於語句,只記錄對數據做了修改的SQL語句,能夠有效的減少binlog的數據量,提高讀取、基於binlog重放的性能
  • Row 只記錄被修改的行,所以Row記錄的binlog日誌量一般來說會比Statement格式要多。基於Row的binlog日誌非常完整、清晰,記錄了所有數據的變動,但是缺點是可能會非常多,例如一條update語句,有可能是所有的數據都有修改;再例如alter table之類的,修改了某個欄位,同樣的每條記錄都有改動。
  • Mixed Statement和Row的結合,怎麼個結合法呢。例如像update或者alter table之類的語句修改,採用Statement格式。其餘的對數據的修改例如updatedelete採用Row格式進行記錄。

為什麼會有這麼多方式呢?因為Statement只會記錄SQL語句,但是並不能保證所有情況下這些語句在從庫上能夠正確的被重放出來。因為可能順序不對。

MySQL什麼時候會記錄binlog呢?是在事務提交的時候,並不是按照語句的執行順序來記錄,當記錄完binlog之後,就會通知底層的存儲引擎提交事務,所以有可能因為語句順序錯誤導致語句出錯。

1.2 查看binlog

這裡拿MySQL 5.6舉例子,binlog默認是處於關閉狀態的。我們可以通過命令show variables like '%log_bin%' 來查看關於binlog的配置。

默認配置
默認配置

log_bin代表是否開啟了binlog,其默認值為OFF

  • log_bin 代表是否開啟了binlog,其默認值為OFF
  • log_bin_basename binlog存儲文件的完整名稱,會在默認的文件名後面添加上遞增的序號,就例如mysql-bin.000001
  • log_bin_index binlog索引文件名稱,例如mysql-bin.index
  • sql_log_bin 在binlog開啟的時候,可以禁用當前session的binlog

你可以在MySQL中通過命令show binary logs查看所有的binlog文件

查看binlog
查看binlog

知道了有哪些文件之後我們可以來看看binlog文件中的內容,可以在MySQL通過show binlog events命令來查看。

show binglog events 查看第一個binlog文件,我們也可以通過in參數來指定,假設我們想看的文件名是mysql-bin.000001,那麼可以使用命令show binlog events in 'mysql-bin.000001'來查看指定的binlog文件

查看binlog
查看binlog

接下來我們來看看我們在MySQL中的操作所對應的binlog內容分別是什麼。

初始化

我們上面提到過,binlog是由一個一個的event組成的。從MySQL 5.0開始,binlog的第一個event都為Format_desc,位於圖中的Event_type那一列。可以看到內容為Server ver;5.6.50-log, Binlog ver: 4,說明當前使用的MySQL版本為5.6.50,Binlog的版本是V4。

創建資料庫

然後我創建了一個名為student的DB,其Event_type是Query,這個event的內容為CREATE DATABASE student DEFAULT CHARACTER SET = utf8mb4,一個建庫語句。

新建表

然後我創建了一個名為student的表,Event_type也是Query,內容為use student; CREATE TABLE student (id INT(11) UNSIGNED NOT NULL PRIMARY KEY AUTO_INCREMENT),一個建表語句。

插入數據

然後我們執行INSERT語句給該表插入兩行數據,再次查看binlog。

INSERT INTO `student` (`id``name`VALUES (NULL'張三');
INSERT INTO `student` (`id``name`VALUES (NULL'李四');
image-20210106123550397
image-20210106123550397

可以看到每次INSERT都會開啟一個事務,你可能會疑惑,我們只是簡單的執行了INSERT語句,沒有顯示的開啟事務。那為什麼會有事務產生呢?

這是因為MySQL採用了自動提交(AUTOCOMMIT)的機制,我使用的InnoDB存儲引擎,是支援事務的,所有的用戶活動都發生在事務中。我們可以通過show variables like '%AUTOCOMMIT%';命令查看,如果結果是ON則代表是開啟的。

1.3 複製的核心步驟

我們假設主庫已經開啟了binlog,並正常的記錄binlog。

首先從庫啟動I/O執行緒,跟主庫建立客戶端連接。

主庫啟動binlog dump執行緒,讀取主庫上的binlog event發送給從庫的I/O執行緒,I/O執行緒獲取到binlog event之後將其寫入到自己的Relay Log中。

然後從庫啟動SQL執行緒,將Relay中的數據進行重放,完成從庫的數據更新。

總結來說,主庫上只會有一個執行緒,而從庫上則會有兩個執行緒。

主從複製流程
主從複製流程

1.4 Relay Log

relay log其實和binlog沒有太大的區別,在MySQL 4.0 之前是沒有Relay Log這部分的,整個過程中只有兩個執行緒。但是這樣也帶來一個問題,那就是複製的過程需要同步的進行,很容易被影響,而且效率不高。例如主庫必須要等待從庫讀取完了才能發送下一個binlog事件。這就有點類似於一個阻塞的信道和非阻塞的信道。

阻塞信道
阻塞信道

阻塞信道就跟你在櫃檯一樣,你要遞歸櫃員一個東西,但是你和櫃員之間沒有可以放東西的地方,你就只能一直把文件拿著,直到櫃員接手;而非阻塞信道就像你們之間有個地方可以放文件,你就直接放上去就好了,不用等櫃員接手。

引入了Relay Log之後,讓原本同步的獲取事件、重放事件解耦了,兩個步驟可以非同步的進行,Relay Log充當了緩衝區的作用。Relay Log有一個relay-log.info的文件,用於記錄當前複製的進度,下一個事件從什麼Pos開始寫入,該文件由SQL執行緒負責更新。

1.5 Relay Log核心參數

接下來讓我們了解一下Relay Log的核心參數。

  • max_relay_log_size 中繼日誌的最大size,默認值0,如果為0就會取默認的size 1G,否則就為設置的值

  • relay_log 定義relay的名稱,默認為主機名+relay-bin,例如像hostname-relay-bin

  • relay_log_basename 中繼日誌的全路徑,即路徑 + 文件名,例如/path/to/hostname-relay-bin,最大長度為256

  • relay_log_index 定義中繼日誌的索引文件的全路徑,同樣其最大的長度為256. 其默認值為hostname + relay-bin.index,例如/path/to/hostname-relay-bin.index

  • relay_log_info_file 定義relay-log.info文件的名稱

  • relay_log_info_repository 存放relay log重放的數據的方式,可以設置為FILETABLE。FILE代表將中繼日誌重放的數據記錄在relay-info.log中,TABLE則將其存放在slave_relay_log_info這張表裡。

  • relay_log_purge 是否自動清空不需要的中繼日誌,默認值為ON

  • relay_log_recovery 當從庫宕機後,如果relay log損壞了導致部分的中繼日誌沒有進行同步,則自動放棄所有未進行重放的中繼日誌,並從主庫重新獲取,默認值為OFF

  • relay_log_space_limit 設置中繼日誌的最大值,防止寫滿磁碟。但是不建議設置這個值,建議還是給中繼日誌需要的空間,0就是不限制,0也是默認值

  • sync_relay_log 用於控制中繼日誌寫入磁碟的變數,假設值為n,那麼在中繼日誌每接受n次binlog事件之後就會調用fdatasync()函數將中繼日誌強制的刷入磁碟;相反,如果值為0,則寫入OS的緩衝區內,由OS調度決定何時將中繼日誌刷入磁碟,這樣一來如果在沒有刷入之前報錯了,那麼中繼日誌就會丟失。默認值是10000,也就是每向中繼日誌中寫入1w次binlog事件就將中繼日誌強制的刷入磁碟。

  • sync_relay_log_info 該參數的影響跟參數relay_log_info_repository有一定關係,同時也跟是否使用支援事務的存儲引擎有關係。該值默認也是10000.

    • sync_relay_log_info為0時

      • relay_log_info_repository為FILE,MySQL不會調用fdatasync(),而是將刷入磁碟的調度交給OS;
      • relay_log_info_repository為TABLE,如果使用了支援事務的存儲引擎,則每次事務的時候該表都會被更新;如果沒有使用事務引擎,則永遠不會被更新
    • sync_relay_log_info大於0時

      • relay_log_info_repository為FILE,假設設置的值為N,那麼每N次事務都會都會調用fdatasync()強制將relay-log.info刷入磁碟
      • relay_log_info_repository為TABLE,如果使用了支援事務的引擎,則該表每次事務結束都會被更新;如果沒有使用事務引擎則會在寫入N個binlog事件的時候更新該表。

2. 複製模型

平常的開發中,其實很少說一上來就直接搞主從架構的。費時間、費錢還引入了額外的複雜度,最後發現投入了這麼多一個單MySQL伺服器就完全能handle。

這就跟一個產品的架構迭代是一樣的,剛剛起步的時候一個單體應用足夠了。當你的業務擴展,請求膨脹,單體無法抗住壓力了,就會考慮開始部署多實例,開始採用微服務架構去做橫向擴展、負載均衡。

2.1 一主多從

當然你也可以把它當成一主一從

這是最簡單的模型,特別適合少量寫、大量讀的情況。讀請求被分到了各個從庫上,有效的幫主庫分散了壓力,能夠提升讀並發。當然,你也可以只是把從庫當成一個災備庫,除了主從複製之外,沒有其他任何的請求和數據傳輸。

甚至你可以把其中一個備庫作為你的預發環境的資料庫,當然,這說到底還是直接動了生產環境的資料庫,是一種過於理想的用途,因為這還涉及到生產環境資料庫的數據敏感性。不是所有人都能夠接觸到的,需要有完善的許可權機制。

MySQL一主多從
MySQL一主多從

值得注意的是,如果有n個從庫,那麼主庫上就會有n個binlog dump執行緒。如果這個n比較大的話在複製的時候可能會造成主庫的性能抖動。所以在從庫較多的情況下可以採用級聯複製。

2.2 級聯複製

級聯複製用大白話說就是套娃

本來從庫B、C、D、E、F、G都是複製的主庫A,但是現在由於A的壓力比較大,就不這麼幹了,調整成了如下的模式。

  • B、C複製A
  • D、E複製B
  • F、G複製C
MySQL級聯複製
MySQL級聯複製

這就叫級聯複製,開啟瘋狂套娃模式。你甚至會覺得這種套娃很眼熟,在Redis主從複製中也可以採用級聯模式, slave去複製另一個slave。

級聯複製的好處在於很大程度上減輕了主庫的壓力,主庫只需要關心與其有直接複製關係的從庫,剩下的複製則交給從庫即可。相反,由於是這種層層嵌套的關係,如果在較上層出現了錯誤,會影響到掛在該伺服器下的所有子庫,這些錯誤的影響效果被放大了。

2.3 主主複製

顧名思義,就是兩個主庫相互複製,客戶端可以對任意一台主庫進行寫操作。任何一台主庫伺服器上的數據發生了變化都會同步到另一台伺服器上去。有點類似於Eureka Server的雙節點模式,兩個註冊中心相互註冊。這樣一來,任何一台掛了都不會對系統產生影響。

而且主主複製可以打破資料庫性能瓶頸,一個很酷的功能——橫向擴展。為什麼說很酷呢,如果DB能做到橫向擴展,那很多被資料庫並發所限制的瓶頸都可以被突破,然而…

但是主主複製其實並不可靠,兩邊的數據衝突的可能性很大。例如複製停止了,系統仍然在向兩個主庫中寫入數據,也就是說一部分數據在A,另一部分的數據在B,但是沒有相互複製,且數據也不同步了。要修復這部分數據的難度就會變得相當大。

所以我認為雙主的更多的意義在於HA,而不是負載均衡。

2.4 主、被動的主主複製

同樣還是雙主的結構,但是區別在於其中一台是只讀的被動伺服器,客戶端不會向該庫進行寫操作。

其用途在哪裡呢?例如我們要在不中斷服務的前提下對MySQL進行維護、優化,舉個例子——修改表結構。假設我們有兩個資料庫,主庫A和被動主庫B,注意此處的被動主庫是只讀的,我們先停止A對B的複製,也就是停掉A上的SQL執行緒。

主主停止複製
主主停止複製

這樣一來,我們之後在B上執行的非常耗時、可能需要鎖表的操作就不會立即同步到A上來。因為此時A正在對外提供服務,所以不能使其收到影響,但是由於採用的是非同步的複製模式,所以Relay Log還是繼續由I/O執行緒寫入,只是不去進行重放。

然後我們在B上執行此次的維護操作,注意,此時A上面發生的更新還是會正常的同步到B來。執行完後交換讀寫的角色。也就是讓A變成只讀的被動主庫,而B變為主動主庫對外提供服務。

重新開啟SQL執行緒
重新開啟SQL執行緒

然後重新開啟SQL執行緒,A開始去對之前Relay Log中積累的event進行重放。雖然A此時可能會阻塞住,但是A已經沒有對外提供服務了,所以沒有問題。

主、被動下的主主模式的好處大家也就清楚了,可以在不停止服務的情況下去做資料庫的結構更新,其次可以在主庫發生故障的情況下,快速的切換,保證資料庫的HA。

3. 複製方式

上文我們不止一次的提到了複製是非同步的,接下來我們來了解一下MySQL的主從複製都有哪些方式。

3.1 非同步複製

首先就是非同步,這也是MySQL默認的方式。在非同步複製下,主庫不會主動的向從庫發送消息,而是等待從庫的I/O執行緒建立連接,然後主庫創建binlog dump執行緒,把binlog event發送給I/O執行緒,流程如下圖。

MySQL複製模式
MySQL複製模式

主庫在執行完自己的事務、記錄完binlog之後就會直接返回,不會與客戶端確認任何結果。然後後續由binlog dump執行緒非同步的讀取binlog,然後發送給從庫。處理請求主從複製是兩個完全非同步化的過程。

3.2 同步複製

同步模式則是,主庫執行一個事務,那麼主庫必須等待所有的從庫全部執行完事務返回commit之後才能給客戶端返回成功,

同步複製
同步複製

值得注意的是,主庫會直接提交事務,而不是等待所有從庫返回之後再提交。MySQL只是延遲了對客戶端的返回,並沒有延後事務的提交。

同步模式用腳趾頭想知道性能會大打折扣,它把客戶端的請求和主從複製耦合在了一起,如果有某個從庫複製執行緒執行的慢,那麼對客戶端的響應也會慢很多。

3.3 半同步複製

半同步相對於同步的區別在於,同步需要等待所有的從庫commit,而半同步只需要一個從庫commit就可以返回了。如果超過默認的時間仍然沒有從庫commit,就會切換為非同步模式再提交。客戶端也不會一直去等待了。

MySQL複製模式
MySQL複製模式

因為即使後面主庫宕機了,也能至少保證有一個從庫節點是可以用的,此外還減少了同步時的等待時間。

4. 複製中的數據一致性

我們在1.3中討論了複製的核心步驟,看似很簡單的一個流程,主庫的binlog dump去讀取binlog,然後從庫的I/O執行緒去讀取、寫入Relay Log,進而從庫的SQL執行緒再讀取Relay Log進行重放。

那如果I/O執行緒複製到一半自己突然掛掉了呢?又或者複製到一半主庫宕機了呢?如果和保證數據一致性的呢?

我們上面提到過,有一個relay-log.info的文件,用於記錄當前從庫正在複製的binlog和寫入的Relay Log的Pos,只要這個文件還在,那麼當從庫意外重啟之後,就會重新讀取文件,從上次複製的地方開始繼續複製。這就跟Redis中的主從複製類似,雙方要維護一個offset,通過對比offset,來進行psync增量數據同步。

但是在MySQL 5.5以及之前,都只能將複製的進度記錄在relog-log.info文件中。換句話說,參數relay_log_info_repository只支援FILE,可以再回到上面的1.5 Relay Log核心參數看一下。所以只有在sync_relay_log_info次事務之後才會把relay-log.info文件刷入磁碟。

如果在刷入磁碟之前從庫掛了,那麼重啟之後就會發現SQL執行緒實際執行到位置和資料庫記錄的不一致,數據一致性的問題就這麼產生了。

所以在MySQL 5.6時,參數relay_log_info_repository支援了TABLE,這樣一來我們就可以將複製的進度放在系統的mysql.slave_relay_log_info表裡去,並且把更新進度、SQL執行緒執行用戶事務綁定成一個事務執行。即使slave宕機了,我們也可以通過MySQL內建的崩潰恢復機制來使實際執行的位置和資料庫保存的進度恢復到一致。

其次還有上面提到的半同步複製,主庫會先提交事務,然後等待從庫的返回,再將結果返回給客戶端,但是如果在主庫等待的時候,從庫掛了呢?

此時主庫上由於事務已經提交了,但是從庫上卻沒有這個數據。所以在MySQL 5.7時引入了無損半同步複製,增加了參數rpl_semi_sync_master_wait_point的值,在MySQL 5.7中值默認為after_sync,在MySQL 5.6中默認值為after_commit

  • after_sync 主庫先不提交事務,等待某一個從庫返回了結果之後,再提交事務。這樣一來,如果從庫在沒有任何返回的情況下宕機了,master這邊也無法提交事務。主從仍然是一致的
  • after_commit 與之前討論的一樣,主庫先提交事務,等待從庫返回結果再通知客戶端

好了以上就是本篇部落格的全部內容了,如果你覺得這篇文章對你有幫助,還麻煩點個贊關個注分個享留個言

歡迎微信搜索關注【SH的全棧筆記】,查看更多相關文章

Tags: