在上一篇文章中,我和你介紹了幾種可能致使備庫延遲的緣由。你會發現,這些場景裏,不管是偶發性的查詢壓力,仍是備份,對備庫延遲的影響通常是分鐘級的,並且在備庫恢復正常之後都可以追上來。sql
可是,若是備庫執行日誌的速度持續低於主庫生成日誌的速度,那這個延遲就有可能成了小時級別。並且對於一個壓力持續比較高的主庫來講,備庫極可能永遠都追不上主庫的節奏。數據庫
這就涉及到今天我要給你介紹的話題:備庫並行複製能力。session
爲了便於你理解,咱們再一塊兒看一下第24篇文章《MySQL是怎麼保證主備一致的?》的主備流程圖。數據結構
談到主備的並行複製能力,咱們要關注的是圖中黑色的兩個箭頭。一個箭頭表明了客戶端寫入主庫,另外一箭頭表明的是備庫上sql_thread執行中轉日誌(relay log)。若是用箭頭的粗細來表明並行度的話,那麼真實狀況就如圖1所示,第一個箭頭要明顯粗於第二個箭頭。多線程
在主庫上,影響併發度的緣由就是各類鎖了。因爲InnoDB引擎支持行鎖,除了全部併發事務都在更新同一行(熱點行)這種極端場景外,它對業務併發度的支持仍是很友好的。因此,你在性能測試的時候會發現,併發壓測線程32就比單線程時,整體吞吐量高。併發
而日誌在備庫上的執行,就是圖中備庫上sql_thread更新數據(DATA)的邏輯。若是是用單線程的話,就會致使備庫應用日誌不夠快,形成主備延遲。性能
在官方的5.6版本以前,MySQL只支持單線程複製,由此在主庫併發高、TPS高時就會出現嚴重的主備延遲問題。測試
從單線程複製到最新版本的多線程複製,中間的演化經歷了好幾個版本。接下來,我就跟你說說MySQL多線程複製的演進過程。優化
其實說到底,全部的多線程複製機制,都是要把圖1中只有一個線程的sql_thread,拆成多個線程,也就是都符合下面的這個模型:spa
圖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在分發的時候,須要知足如下這兩個基本要求:
不能形成更新覆蓋。這就要求更新同一行的兩個事務,必須被分發到同一個worker中。
同一個事務不能被拆開,必須放到同一個worker中。
各個版本的多線程複製,都遵循了這兩條基本原則。接下來,咱們就看看各個版本的並行複製策略。
官方MySQL 5.5版本是不支持並行複製的。可是,在2012年的時候,我本身服務的業務出現了嚴重的主備延遲,緣由就是備庫只有單線程複製。而後,我就前後寫了兩個版本的並行策略。
這裏,我給你介紹一下這兩個版本的並行策略,即按表分發策略和按行分發策略,以幫助你理解MySQL官方版本並行複製策略的迭代。
按表分發事務的基本思路是,若是兩個事務更新不一樣的表,它們就能夠並行。由於數據是存儲在表裏的,因此按表分發,能夠保證兩個worker不會更新同一行。
固然,若是有跨表的事務,仍是要把兩張表放在一塊兒考慮的。如圖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的分配流程,來看一下分配規則。
因爲事務T中涉及修改表t1,而worker_1隊列中有事務在修改表t1,事務T和隊列中的某個事務要修改同一個表的數據,這種狀況咱們說事務T和worker_1是衝突的。
按照這個邏輯,順序判斷事務T和每一個worker隊列的衝突關係,會發現事務T跟worker_2也衝突。
事務T跟多於一個worker衝突,coordinator線程就進入等待。
每一個worker繼續執行,同時修改hash_table。假設hash_table_2裏面涉及到修改表t3的事務先執行完成,就會從hash_table_2中把db1.t3這一項去掉。
這樣coordinator會發現跟事務T衝突的worker只有worker_1了,所以就把它分配給worker_1。
coordinator繼續讀下一個中轉日誌,繼續分配事務。
也就是說,每一個事務在分發的時候,跟全部worker的衝突關係包括如下三種狀況:
若是跟全部worker都不衝突,coordinator線程就會把這個事務分配給最空閒的woker;
若是跟多於一個worker衝突,coordinator線程就進入等待狀態,直到和這個事務存在衝突關係的worker只剩下1個;
若是隻跟一個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);
假設,接下來咱們要在主庫執行這兩個事務:
能夠看到,這兩個事務要更新的行的主鍵值不一樣,可是若是它們被分到不一樣的worker,就有可能session B的語句先執行。這時候id=1的行的a的值仍是1,就會報惟一鍵衝突。
所以,基於行的策略,事務hash表中還須要考慮惟一鍵,即key應該是「庫名+表名+索引a的名字+a的值」。
好比,在上面這個例子中,我要在表t1上執行update t1 set a=1 where id=2語句,在binlog裏面記錄了整行的數據修改前各個字段的值,和修改後各個字段的值。
所以,coordinator在解析這個語句的binlog的時候,這個事務的hash表就有三個項:
key=hash_func(db1+t1+「PRIMARY」+2), value=2; 這裏value=2是由於修改先後的行id值不變,出現了兩次。
key=hash_func(db1+t1+「a」+2), value=1,表示會影響到這個表a=2的行。
key=hash_func(db1+t1+「a」+1), value=1,表示會影響到這個表a=1的行。
可見,相比於按表並行分發策略,按行並行策略在決定線程分發的時候,須要消耗更多的計算資源。你可能也發現了,這兩個方案其實都有一些約束條件:
要可以從binlog裏面解析出表名、主鍵值和惟一索引的值。也就是說,主庫的binlog格式必須是row;
表必須有主鍵;
不能有外鍵。表上若是有外鍵,級聯更新的行不會記錄在binlog中,這樣衝突檢測就不許確。
但,好在這三條約束規則,原本就是DBA以前要求業務開發人員必須遵照的線上使用規範,因此這兩個並行複製策略在應用上也沒有碰到什麼麻煩。
對比按表分發和按行分發這兩個方案的話,按行分發策略的並行度更高。不過,若是是要操做不少行的大事務的話,按行分發的策略有兩個問題:
耗費內存。好比一個語句要刪除100萬行數據,這時候hash表就要記錄100萬個項。
耗費CPU。解析binlog,而後計算hash值,對於大事務,這個成本仍是很高的。
因此,我在實現這個策略的時候會設置一個閾值,單個事務若是超過設置的行數閾值(好比,若是單個事務更新的行數超過10萬行),就暫時退化爲單線程模式,退化過程的邏輯大概是這樣的:
coordinator暫時先hold住這個事務;
等待全部worker都執行完成,變成空隊列;
coordinator直接執行這個事務;
恢復並行模式。
讀到這裏,你可能會感到奇怪,這兩個策略又沒有被合到官方,我爲何要介紹這麼詳細呢?其實,介紹這兩個策略的目的是拋磚引玉,方便你理解後面要介紹的社區版本策略。
官方MySQL5.6版本,支持了並行複製,只是支持的粒度是按庫並行。理解了上面介紹的按表分發策略和按行分發策略,你就理解了,用於決定分發策略的hash表裏,key就是數據庫名。
這個策略的並行效果,取決於壓力模型。若是在主庫上有多個DB,而且各個DB的壓力均衡,使用這個策略的效果會很好。
相比於按表和按行分發,這個策略有兩個優點:
構造hash值的時候很快,只須要庫名;並且一個實例上DB數也不會不少,不會出現須要構造100萬個項這種狀況。
不要求binlog的格式。由於statement格式的binlog也能夠很容易拿到庫名。
可是,若是你的主庫上的表都放在同一個DB裏面,這個策略就沒有效果了;或者若是不一樣DB的熱點不一樣,好比一個是業務邏輯庫,一個是系統配置庫,那也起不到並行的效果。
理論上你能夠建立不一樣的DB,把相同熱度的表均勻分到這些不一樣的DB中,強行使用這個策略。不過據我所知,因爲須要特意移動數據,這個策略用得並很少。
在第23篇文章中,我給你介紹了redo log組提交(group commit)優化, 而MariaDB的並行複製策略利用的就是這個特性:
可以在同一組裏提交的事務,必定不會修改同一行;
主庫上能夠並行執行的事務,備庫上也必定是能夠並行執行的。
在實現上,MariaDB是這麼作的:
在一組裏面一塊兒提交的事務,有一個相同的commit_id,下一組就是commit_id+1;
commit_id直接寫到binlog裏面;
傳到備庫應用的時候,相同commit_id的事務分發到多個worker執行;
這一組所有執行完成後,coordinator再去取下一批。
當時,這個策略出來的時候是至關驚豔的。由於,以前業界的思路都是在「分析binlog,並拆分到worker」上。而MariaDB的這個策略,目標是「模擬主庫的並行模式」。
可是,這個策略有一個問題,它並無實現「真正的模擬主庫併發度」這個目標。在主庫上,一組事務在commit的時候,下一組事務是同時處於「執行中」狀態的。
如圖5所示,假設了三組事務在主庫的執行狀況,你能夠看到在trx一、trx2和trx3提交的時候,trx四、trx5和trx6是在執行的。這樣,在第一組事務提交完成的時候,下一組事務很快就會進入commit狀態。
而按照MariaDB的並行複製策略,備庫上的執行效果如圖6所示。
能夠看到,在備庫上執行的時候,要等第一組事務徹底執行完成後,第二組事務才能開始執行,這樣系統的吞吐量就不夠。
另外,這個方案很容易被大事務拖後腿。假設trx2是一個超大事務,那麼在備庫應用的時候,trx1和trx3執行完成後,就只能等trx2徹底執行完成,下一組才能開始執行。這段時間,只有一個worker線程在工做,是對資源的浪費。
不過即便如此,這個策略仍然是一個很漂亮的創新。由於,它對原系統的改造很是少,實現也很優雅。
在MariaDB並行複製實現以後,官方的MySQL5.7版本也提供了相似的功能,由參數slave-parallel-type來控制並行複製策略:
配置爲DATABASE,表示使用MySQL 5.6版本的按庫並行策略;
配置爲 LOGICAL_CLOCK,表示的就是相似MariaDB的策略。不過,MySQL 5.7這個策略,針對並行度作了優化。這個優化的思路也頗有趣兒。
你能夠先考慮這樣一個問題:同時處於「執行狀態」的全部事務,是否是能夠並行?
答案是,不能。
由於,這裏面可能有因爲鎖衝突而處於鎖等待狀態的事務。若是這些事務在備庫上被分配到不一樣的worker,就會出現備庫跟主庫不一致的狀況。
而上面提到的MariaDB這個策略的核心,是「全部處於commit」狀態的事務能夠並行。事務處於commit狀態,表示已經經過了鎖衝突的檢驗了。
這時候,你能夠再回顧一下兩階段提交,我把前面第23篇文章中介紹過的兩階段提交過程圖貼過來。
其實,不用等到commit階段,只要可以到達redo log prepare階段,就表示事務已經經過鎖衝突的檢驗了。
所以,MySQL 5.7並行複製策略的思想是:
同時處於prepare狀態的事務,在備庫執行時是能夠並行的;
處於prepare狀態的事務,與處於commit狀態的事務之間,在備庫執行時也是能夠並行的。
我在第23篇文章,講binlog的組提交的時候,介紹過兩個參數:
binlog_group_commit_sync_delay參數,表示延遲多少微秒後才調用fsync;
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,用來控制是否啓用這個新策略。這個參數的可選值有如下三種。
COMMIT_ORDER,表示的就是前面介紹的,根據同時進入prepare和commit來判斷是否能夠並行的策略。
WRITESET,表示的是對於事務涉及更新的每一行,計算出這一行的hash值,組成集合writeset。若是兩個事務沒有操做相同的行,也就是說它們的writeset沒有交集,就能夠並行。
WRITESET_SESSION,是在WRITESET的基礎上多了一個約束,即在主庫上同一個線程前後執行的兩個事務,在備庫執行的時候,要保證相同的前後順序。
固然爲了惟一標識,這個hash值是經過「庫名+表名+索引名+值」計算出來的。若是一個表上除了有主鍵索引外,還有其餘惟一索引,那麼對於每一個惟一索引,insert語句對應的writeset就要多增長一個hash值。
你可能看出來了,這跟咱們前面介紹的基於MySQL 5.5版本的按行分發的策略是差很少的。不過,MySQL官方的這個實現仍是有很大的優點:
writeset是在主庫生成後直接寫入到binlog裏面的,這樣在備庫執行的時候,不須要解析binlog內容(event裏的行數據),節省了不少計算量;
不須要把整個事務的binlog都掃一遍才能決定分發到哪一個worker,更省內存;
因爲備庫的分發策略不依賴於binlog內容,因此binlog是statement格式也是能夠的。
所以,MySQL 5.7.22的並行複製策略在通用性上仍是有保證的。
固然,對於「表上沒主鍵」和「外鍵約束」的場景,WRITESET策略也是無法並行的,也會暫時退化爲單線程模型。
在今天這篇文章中,我和你介紹了MySQL的各類多線程複製策略。
爲何要有多線程複製呢?這是由於單線程複製的能力全面低於多線程複製,對於更新壓力較大的主庫,備庫是可能一直追不上主庫的。從現象上看就是,備庫上seconds_behind_master的值愈來愈大。
在介紹完每一個並行複製策略後,我還和你分享了不一樣策略的優缺點:
從這些分析中,你也會發現大事務不只會影響到主庫,也是形成備庫複製延遲的主要緣由之一。所以,在平時的開發工做中,我建議你儘可能減小大事務操做,把大事務拆成小事務。
官方MySQL5.7版本新增的備庫並行策略,修改了binlog的內容,也就是說binlog協議並非向上兼容的,在主備切換、版本升級的時候須要把這個因素也考慮進去。
最後,我給你留下一個思考題吧。
假設一個MySQL 5.7.22版本的主庫,單線程插入了不少數據,過了3個小時後,咱們要給這個主庫搭建一個相同版本的備庫。
這時候,你爲了更快地讓備庫追上主庫,要開並行複製。在binlog-transaction-dependency-tracking參數的COMMIT_ORDER、WRITESET和WRITE_SESSION這三個取值中,你會選擇哪個呢?
你選擇的緣由是什麼?若是設置另外兩個參數,你認爲會出現什麼現象呢?
你能夠把你的答案和分析寫在評論區,我會在下一篇文章跟你討論這個問題。感謝你的收聽,也歡迎你把這篇文章分享給更多的朋友一塊兒閱讀。
上期的問題是,什麼狀況下,備庫的主備延遲會表現爲一個45度的線段?評論區有很多同窗的回覆都說到了重點:備庫的同步在這段時間徹底被堵住了。
產生這種現象典型的場景主要包括兩種:
begin; select * from t limit 1;
而後就不動了。
這時候主庫對錶t作了一個加字段操做,即便這個表很小,這個DDL在備庫應用的時候也會被堵住,也不能看到這個現象。
評論區還有同窗說是否是主庫多線程、從庫單線程,備庫跟不上主庫的更新節奏致使的?今天這篇文章,咱們恰好講的是並行複製。因此,你知道了,這種狀況會致使主備延遲,但不會表現爲這種標準的呈45度的直線。