- 數據生態:MySQL復制技術與生產實踐
- 羅小波
- 4687字
- 2020-11-24 13:05:01
第6章 多線程復制
MySQL 5.6之前的版本不支持從庫并行重放主庫的二進制日志,所以一旦主庫的寫壓力稍微大一點,從庫就容易出現延遲。當然,目前最新的MySQL版本已經能夠很好地支持多線程復制。為了便于理解復制是如何一步一步演進為多線程復制的,本章將從單線程復制說起。在開始學習本章內容之前,也許你需要回顧一下復制的基本原理,詳見第2章“復制的基本原理”。
提示:
? 本章中解析的所有二進制日志示例均為 row格式。
? 下文中提到的“單線程復制”也可稱為“串行復制”,“多線程復制”也可稱為“并行復制”(注意,這里所說的“串行復制”與“并行復制”,指的是數據庫中數據變更操作的“串行”與“并行”,不要和復制拓撲中的主從復制的“串行”與“并行”混淆)。
? 下文中提到的系統變量詳見《?千金良方:MySQL性能優化金字塔法則》附錄C“MySQL常用配置變量和狀態變量詳解”。
6.1 單線程復制原理
單線程復制是MySQL中最早出現的Server之間的數據同步技術,當從庫的I/O線程將主庫二進制日志寫入自身的中繼日志之后,讀取中繼日志并進行回放的線程只有一個,也就是SQL Thread(SQL線程),參見圖6-1。

圖6-1
下面以單線程復制中主庫寫入二進制日志的日志解析記錄為例,對單線程復制原理進行詳細的闡述。假設主庫中執行了一個INSERT操作,那么在二進制日志中的記錄如下(MySQL 5.5):

如圖6-1所示,從庫的SQL線程從中繼日志中讀取并解析主庫二進制日志,由于執行事件的線程只有一個,所以讀取的所有事件被串行執行。而二進制日志的寫入時機是在事務發起提交之后,也就是說,主庫事務先執行,然后產生的二進制日志才會被發送到從庫執行。所以,從理論上講,主從庫這一前一后的時差就必然會導致從庫復制延遲。如果遇到大事務,則從庫延遲會急劇增加,例如,主庫執行一個大事務耗費1小時,當從庫收到這個事務之后開始執行時,就已經落后于主庫1小時了。也正是因為單線程復制的效率極端低下,倒逼單線程復制向著多線程復制發展。
提示:前面提到,MySQL 5.6之前的版本,從庫不支持多線程復制,但實際上在這之前的版本中,當不啟用二進制日志時,InnoDB存儲引擎本身是支持Group Commit的(即支持一次提交多個事務),但當啟用二進制日志之后,為了保證數據的一致性(也就是必須保證MySQL Server層和存儲引擎層的提交順序一致),啟用了兩階段提交。而兩階段提交中的prepare階段使用了prepare_commit_mutex互斥鎖來強制事務串行提交,這也大大降低了數據庫的寫入效率。
6.2 DATABASE多線程復制
6.2.1 原理
顧名思義,DATABASE多線程復制就是在單線程復制的基礎之上做了改進,基于庫級別的多線程復制。MySQL從5.6版本開始,支持庫級別的多線程復制,這也是最早出現的多線程復制機制,在二進制日志中記錄類似如下的內容(MySQL 5.6):

以上對于INSERT語句二進制日志的解析內容,與MySQL 5.5的解析內容相比,幾乎沒有什么變化。那么,從MySQL 5.6開始,對復制功能的改進主要是什么呢?
對于實例自身的事務而言(這里指的是本地事務,不區分主從庫),在原先的兩階段提交中,移除了prepare_commit_mutex互斥鎖。為保證二進制日志的提交順序和存儲引擎層的一致,引入與InnoDB存儲引擎層的Group Commit類似的機制,并將其稱為Binary Log Group Commit(BLGC)。在MySQL數據庫上層提交事務時,首先按照順序將事務放入一個隊列中,隊列中的第一個事務稱為leader,其他事務稱為follower,leader控制follower的行為。一旦前一個階段中的隊列任務執行完,后一個階段隊列中的leader就會帶領它的follower進入前一個階段的隊列中執行,這樣的并行提交可以持續不斷地進行。BLGC的步驟分為圖6-2所示的3個階段(該圖來自mysqlmusings blogspot)。

