MySQL實戰45講學習筆記:第二十六講

 1、引子

在上一篇文章中,我和你介紹了幾種可能致使備庫延遲的緣由。你會發現,這些場景裏,不管是偶發性的查詢壓力,仍是備份,對備庫延遲的影響通常是分鐘級的,並且在備庫恢
復正常之後都可以追上來。sql

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

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

2、備庫並行複製能力

一、在官方的 5.6 版本以前,MySQL 只支持單線程複製

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

圖 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

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

實際上是不行的。由於,事務被分發給 worker 之後,不一樣的 worker 就獨立執行了。可是,因爲 CPU 的調度策略,極可能第二個事務最終比第一個事務先執行。而若是這時候剛
好這兩個事務更新的是同一行,也就意味着,同一行上的兩個事務,在主庫和備庫上的執行順序相反,會致使主備不一致的問題。

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

答案是,也不行。

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

三、coordinator 在分發的時候,須要知足如下這兩個基本要求

1. 不能形成更新覆蓋。這就要求更新同一行的兩個事務,必須被分發到同一個 worker中。
2. 同一個事務不能被拆開,必須放到同一個 worker 中。各個版本的多線程複製,都遵循了這兩條基本原則。接下來,咱們就看看各個版本的並行複製策略。

3、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. 恢復並行模式。

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

4、MySQL 5.6 版本的並行複製策略

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

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

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

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

2. 不要求 binlog 的格式。由於 statement 格式的 binlog 也能夠很容易拿到庫名

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

理論上你能夠建立不一樣的 DB,把相同熱度的表均勻分到這些不一樣的 DB 中,強行使用這個策略。不過據我所知,因爲須要特意移動數據,這個策略用得並很少。

5、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 所示,假設了三組事務在主庫的執行狀況,你能夠看到在 trx一、trx2 和 trx3 提交的時候,trx四、trx5 和 trx6 是在執行的。這樣,在第一組事務提交完成的時候,下一組事
務很快就會進入 commit 狀態。

 

圖 5 主庫並行事務

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

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

 

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

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

6、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 處理備庫延遲的時候,能夠考慮調整這兩個參數值,來達到提高備庫複製
併發度的目的。

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 策略也是無法並行的,也會暫時退化爲單線程模型。

8、小結

在今天這篇文章中,我和你介紹了 MySQL 的各類多線程複製策略。

爲何要有多線程複製呢?這是由於單線程複製的能力全面低於多線程複製,對於更新壓力較大的主庫,備庫是可能一直追不上主庫的。從現象上看就是,備庫上
seconds_behind_master 的值愈來愈大。

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

若是你是 DBA,就須要根據不一樣的業務場景,選擇不一樣的策略;

若是是你業務開發人員,也但願你能從中獲取靈感用到平時的開發工做中。

從這些分析中,你也會發現大事務不只會影響到主庫,也是形成備庫複製延遲的主要緣由之一。所以,在平時的開發工做中,我建議你儘可能減小大事務操做,把大事務拆成小事務。

官方 MySQL5.7 版本新增的備庫並行策略,修改了 binlog 的內容,也就是說 binlog 協議並非向上兼容的,在主備切換、版本升級的時候須要把這個因素也考慮進去。
最後,我給你留下一個思考題吧。

假設一個 MySQL 5.7.22 版本的主庫,單線程插入了不少數據,過了 3 個小時後,咱們要給這個主庫搭建一個相同版本的備庫。

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

你選擇的緣由是什麼?若是設置另外兩個參數,你認爲會出現什麼現象呢?

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

9、上節問題

上期的問題是,什麼狀況下,備庫的主備延遲會表現爲一個 45 度的線段?評論區有很多同窗的回覆都說到了重點:備庫的同步在這段時間徹底被堵住了。

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

一種是大事務(包括大表 DDL、一個事務操做不少行);
還有一種狀況比較隱蔽,就是備庫起了一個長事務,好比

begin; 
select * from t limit 1;

而後就不動了。

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

一種是大事務(包括大表 DDL、一個事務操做不少行);

還有一種狀況比較隱蔽,就是備庫起了一個長事務,好比

評論區還有同窗說是否是主庫多線程、從庫單線程,備庫跟不上主庫的更新節奏致使的?

今天這篇文章,咱們恰好講的是並行複製。因此,你知道了,這種狀況會致使主備延遲,但不會表現爲這種標準的呈 45 度的直線。

評論區留言點贊板:

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

@Max 同窗提了一個很不錯的問題。主備關係裏面,備庫主動鏈接,以後的binlog 發送是主庫主動推送的。之因此這麼設計也是爲了效率和實時性考慮,畢竟靠備庫輪詢,會有時間差。

相關文章
相關標籤/搜索