1. 程式人生 > 其它 >mysql_26 _ 備庫為什麼會延遲好幾個小時

mysql_26 _ 備庫為什麼會延遲好幾個小時

在上一篇文章中,我和你介紹了幾種可能導致備庫延遲的原因。你會發現,這些場景裡,不論是偶發性的查詢壓力,還是備份,對備庫延遲的影響一般是分鐘級的,而且在備庫恢復正常以後都能夠追上來。

但是,如果備庫執行日誌的速度持續低於主庫生成日誌的速度,那這個延遲就有可能成了小時級別。而且對於一個壓力持續比較高的主庫來說,備庫很可能永遠都追不上主庫的節奏。

這就涉及到今天我要給你介紹的話題:備庫並行複製能力。

為了便於你理解,我們再一起看一下第24篇文章《MySQL是怎麼保證主備一致的?》的主備流程圖。

圖1 主備流程圖

談到主備的並行複製能力,我們要關注的是圖中黑色的兩個箭頭。一個箭頭代表了客戶端寫入主庫,另一箭頭代表的是備庫上sql_thread執行中轉日誌(relay log)。如果用箭頭的粗細來代表並行度的話,那麼真實情況就如圖1所示,第一個箭頭要明顯粗於第二個箭頭。

在主庫上,影響併發度的原因就是各種鎖了。由於InnoDB引擎支援行鎖,除了所有併發事務都在更新同一行(熱點行)這種極端場景外,它對業務併發度的支援還是很友好的。所以,你在效能測試的時候會發現,併發壓測執行緒32就比單執行緒時,總體吞吐量高。

而日誌在備庫上的執行,就是圖中備庫上sql_thread更新資料(DATA)的邏輯。如果是用單執行緒的話,就會導致備庫應用日誌不夠快,造成主備延遲。

在官方的5.6版本之前,MySQL只支援單執行緒複製,由此在主庫併發高、TPS高時就會出現嚴重的主備延遲問題。

從單執行緒複製到最新版本的多執行緒複製,中間的演化經歷了好幾個版本。接下來,我就跟你說說MySQL多執行緒複製的演進過程。

其實說到底,所有的多執行緒複製機制,都是要把圖1中只有一個執行緒的sql_thread,拆成多個執行緒,也就是都符合下面的這個模型:

圖2 多執行緒模型

圖2中,coordinator就是原來的sql_thread, 不過現在它不再直接更新資料了,只負責讀取中轉日誌和分發事務。真正更新日誌的,變成了worker執行緒。而work執行緒的個數,就是由引數slave_parallel_workers決定的。根據我的經驗,把這個值設定為8~16之間最好(32核物理機的情況),畢竟備庫還有可能要提供讀查詢,不能把CPU都吃光了。

接下來,你需要先思考一個問題:事務能不能按照輪詢的方式分發給各個worker,也就是第一個事務分給worker_1,第二個事務發給worker_2呢?

其實是不行的。因為,事務被分發給worker以後,不同的worker就獨立執行了。但是,由於CPU的排程策略,很可能第二個事務最終比第一個事務先執行。而如果這時候剛好這兩個事務更新的是同一行,也就意味著,同一行上的兩個事務,在主庫和備庫上的執行順序相反,會導致主備不一致的問題。

接下來,請你再設想一下另外一個問題:同一個事務的多個更新語句,能不能分給不同的worker來執行呢?

答案是,也不行。舉個例子,一個事務更新了表t1和表t2中的各一行,如果這兩條更新語句被分到不同worker的話,雖然最終的結果是主備一致的,但如果表t1執行完成的瞬間,備庫上有一個查詢,就會看到這個事務“更新了一半的結果”,破壞了事務邏輯的隔離性。

所以,coordinator在分發的時候,需要滿足以下這兩個基本要求:

  1. 不能造成更新覆蓋。這就要求更新同一行的兩個事務,必須被分發到同一個worker中。

  2. 同一個事務不能被拆開,必須放到同一個worker中。

各個版本的多執行緒複製,都遵循了這兩條基本原則。接下來,我們就看看各個版本的並行複製策略。

MySQL 5.5版本的並行複製策略

官方MySQL 5.5版本是不支援並行複製的。但是,在2012年的時候,我自己服務的業務出現了嚴重的主備延遲,原因就是備庫只有單執行緒複製。然後,我就先後寫了兩個版本的並行策略。

