在上一篇文章中,我和你介紹了幾種可能致使備庫延遲的緣由。你會發現,這些場景裏,不管是偶發性的查詢壓力,仍是備份,對備庫延遲的影響通常是分鐘級的,並且在備庫恢
復正常之後都可以追上來。sql
可是,若是備庫執行日誌的速度持續低於主庫生成日誌的速度,那這個延遲就有可能成了小時級別。並且對於一個壓力持續比較高的主庫來講,備庫極可能永遠都追不上主庫的節奏。數據庫
這就涉及到今天我要給你介紹的話題:備庫並行複製能力。bash
爲了便於你理解,咱們再一塊兒看一下第 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_1,第二個事務發給 worker_2 呢?
實際上是不行的。由於,事務被分發給 worker 之後,不一樣的 worker 就獨立執行了。可是,因爲 CPU 的調度策略,極可能第二個事務最終比第一個事務先執行。而若是這時候剛
好這兩個事務更新的是同一行,也就意味着,同一行上的兩個事務,在主庫和備庫上的執行順序相反,會致使主備不一致的問題。
接下來,請你再設想一下另一個問題:同一個事務的多個更新語句,能不能分給不一樣的worker 來執行呢?
答案是,也不行。
舉個例子,一個事務更新了表 t1 和表 t2 中的各一行,若是這兩條更新語句被分到不一樣 worker 的話,雖然最終的結果是主備一致的,但若是表 t1 執行完成的瞬
間,備庫上有一個查詢,就會看到這個事務「更新了一半的結果」,破壞了事務邏輯的隔離性。
1. 不能形成更新覆蓋。這就要求更新同一行的兩個事務,必須被分發到同一個 worker中。
2. 同一個事務不能被拆開,必須放到同一個 worker 中。各個版本的多線程複製,都遵循了這兩條基本原則。接下來,咱們就看看各個版本的並行複製策略。
官方 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. 恢復並行模式。
讀到這裏,你可能會感到奇怪,這兩個策略又沒有被合到官方,我爲何要介紹這麼詳細呢?其實,介紹這兩個策略的目的是拋磚引玉,方便你理解後面要介紹的社區版本策略。
官方 MySQL5.6 版本,支持了並行複製,只是支持的粒度是按庫並行。理解了上面介紹的按表分發策略和按行分發策略,你就理解了,用於決定分發策略的 hash 表裏,key 就是
數據庫名。
這個策略的並行效果,取決於壓力模型。若是在主庫上有多個 DB,而且各個 DB 的壓力均衡,使用這個策略的效果會很好。
相比於按表和按行分發,這個策略有兩個優點:
1. 構造 hash 值的時候很快,只須要庫名;並且一個實例上 DB 數也不會不少,不會出現須要構造 100 萬個項這種狀況。
2. 不要求 binlog 的格式。由於 statement 格式的 binlog 也能夠很容易拿到庫名
可是,若是你的主庫上的表都放在同一個 DB 裏面,這個策略就沒有效果了;或者若是不一樣 DB 的熱點不一樣,好比一個是業務邏輯庫,一個是系統配置庫,那也起不到並行的效果。
理論上你能夠建立不一樣的 DB,把相同熱度的表均勻分到這些不一樣的 DB 中,強行使用這個策略。不過據我所知,因爲須要特意移動數據,這個策略用得並很少。
在第 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 線程在工做,是對資源的浪費。
不過即便如此,這個策略仍然是一個很漂亮的創新。由於,它對原系統的改造很是少,實現也很優雅。
在 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 處理備庫延遲的時候,能夠考慮調整這兩個參數值,來達到提高備庫複製
併發度的目的。
在 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 在備庫應用的時候也會被堵住,也不能看到這個現象。
一種是大事務(包括大表 DDL、一個事務操做不少行);
還有一種狀況比較隱蔽,就是備庫起了一個長事務,好比
評論區還有同窗說是否是主庫多線程、從庫單線程,備庫跟不上主庫的更新節奏致使的?
今天這篇文章,咱們恰好講的是並行複製。因此,你知道了,這種狀況會致使主備延遲,但不會表現爲這種標準的呈 45 度的直線。
@易翔 、 @萬勇、@老楊同志 等同窗的回覆都提到了咱們上面說的場景;
@Max 同窗提了一個很不錯的問題。主備關係裏面,備庫主動鏈接,以後的binlog 發送是主庫主動推送的。之因此這麼設計也是爲了效率和實時性考慮,畢竟靠備庫輪詢,會有時間差。