圖6-2
(1)Flush階段:將每個事務的二進制日志寫入內存。
(2)Sync階段:將內存中的二進制日志刷新到磁盤,若隊列中有多個事務,那么僅一次fsync操作就完成多個事務的二進制日志寫入,這就是BLGC。
(3)Commit階段:leader根據順序調用存儲引擎層事務的提交,InnoDB存儲引擎本就支持Group Commit,因此修復了原先由prepare_commit_mutex互斥鎖導致InnoDB存儲引擎層Group Commit失效的問題。這樣一來,在啟用二進制日志的情況下,就實現了數據庫中事務的并行提交。
對于從庫事務而言(這里指的是從庫通過二進制日志重放的主庫事務),主要的改進在于從庫復制的SQL線程——增加了一個SQL協調器線程(Coordinator線程),真正干活的SQL線程被稱為工作線程(Worker線程),當Worker線程為N個(N > 1)以及主庫的DATABASE(Schema)為N個時,從庫就可以根據多個DATABASE之間相互獨立(彼此之間無鎖沖突)的語句來實現多線程復制;反之,如果N = 1,則多線程復制跟MySQL 5.6之前版本中的單線程復制沒有太大區別。多線程復制大致的工作流如圖6-3所示。
提示:對于DATABASE多線程復制,如果有跨庫事務,并行的Worker線程之間可能產生相互等待。

圖6-3
6.2.2 系統變量的配置
1. 主庫

2. 從庫

6.3 LOGICAL_CLOCK多線程復制
6.3.1 原理
MySQL從5.7版本開始,支持LOGICAL_CLOCK多線程復制。基于MySQL 5.6庫級別的Group Commit多線程復制做了大幅改進。對于DATABASE多線程復制,允許并行回放的粒度為數據庫級別,只有在同一時間修改數據且修改操作針對的是不同數據庫,才允許并行。而對于LOGICAL_CLOCK多線程復制,允許并行回放的粒度為事務級別,即便在同一時間修改數據的操作針對的是同一個數據庫,理論上只要事務之間不存在鎖沖突,就允許并行,可通過設置系統變量slave_parallel_type為LOGICAL_CLOCK來啟用,如果該變量被設置為DATABASE,則與MySQL 5.6的多線程復制相同。從字面上無法直觀地看出LOGICAL_CLOCK是基于什么維度來實現多線程復制的。下面我們通過解析二進制日志的內容來進行解讀。