這裡,我給你介紹一下這兩個版本的並行策略,即按表分發策略和按行分發策略,以幫助你理解MySQL官方版本並行複製策略的迭代。

按表分發策略

按表分發事務的基本思路是,如果兩個事務更新不同的表,它們就可以並行。因為資料是儲存在表裡的,所以按表分發,可以保證兩個worker不會更新同一行。

當然,如果有跨表的事務,還是要把兩張表放在一起考慮的。如圖3所示,就是按表分發的規則。

圖3 按表並行複製程模型

可以看到,每個worker執行緒對應一個hash表,用於儲存當前正在這個worker的“執行佇列”裡的事務所涉及的表。hash表的key是“庫名.表名”,value是一個數字,表示佇列中有多少個事務修改這個表。

在有事務分配給worker時,事務裡面涉及的表會被加到對應的hash表中。worker執行完成後,這個表會被從hash表中去掉。

圖3中,hash_table_1表示,現在worker_1的“待執行事務佇列”裡,有4個事務涉及到db1.t1表,有1個事務涉及到db2.t2表;hash_table_2表示,現在worker_2中有一個事務會更新到表t3的資料。

假設在圖中的情況下,coordinator從中轉日誌中讀入一個新事務T,這個事務修改的行涉及到表t1和t3。

現在我們用事務T的分配流程,來看一下分配規則。

  1. 由於事務T中涉及修改表t1,而worker_1佇列中有事務在修改表t1,事務T和佇列中的某個事務要修改同一個表的資料,這種情況我們說事務T和worker_1是衝突的。

  2. 按照這個邏輯,順序判斷事務T和每個worker佇列的衝突關係,會發現事務T跟worker_2也衝突。

  3. 事務T跟多於一個worker衝突,coordinator執行緒就進入等待。

  4. 每個worker繼續執行,同時修改hash_table。假設hash_table_2裡面涉及到修改表t3的事務先執行完成,就會從hash_table_2中把db1.t3這一項去掉。

  5. 這樣coordinator會發現跟事務T衝突的worker只有worker_1了,因此就把它分配給worker_1。

  6. coordinator繼續讀下一個中轉日誌,繼續分配事務。

也就是說,每個事務在分發的時候,跟所有worker的衝突關係包括以下三種情況:

  1. 如果跟所有worker都不衝突,coordinator執行緒就會把這個事務分配給最空閒的woker;

  2. 如果跟多於一個worker衝突,coordinator執行緒就進入等待狀態,直到和這個事務存在衝突關係的worker只剩下1個;

  3. 如果只跟一個worker衝突,coordinator執行緒就會把這個事務分配給這個存在衝突關係的worker。

這個按表分發的方案,在多個表負載均勻的場景裡應用效果很好。但是,如果碰到熱點表,比如所有的更新事務都會涉及到某一個表的時候,所有事務都會被分配到同一個worker中,就變成單執行緒複製了。

按行分發策略

要解決熱點表的並行複製問題,就需要一個按行並行複製的方案。按行復制的核心思路是:如果兩個事務沒有更新相同的行,它們在備庫上可以並行執行。顯然,這個模式要求binlog格式必須是row。

這時候,我們判斷一個事務T和worker是否衝突,用的就規則就不是“修改同一個表”,而是“修改同一行”。

按行復制和按表複製的資料結構差不多,也是為每個worker,分配一個hash表。只是要實現按行分發,這時候的key,就必須是“庫名+表名+唯一鍵的值”。

但是,這個“唯一鍵”只有主鍵id還是不夠的,我們還需要考慮下面這種場景,表t1中除了主鍵,還有唯一索引a:

CREATE TABLE `t1` (
  `id` int(11) NOT NULL,
  `a` int(11) DEFAULT NULL,
  `b` int(11) DEFAULT NULL,
  PRIMARY KEY (`id`),
  UNIQUE KEY `a` (`a`)
) ENGINE=InnoDB;

insert into t1 values(1,1,1),(2,2,2),(3,3,3),(4,4,4),(5,5,5);

假設,接下來我們要在主庫執行這兩個事務:

圖4 唯一鍵衝突示例

可以看到,這兩個事務要更新的行的主鍵值不同,但是如果它們被分到不同的worker,就有可能session B的語句先執行。這時候id=1的行的a的值還是1,就會報唯一鍵衝突。

因此,基於行的策略,事務hash表中還需要考慮唯一鍵,即key應該是“庫名+表名+索引a的名字+a的值”。

比如,在上面這個例子中,我要在表t1上執行update t1 set a=1 where id=2語句,在binlog裡面記錄了整行的資料修改前各個欄位的值,和修改後各個欄位的值。

因此,coordinator在解析這個語句的binlog的時候,這個事務的hash表就有三個項:

  1. key=hash_func(db1+t1+“PRIMARY”+2), value=2; 這裡value=2是因為修改前後的行id值不變,出現了兩次。

  2. key=hash_func(db1+t1+“a”+2), value=1,表示會影響到這個表a=2的行。

  3. key=hash_func(db1+t1+“a”+1), value=1,表示會影響到這個表a=1的行。

可見,相比於按表並行分發策略,按行並行策略在決定執行緒分發的時候,需要消耗更多的計算資源。你可能也發現了,這兩個方案其實都有一些約束條件:

  1. 要能夠從binlog裡面解析出表名、主鍵值和唯一索引的值。也就是說,主庫的binlog格式必須是row;

  2. 表必須有主鍵;

  3. 不能有外來鍵。表上如果有外來鍵,級聯更新的行不會記錄在binlog中,這樣衝突檢測就不準確。

但,好在這三條約束規則,本來就是DBA之前要求業務開發人員必須遵守的線上使用規範,所以這兩個並行複製策略在應用上也沒有碰到什麼麻煩。

對比按表分發和按行分發這兩個方案的話,按行分發策略的並行度更高。不過,如果是要操作很多行的大事務的話,按行分發的策略有兩個問題:

  1. 耗費記憶體。比如一個語句要刪除100萬行資料,這時候hash表就要記錄100萬個項。

  2. 耗費CPU。解析binlog,然後計算hash值,對於大事務,這個成本還是很高的。

所以,我在實現這個策略的時候會設定一個閾值,單個事務如果超過設定的行數閾值(比如,如果單個事務更新的行數超過10萬行),就暫時退化為單執行緒模式,退化過程的邏輯大概是這樣的:

  1. coordinator暫時先hold住這個事務;

  2. 等待所有worker都執行完成,變成空佇列;

  3. coordinator直接執行這個事務;

  4. 恢復並行模式。

讀到這裡,你可能會感到奇怪,這兩個策略又沒有被合到官方,我為什麼要介紹這麼詳細呢?其實,介紹這兩個策略的目的是拋磚引玉,方便你理解後面要介紹的社群版本策略。

MySQL 5.6版本的並行複製策略

官方MySQL5.6版本,支援了並行複製,只是支援的粒度是按庫並行。理解了上面介紹的按表分發策略和按行分發策略,你就理解了,用於決定分發策略的hash表裡,key就是資料庫名。

這個策略的並行效果,取決於壓力模型。如果在主庫上有多個DB,並且各個DB的壓力均衡,使用這個策略的效果會很好。

相比於按表和按行分發,這個策略有兩個優勢:

  1. 構造hash值的時候很快,只需要庫名;而且一個例項上DB數也不會很多,不會出現需要構造100萬個項這種情況。

  2. 不要求binlog的格式。因為statement格式的binlog也可以很容易拿到庫名。

但是,如果你的主庫上的表都放在同一個DB裡面,這個策略就沒有效果了;或者如果不同DB的熱點不同,比如一個是業務邏輯庫,一個是系統配置庫,那也起不到並行的效果。

理論上你可以建立不同的DB,把相同熱度的表均勻分到這些不同的DB中,強行使用這個策略。不過據我所知,由於需要特地移動資料,這個策略用得並不多。

MariaDB的並行複製策略

第23篇文章中,我給你介紹了redo log組提交(group commit)優化, 而MariaDB的並行複製策略利用的就是這個特性:

  1. 能夠在同一組裡提交的事務,一定不會修改同一行;

  2. 主庫上可以並行執行的事務,備庫上也一定是可以並行執行的。

在實現上,MariaDB是這麼做的:

  1. 在一組裡面一起提交的事務,有一個相同的commit_id,下一組就是commit_id+1;

  2. commit_id直接寫到binlog裡面;

  3. 傳到備庫應用的時候,相同commit_id的事務分發到多個worker執行;

  4. 這一組全部執行完成後,coordinator再去取下一批。