從以上對于INSERT語句的二進制日志解析內容來看,MySQL從5.7版本開始,新增了兩個事件類型,Anonymous_gtid_log_event類型用于記錄未啟用GTID時的Binlog Group信息,Gtid_log_event類型用于記錄啟用GTID時的Binlog Group信息。利用這些信息,從庫的SQL線程在應用主庫的二進制日志時,就可以并行回放,大大提高了從庫復制的效率。
從上述代碼段的注解中我們可以知道,擁有相同last_committed值的事務可以并行回放,但是事務的last_committed值是如何確定的呢?
last_committed值是主庫事務在進入prepare階段時獲取的已提交事務的最大sequence_number值。這個值稱為此事務的commit-parent,被記錄在二進制日志中,當從庫回放事務時,如果兩個事務有同一個commit-parent,它們就可以并行執行。
提示:
? 在LOGICAL_CLOCK多線程復制中,發生變更的是主庫記錄二進制日志的算法與格式,以及從庫分發事務的算法。至于從庫的復制線程,仍然沿用圖6-3所示的框架。
? 雖然這種方式大幅提高了從庫復制的效率,也可以說,允許并行回放的粒度細化到事務級別,甚至可以說細化到行級別(每個事務只修改一行數據)。但是,可以并行回放的事務必須具有相同的last_committed值,即使兩個事務的數據完全不相關,但如果last_committed值不同,也不能并行回放,而有多少事務具有相同的last_committed值,則由主庫瞬時并發請求的數量而定(系統變量binlog_group_commit_sync_no_delay_count = 0時)。如果主庫沒有什么寫壓力,寫入二進制日志中的每個事務的last_committed值都不相同,這時從庫的復制實際上仍然是單線程復制。所以,LOGICAL_CLOCK多線程復制仍然有一定的優化空間。
? 在MySQL 5.7較新的版本中,將允許并行回放的算法升級為基于Lock(鎖)的,即通過一個鎖的時間范圍來確定是否可以并行回放。主庫會力求為每一個事務生成盡可能小的last_committed值,這樣就可以提高從庫回放的并行度,因為從庫進行回放時判斷是否可以并行回放的依據,就是last_committed值是否相同。該鎖的時間范圍設定為:以事務在兩階段提交的prepare階段獲取最后一把鎖的時間為開始時間點,以commit階段釋放第一把鎖的時間為結束時間點。如果鎖的時間范圍有重疊,事務就可以并行回放,無重疊就不能并行回放。在升級為“基于Lock”的多線程復制之前,主庫為事務生成的last_committed值是在每個事務執行提交操作時獲取的當前已提交完成事務的最大sequence_number,這種機制稱為Commit-Parent-Based。對于該機制有興趣的讀者可自行研究,這里不展開闡述。
6.3.2 系統變量的配置
1. 主庫

2. 從庫

6.4 WRITESET多線程復制
6.4.1 原理
WRITESET多線程復制,其實是在MySQL 5.7的大于或等于5.7.22的版本、MySQL 8.0的大于或等于8.0.1的版本中,對LOGICAL_CLOCK多線程復制的優化機制。優化之后,只要不同事務的修改記錄不重疊,就可以在從庫中并行回放。通過計算每行記錄的哈希值(hash)來確定其是否為相同的記錄。該哈希值就是WRITESET值。
WRITESET多線程復制允許并行回放的粒度依然為事務級別。嚴格來說,WRITESET以及下文中提到WRITESET_SESSION都只是在原有的LOGICAL_CLOCK多線程復制的基礎上優化事務并行度的依賴模式,默認的依賴模式為COMMIT_ORDER。但由于WRITESET和WRITESET_SESSION依賴模式在某些場景下,能夠顯著提高多線程復制的效率,加上這兩種依賴模式都是基于全新引入的沖突檢測數據庫的機制實現的,因此,通常大家都習慣將它們一起稱為“WRITESET多線程復制”,以便和COMMIT_ORDER依賴模式的“LOGICAL_CLOCK多線程復制”區分。
WRITESET多線程復制的本質就是基于WRITESET的值對生成last_committed值的依賴模式做了大量優化。下面我們通過解析二進制日志中記錄的內容來進行解讀。