當時,這個策略出來的時候是相當驚豔的。因為,之前業界的思路都是在“分析binlog,並拆分到worker”上。而MariaDB的這個策略,目標是“模擬主庫的並行模式”。

但是,這個策略有一個問題,它並沒有實現“真正的模擬主庫併發度”這個目標。在主庫上,一組事務在commit的時候,下一組事務是同時處於“執行中”狀態的。

如圖5所示,假設了三組事務在主庫的執行情況,你可以看到在trx1、trx2和trx3提交的時候,trx4、trx5和trx6是在執行的。這樣,在第一組事務提交完成的時候,下一組事務很快就會進入commit狀態。

圖5 主庫並行事務

而按照MariaDB的並行複製策略,備庫上的執行效果如圖6所示。

圖6 MariaDB 並行複製,備庫並行效果

可以看到,在備庫上執行的時候,要等第一組事務完全執行完成後,第二組事務才能開始執行,這樣系統的吞吐量就不夠。

另外,這個方案很容易被大事務拖後腿。假設trx2是一個超大事務,那麼在備庫應用的時候,trx1和trx3執行完成後,就只能等trx2完全執行完成,下一組才能開始執行。這段時間,只有一個worker執行緒在工作,是對資源的浪費。

不過即使如此,這個策略仍然是一個很漂亮的創新。因為,它對原系統的改造非常少,實現也很優雅。

MySQL 5.7的並行複製策略

在MariaDB並行複製實現之後,官方的MySQL5.7版本也提供了類似的功能,由引數slave-parallel-type來控制並行複製策略:

  1. 配置為DATABASE,表示使用MySQL 5.6版本的按庫並行策略;

  2. 配置為 LOGICAL_CLOCK,表示的就是類似MariaDB的策略。不過,MySQL 5.7這個策略,針對並行度做了優化。這個優化的思路也很有趣兒。

你可以先考慮這樣一個問題:同時處於“執行狀態”的所有事務,是不是可以並行?

答案是,不能。

因為,這裡面可能有由於鎖衝突而處於鎖等待狀態的事務。如果這些事務在備庫上被分配到不同的worker,就會出現備庫跟主庫不一致的情況。

而上面提到的MariaDB這個策略的核心,是“所有處於commit”狀態的事務可以並行。事務處於commit狀態,表示已經通過了鎖衝突的檢驗了。

這時候,你可以再回顧一下兩階段提交,我把前面第23篇文章中介紹過的兩階段提交過程圖貼過來。

圖7 兩階段提交細化過程圖

其實,不用等到commit階段,只要能夠到達redo log prepare階段,就表示事務已經通過鎖衝突的檢驗了。

因此,MySQL 5.7並行複製策略的思想是:

  1. 同時處於prepare狀態的事務,在備庫執行時是可以並行的;

  2. 處於prepare狀態的事務,與處於commit狀態的事務之間,在備庫執行時也是可以並行的。

我在第23篇文章,講binlog的組提交的時候,介紹過兩個引數:

  1. binlog_group_commit_sync_delay引數,表示延遲多少微秒後才呼叫fsync;

  2. binlog_group_commit_sync_no_delay_count引數,表示累積多少次以後才呼叫fsync。

這兩個引數是用於故意拉長binlog從write到fsync的時間,以此減少binlog的寫盤次數。在MySQL 5.7的並行複製策略裡,它們可以用來製造更多的“同時處於prepare階段的事務”。這樣就增加了備庫複製的並行度。

也就是說,這兩個引數,既可以“故意”讓主庫提交得慢些,又可以讓備庫執行得快些。在MySQL 5.7處理備庫延遲的時候,可以考慮調整這兩個引數值,來達到提升備庫複製併發度的目的。

MySQL 5.7.22的並行複製策略

在2018年4月份釋出的MySQL 5.7.22版本里,MySQL增加了一個新的並行複製策略,基於WRITESET的並行複製。

相應地,新增了一個引數binlog-transaction-dependency-tracking,用來控制是否啟用這個新策略。這個引數的可選值有以下三種。

  1. COMMIT_ORDER,表示的就是前面介紹的,根據同時進入prepare和commit來判斷是否可以並行的策略。

  2. WRITESET,表示的是對於事務涉及更新的每一行,計算出這一行的hash值,組成集合writeset。如果兩個事務沒有操作相同的行,也就是說它們的writeset沒有交集,就可以並行。

  3. WRITESET_SESSION,是在WRITESET的基礎上多了一個約束,即在主庫上同一個執行緒先後執行的兩個事務,在備庫執行的時候,要保證相同的先後順序。