從以上對于INSERT語句的二進制日志解析內容來看,WRITESET多線程復制與LOGICAL_CLOCK多線程復制記錄的二進制日志相比,格式上沒有什么變化,那么它對于復制的改進主要是什么呢?細心的讀者可能已經發現,last_committed = 2的兩個事務的時間戳并不是同一個時刻的,并且這兩個事務之間,還夾了一個last_committed = 8的事務。在以往的LOGICAL_CLOCK多線程復制中幾乎不可能出現這種情況,這其中發生了什么?
對于主庫來說,WRITESET多線程復制對LOGICAL_CLOCK多線程復制的優化,并不是在二進制日志記錄的格式上,而是在事務寫二進制日志時對last_committed值的計算做了大量優化。在6.3.1節中,我們提到過,LOGICAL_CLOCK多線程復制的并行粒度為事務級別,在理論上雖然只要不同事務之間不存在鎖沖突即可并行,但實際上采用默認的COMMIT_ORDER依賴模式的多線程復制,在從庫中的并行分發機制上并不能完全實現。這是因為在默認的COMMIT_ORDER依賴模式下,主庫生成二進制日志時,只有同一時間提交的事務生成的last_committed值才相同,但實際上不同時間提交的事務也有不存在鎖沖突的可能。因此,這些事務在主庫中生成二進制日志時的last_committed值理論上也應該相同。要實現在主庫生成中生成二進制日志時,在不同時間提交的且不存在鎖沖突的事務生成相同的last_committed值,必須引入新機制,而這個新的機制,便是WRITESET多線程復制對LOGICAL_CLOCK多線程復制優化的關鍵。
通過唯一索引或主鍵索引來區分不同的記錄,然后和行記錄的庫表屬性以及數據屬性一起計算哈希值,計算出的WRITESET值存放在一張哈希表中。后面如果新事務的行記錄計算出的哈希值在哈希表中無匹配記錄,那么此新事務不會產生新的last_committed值,就相當于新事務和之前的事務被歸并到同一個Binlog Group,即新舊事務的last_committed值相同。如果新事務的行記錄計算出的哈希值在哈希表中找到了匹配記錄,則表示存在事務沖突,就會產生新的last_committed值(即產生了一個新的Binlog Group)。具體的計算公式如下:
WRITESET = hash(index_name,db_name,db_name_length,table_name,table_name_length, value,value_length)
主庫中生成last_committed值的依賴模式改進如圖6-4所示。

圖6-4
根據系統變量binlog_transaction_dependency_tracking的設置,采用不同的依賴模式生成last_committed值。
在WRITESET多線程復制中做沖突認證的過程如圖6-5所示。
對于從庫來說,并行應用二進制日志的邏輯幾乎沒有變化,仍然根據last_committed的值判斷是否可以并行回放。
提示:
WRITESET多線程復制在如下場景中不可用:
? DDL語句。
? 會話當前生效的哈希算法(可以使用系統變量transaction_write_set_extraction在會話級別修改)和生成writeset_history列表值使用的哈希算法不同(哈希算法被動態修改過之后,碰到不同算法產生的值將無法進行比較)。
? 事務更新了被外鍵關聯的字段。
使用WRITESET多線程復制時,如果主庫中具有高并發的事務且這些事務具備“短、平、快”的特性,則WRITESET多線程復制中的WRITESET、WRITESET_SESSION依賴模式與默認的COMMIT_ORDER依賴模式相比,對從庫的多線程復制效率并沒有太大提高,但如果主庫中有高并發的事務且這些事務中大事務居多,則使用COMMIT_ORDER依賴模式時,二進制日志中的last_committed值重復率可能不夠高,導致從庫多線程復制的效率大大降低。在這種情況下,使用WRITESET多線程復制中的WRITESET、WRITESET_SESSION依賴模式能夠大大提高主庫二進制日志中last_committed值的重復率,大幅提高從庫的多線程復制效率。即便如此,也不建議在MySQL中寫入大事務,過大的事務在從庫的協調器線程做事務分發時極易成為瓶頸,進而使多線程復制的效率難以提高,而且大事務還會帶來諸多負面影響。

圖6-5
生成last_committed值的部分代碼如下(僅供參考,有興趣的讀者請自行翻閱源碼文件sql/rpl_trx_tracking.cc,這里不再贅述):





6.4.2 系統變量的配置
1. 主庫

2. 從庫

提示:更多信息可參考高鵬的“復制”專欄,登錄簡書網站搜索“第16節:基于WRITESET的并行復制方式”。