當然為了唯一標識,這個hash值是通過“庫名+表名+索引名+值”計算出來的。如果一個表上除了有主鍵索引外,還有其他唯一索引,那麼對於每個唯一索引,insert語句對應的writeset就要多增加一個hash值。

你可能看出來了,這跟我們前面介紹的基於MySQL 5.5版本的按行分發的策略是差不多的。不過,MySQL官方的這個實現還是有很大的優勢:

  1. writeset是在主庫生成後直接寫入到binlog裡面的,這樣在備庫執行的時候,不需要解析binlog內容(event裡的行資料),節省了很多計算量;

  2. 不需要把整個事務的binlog都掃一遍才能決定分發到哪個worker,更省記憶體;

  3. 由於備庫的分發策略不依賴於binlog內容,所以binlog是statement格式也是可以的。

因此,MySQL 5.7.22的並行複製策略在通用性上還是有保證的。

當然,對於“表上沒主鍵”和“外來鍵約束”的場景,WRITESET策略也是沒法並行的,也會暫時退化為單執行緒模型。

小結

在今天這篇文章中,我和你介紹了MySQL的各種多執行緒複製策略。

為什麼要有多執行緒複製呢?這是因為單執行緒複製的能力全面低於多執行緒複製,對於更新壓力較大的主庫,備庫是可能一直追不上主庫的。從現象上看就是,備庫上seconds_behind_master的值越來越大。

在介紹完每個並行複製策略後,我還和你分享了不同策略的優缺點:

  • 如果你是DBA,就需要根據不同的業務場景,選擇不同的策略;
  • 如果是你業務開發人員,也希望你能從中獲取靈感用到平時的開發工作中。

從這些分析中,你也會發現大事務不僅會影響到主庫,也是造成備庫複製延遲的主要原因之一。因此,在平時的開發工作中,我建議你儘量減少大事務操作,把大事務拆成小事務。

官方MySQL5.7版本新增的備庫並行策略,修改了binlog的內容,也就是說binlog協議並不是向上相容的,在主備切換、版本升級的時候需要把這個因素也考慮進去。

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

假設一個MySQL 5.7.22版本的主庫,單執行緒插入了很多資料,過了3個小時後,我們要給這個主庫搭建一個相同版本的備庫。

這時候,你為了更快地讓備庫追上主庫,要開並行複製。在binlog-transaction-dependency-tracking引數的COMMIT_ORDER、WRITESET和WRITE_SESSION這三個取值中,你會選擇哪一個呢?

你選擇的原因是什麼?如果設定另外兩個引數,你認為會出現什麼現象呢?

你可以把你的答案和分析寫在評論區,我會在下一篇文章跟你討論這個問題。感謝你的收聽,也歡迎你把這篇文章分享給更多的朋友一起閱讀。

上期問題時間

上期的問題是,什麼情況下,備庫的主備延遲會表現為一個45度的線段?評論區有不少同學的回覆都說到了重點:備庫的同步在這段時間完全被堵住了。

產生這種現象典型的場景主要包括兩種:

  • 一種是大事務(包括大表DDL、一個事務操作很多行);
  • 還有一種情況比較隱蔽,就是備庫起了一個長事務,比如
begin; 
select * from t limit 1;

然後就不動了。

這時候主庫對錶t做了一個加欄位操作,即使這個表很小,這個DDL在備庫應用的時候也會被堵住,也不能看到這個現象。

評論區還有同學說是不是主庫多執行緒、從庫單執行緒,備庫跟不上主庫的更新節奏導致的?今天這篇文章,我們剛好講的是並行複製。所以,你知道了,這種情況會導致主備延遲,但不會表現為這種標準的呈45度的直線。

評論區留言點贊板:

@易翔 、 @萬勇、@老楊同志 等同學的回覆都提到了我們上面說的場景;

@Max 同學提了一個很不錯的問題。主備關係裡面,備庫主動連線,之後的binlog傳送是主庫主動推送的。之所以這麼設計也是為了效率和實時性考慮,畢竟靠備庫輪詢,會有時間差。