一、MTS:多線程復制
MTS簡介
在MySQL 5.6版本之前,Slave服務器上有兩個線程I/O線程和SQL Thread線程。I/O線程負責接收二進制日志(Binary Log,更准確的說是二進制日志的event),SQL線程進行回放二進制日志。
如果在MySQL 5.6版本開啟並行復制功能,那么SQL線程就變為了coordinator(協調者)線程:
coordinator線程主要負責以前兩部分的內容:
- 若判斷可以並行執行,那么選擇worker線程執行事務的二進制日志。
- 若判斷不可以並行執行,如該操作是DDL,亦或者是事務跨schema操作,則等待所有的worker線程執行完成之后,再執行當前的日志。
這意味着coordinator線程並不是僅將日志發送給worker線程,自己也可以回放日志,但是所有可以並行的操作交付由worker線程完成。coordinator線程與worker是典型的生產者與消費者模型。
5.6版本存在的問題
上述機制實現了基於schema的並行復制存在兩個問題:
- crash safe功能不好做,因為可能之后執行的事務由於並行復制的關系先完成執行,那么當發生crash的時候,這部分的處理邏輯是比較復雜的。從代碼上看,5.6這里引入了Low-Water-Mark標記來解決該問題,從設計上看(WL#5569),其是希望借助於日志的冪等性來解決該問題,不過5.6的二進制日志回放還不能實現冪等性。
- 另一個最為關鍵的問題是這樣設計的並行復制效果並不高,如果用戶實例僅有一個庫,那么就無法實現並行回放,甚至性能會比原來的單線程更差。而單庫多表是比多庫多表更為常見的一種情形。
二、MySQL 5.7並行復制原理
MySQL 5.6基於庫的並行復制出來后,基本無人問津,在沉寂了一段時間之后,MySQL 5.7出來了,它的並行復制以一種全新的姿態出現在了DBA面前。MySQL 5.7才可稱為真正的並行復制,這其中最為主要的原因就是slave服務器的回放與master是一致的,即master服務器上是怎么並行執行的,那么slave上就怎樣進行並行回放。不再有庫的並行復制限制,對於二進制日志格式也無特殊的要求(基於庫的並行復制也沒有要求)。
從MySQL官方來看,其並行復制的原本計划是支持表級的並行復制和行級的並行復制,行級的並行復制通過解析ROW格式的二進制日志的方式來完成。
5.7中的並行復制是如何實現的?
組復制(group commit):通過對事務進行分組,優化減少了生成二進制日志所需的操作數。當事務同時提交時,它們將在單個操作中寫入到二進制日志中。如果事務能同時提交成功,那么它們就不會共享任何鎖,這意味着它們沒有沖突,因此可以在Slave上並行執行。所以通過在主機上的二進制日志中添加組提交信息,這些Slave可以並行地安全地運行事務。
首先,MySQL 5.7的並行復制基於一個前提,即所有已經處於prepare階段的事務,都是可以並行提交的。這些當然也可以在從庫中並行提交,因為處理這個階段的事務,都是沒有沖突的,該獲取的資源都已經獲取了。反過來說,如果有沖突,則后來的會等已經獲取資源的事務完成之后才能繼續,故而不會進入prepare階段。這是一種新的並行復制思路,完全擺脫了原來一直致力於為了防止沖突而做的分發算法,等待策略等復雜的而又效率底下的工作。MySQL 5.7並行復制的思想一言以蔽之:一個組提交(group commit)的事務都是可以並行回放,因為這些事務都已進入到事務的prepare階段,則說明事務之間沒有任何沖突(否則就不可能提交)。
根據以上描述,這里的重點是如何來定義哪些事務是處於prepare階段的?以及在生成的Binlog內容中該如何告訴Slave哪些事務是可以並行復制的?為了兼容MySQL 5.6基於庫的並行復制,5.7引入了新的變量slave-parallel-type,其可以配置的值有:DATABASE(默認值,基於庫的並行復制方式)、LOGICAL_CLOCK(基於組提交的並行復制方式)。
三、支持並行復制的GTID
(GTID全稱是Global Transaction Identifier,可簡化MySQL的主從切換以及Failover。GTID用於在binlog中唯一標識一個事務。當事務提交時,MySQL Server在寫binlog的時候,會先寫一個特殊的Binlog Event,類型為GTID_Event,指定下一個事務的GTID,然后再寫事務的Binlog。主從同步時GTID_Event和事務的Binlog都會傳遞到從庫,從庫在執行的時候也是用同樣的GTID寫binlog,這樣主從同步以后,就可通過GTID確定從庫同步到的位置了。)
那么如何知道事務是否在同一組中,又是一個問題,因為原版的MySQL並沒有提供這樣的信息。在MySQL 5.7版本中,其設計方式是將組提交的信息存放在GTID中。那么如果用戶沒有開啟GTID功能,即將參數gtid_mode設置為OFF呢?故MySQL 5.7又引入了稱之為Anonymous_Gtid(ANONYMOUS_GTID_LOG_EVENT)的二進制日志event類型,如:
mysql> SHOW BINLOG EVENTS in 'mysql-bin.000006';
+------------------+-----+----------------+-----------+-------------+--------------------
| Log_name | Pos | Event_type | Server_id | End_log_pos | Info |
+------------------+-----+----------------+-----------+-------------+--------------------
| mysql-bin.000006 | 4 | Format_desc | 88 | 123 | Server ver: 5.7.7-rc-debug-log, Binlog ver: 4 |
| mysql-bin.000006 | 123 | Previous_gtids | 88 | 194 | f11232f7-ff07-11e4-8fbb-00ff55e152c6:1-2 |
| mysql-bin.000006 | 194 | Anonymous_Gtid | 88 | 259 | SET @@SESSION.GTID_NEXT= 'ANONYMOUS' |
| mysql-bin.000006 | 259 | Query | 88 | 330 | BEGIN |
| mysql-bin.000006 | 330 | Table_map | 88 | 373 | table_id: 108 (aaa.t) |
| mysql-bin.000006 | 373 | Write_rows | 88 | 413 | table_id: 108 flags: STMT_END_F
PREVIOUS_GTIDS_LOG_EVENT
用於表示上一個binlog最后一個gitd的位置,每個binlog只有一個,當沒有開啟GTID時此事件為空。
GTID_LOG_EVENT
當開啟GTID時,每一個操作語句(DML/DDL)執行前就會添加一個GTID事件,記錄當前全局事務ID;同時在MySQL 5.7版本中,組提交信息也存放在GTID事件中,有兩個關鍵字段last_committed,sequence_number就是用來標識組提交信息的。在InnoDB中有一個全局計數器(global counter),在每一次存儲引擎提交之前,計數器值就會增加。在事務進入prepare階段之前,全局計數器的當前值會被儲存在事務中,這個值稱為此事務的commit-parent(也就是last_committed)。
這意味着在MySQL 5.7版本中即使不開啟GTID,每個事務開始前也是會存在一個Anonymous_Gtid,而這個Anonymous_Gtid事件中就存在着組提交的信息。反之,如果開啟了GTID后,就不會存在這個Anonymous_Gtid了,從而組提交信息就記錄在非匿名GTID事件中。
LOGICAL_CLOCK
然而,通過上述的SHOW BINLOG EVENTS,我們並沒有發現有關組提交的任何信息。但是通過mysqlbinlog工具,用戶就能發現組提交的內部信息:
$ mysqlbinlog mysql-bin.0000006 | grep last_committed
#150520 14:23:11 server id 88 end_log_pos 259 CRC32 0x4ead9ad6 GTID last_committed=0 sequence_number=1
#150520 14:23:11 server id 88 end_log_pos 1483 CRC32 0xdf94bc85 GTID last_committed=0 sequence_number=2
#150520 14:23:11 server id 88 end_log_pos 2708 CRC32 0x0914697b GTID last_committed=0 sequence_number=3
#150520 14:23:11 server id 88 end_log_pos 3934 CRC32 0xd9cb4a43 GTID last_committed=0 sequence_number=4
#150520 14:23:11 server id 88 end_log_pos 5159 CRC32 0x06a6f531 GTID last_committed=0 sequence_number=5
#150520 14:23:11 server id 88 end_log_pos 6386 CRC32 0xd6cae930 GTID last_committed=0 sequence_number=6
#150520 14:23:11 server id 88 end_log_pos 7610 CRC32 0xa1ea531c GTID last_committed=6 sequence_number=7
#150520 14:23:11 server id 88 end_log_pos 8834 CRC32 0x96864e6b GTID last_committed=6 sequence_number=8
#150520 14:23:11 server id 88 end_log_pos 10057 CRC32 0x2de1ae55 GTID last_committed=6 sequence_number=9
#150520 14:23:11 server id 88 end_log_pos 11280 CRC32 0x5eb13091 GTID last_committed=6 sequence_number=10
#150520 14:23:11 server id 88 end_log_pos 12504 CRC32 0x16721011 GTID last_committed=6 sequence_number=11
#150520 14:23:11 server id 88 end_log_pos 13727 CRC32 0xe2210ab6 GTID last_committed=6 sequence_number=12
#150520 14:23:11 server id 88 end_log_pos 14952 CRC32 0xf41181d3 GTID last_committed=12 sequence_number=13
可以發現MySQL 5.7二進制日志較之原來的二進制日志內容多了last_committed和sequence_number,last_committed表示事務提交的時候,上次事務提交的編號,如果事務具有相同的last_committed,表示這些事務都在一組內,可以進行並行的回放。例如上述last_committed為0的事務有6個,表示組提交時提交了6個事務,而這6個事務在從機是可以進行並行回放的,而sequence_number是順序增長的,每個事務對應一個序列號。另外,還有一個細節,其實每一個組的last_committed值,都是上一個組中事務的sequence_number最大值,也是本組中事務sequence_number最小值減1。同時這兩個值的有效作用域都在文件內,只要換一個文件(flush binary logs),這兩個值就都會從0開始計數。上述的last_committed和sequence_number代表的就是所謂的LOGICAL_CLOCK。
那么此時,還有一個重要的技術問題–MySQL是如何做到將這些事務分組的呢?要搞清楚這個問題,首先需要了解一下MySQL事務提交方式。
事務兩階段提交
事務的提交主要分為兩個主要步驟:
-
准備階段(Storage Engine(InnoDB) Transaction Prepare Phase)
此時SQL已經成功執行,並生成xid信息及redo和undo的內存日志。然后調用prepare方法完成第一階段,papare方法實際上什么也沒做,將事務狀態設為TRX_PREPARED,並將redo log刷磁盤。
-
提交階段(Storage Engine(InnoDB)Commit Phase)
(1) 記錄協調者日志,即Binlog日志。
如果事務涉及的所有存儲引擎的prepare都執行成功,則調用TC_LOG_BINLOG::log_xid方法將SQL語 句寫到 binlog(write()將binary log內存日志數據寫入文件系統緩存,fsync()將binary log文件系統緩 存日志數據永久寫入磁盤)。此時,事務已經鐵定要提交了。否則,調用ha_rollback_trans方法回滾事 務,而SQL語句實際上也不會寫到binlog。
(2)告訴引擎做commit。
最后,調用引擎的commit完成事務的提交。會清除undo信息,刷redo日志,將事務設TRX_NOT_STARTED狀態。
ordered commit流程介紹
關於MySQL是如何提交的,內部使用ordered_commit函數來處理的。先看它的邏輯圖,如下:
從圖中可以看到,只要事務提交(調用ordered_commit),就都會先加入隊列中。而提交有三個步驟,包括FLUSH、SYNC及COMMIT,相應地也有三個隊列。
首先要加入的是FLUSH隊列,如果某個事務加入時,隊列還是空的,則這個事務就擔任隊長,來代表其他事務執行提交操作。而在其他事務繼續加入時,就會發現此時隊列已經不為空了,那么這些事務就會等待隊長幫它們完成提交操作。
在上圖中,事務2-6都是這種坐享其成之輩,事務1就是隊長了。不過這里需要注意一點,不是說隊長會一直等待要提交的事務不停地加入,而是有一個時限,只有在這個時限之內成功加入到隊列的,才能幫它提交。這個時限就是從隊長加入開始,到它去處理隊列的時間,這個時間實際非常小,基本上就是程序從這行到哪行的一個過程,也沒有刻意去等待。
只要對長將這個隊列中的事務取出,其他事務就可以加入這個隊列了。第一個加入的還是隊長,但此時必須要等待。因為此時有事務正在做FLUSH,做完FLUSH之后,其他的對長才能帶着隊員做FLUSH。而在同一時刻,只能有一個組在做FLUSH。這就是上圖中所示的等待事務組2和等待事務組3,此時隊長會按照順序依次做FLUSH,做FLUSH的過程中,有一些重要的事務需要去做,如下:
- 要保證順序必須是提交加入到隊列的順序。
- 如果有新的事務提交,此時隊列為空,則可以加入到FLUSH隊列中。不過,因為此時FLUSH臨界區正在被占用,所以新事務組必須要等待。
- 給每個事務分配sequence_number,如果是第一個事務,則將這個組的last_committed設置為sequence_number-1.
- 將帶着last_committed與sequence_number的GTID事件FLUSH到Binlog文件中。
- 將當前事務所產生的Binlog內容FLUSH到Binlog文件中。
這樣,一個事務的FLUSH就完成了。接下來,依次做完組內所有事務的FLUSH,然后做SYNC。如果SYNC的臨界區是空的,則直接做SYNC操作,而如果已經有事務組在做,則必須要等待。同樣地,做完FLUSH之后,FLUSH臨界區會空閑出來,哪兒此時再等待這個臨界區的組就可以做FLUSH操作了。總而言之,每個步驟都會有事務組在做, 就像一個流水線一樣。完成一件產品需要三個工序,每個工序都可以批量來做,那么每個工序車間都不會閑着,都一直重復着相同的事情,最終每個產品都是以完全相同的順序完成。
到COMMIT時,實際做的是存儲引擎提交,參數binlog_order_commits會影響提交行為。如果設置為ON,那么此時提交就變為串行操作了,就以隊列的順序為提交順序。而如果設置為OFF,提交就不會在這里進行,而會在每個事務(包括隊長和隊員)做finish_commit(FINISH)時各自做存儲引擎的提交操作。組內每個事務做finish_commit是在隊長完成COMMIT工序之后進行,到步驟DONE時,便會喚醒每個等待提交完成的事務,告訴他們可以繼續了,那么每個事務就會去做finish_commit。而后,自己再去做finish_commit。這樣,一個組的事務就都按部就班地提交完成了。現在也可以知道,與這個組中同時在做提交的最多還有另外兩個事務,一個是在做FLUSH,一個是在做SYNC。
這就是order commit的原理,這也是LOGICAL_CLOCK並行復制的基礎。因為order commit使得所有的事務分了組,並且有了序列號,從庫拿到這些信息之后,就可以根據序號放心大膽地做分發了。
但是有沒有發現一個問題,每個組的事務數都沒有做過特殊處理。因為從時間上說,從隊長開始入隊,到取隊列中的所有事務出來,這之間的時間是非常非常小的,其實就是幾行代碼的事,也不會有任何費時間的操作,所以在這段時間內其實不會有多少個事務。只有在壓力很大,提交的事務非常多的時候,才會提高並發度(組內事務數變大)。不過這個問題也可以解釋得通,主庫壓力小的時候,從庫何必要那么大的並發度呢?只有主庫壓力大的時候,從庫才會延遲。
這種情況下也可以通過調整主服務器上的參數binlog_group_commit_sync_delay、binlog_group_commit_sync_no_delay_count。前者表示事務延遲提交多少時間來加大整個組提交的事務數量,從而減少進行磁盤刷盤sync的次數,單位為1/1000000秒,最大值1000000也就是1秒;后者表示組提交的事務數量湊齊多少此值時就跳出等待,然后提交事務,而無需等待binlog_group_commit_sync_delay的延遲時間;但是binlog_group_commit_sync_no_delay_count也不會超過binlog_group_commit_sync_delay設置。幾個參數都是為了增加主服務器組提交的事務比例,從而增大從機MTS的並行度。
三、從庫多線程復制分發原理
知道了order commit原理之后,現在很容易可以想到在從庫端是如何分發的,從庫以事務為單位做APPLY的,每個事務有一個GTID事件,從而都有一個last_committed及sequence_number值,分發原理如下。
-
從庫SQL線程拿到一個新事務,取出last_committed及sequence_number值。
-
判斷當前last_committed是不是大於當前已經執行的sequence_number的最小值(low water mark,下面稱lwm)。
-
如果大於,則說明上一個組的事務還沒有完成。此時等待lwm變大,直到last_committed與lwm相等,才可以繼續。
-
如果小於或等於,則說明當前事務與正在執行的組是同一個組,不需要等待。
-
SQL線程通過統計,找到一個空閑的worker線程,如果沒有空閑,則SQL線程轉入等待狀態,直到找到一個為止。
-
將當前事務打包,交給選定的worker,之后worker線程會去APPLY這個事務,此時的SQL線程就會處理下一個事務。
說明:上面的步驟是以事務為單位介紹的,其實實際處理中還是一個事件一個事件地分發。如果一個事務已經選定了worker,而新的event還在那個事務中,則直接交給那個worker處理即可。
從上面的分發原理來看,同時執行的都是具有相同last_committed值的事務,不同的只是后面的需要等前面做完了才能執行,這樣的執行方式有點如下圖所示:
可以看出,事務都是隨機分配到了worker線程中,但是執行的話,必須是一行一行地執行。一行事務個數越多,並行度越高,也說明主庫瞬時壓力越大。
四、MySQL 5.7並行復制測試
下圖顯示了開啟MTS后,Slave服務器的QPS。測試的工具是sysbench的單表全update測試,測試結果顯示在16個線程下的性能最好,從機的QPS可以達到25000以上,進一步增加並行執行的線程至32並沒有帶來更高的提升。而原單線程回放的QPS僅在4000左右,可見MySQL 5.7 MTS帶來的性能提升,而由於測試的是單表,所以MySQL 5.6的MTS機制則完全無能為力了。
五、並行復制配置與調優
master_info_repository
開啟MTS功能后,務必將參數master_info_repostitory設置為TABLE,這樣性能可以有50%~80%的提升。這是因為並行復制開啟后對於元master.info這個文件的更新將會大幅提升,資源的競爭也會變大。
slave_parallel_workers
若將slave_parallel_workers設置為0,則MySQL 5.7退化為原單線程復制,但將slave_parallel_workers設置為1,則SQL線程功能轉化為coordinator線程,但是只有1個worker線程進行回放,也是單線程復制。然而,這兩種性能卻又有一些的區別,因為多了一次coordinator線程的轉發,因此slave_parallel_workers=1的性能反而比0還要差,測試下還有20%左右的性能下降,如下圖所示:
MySQL 5.7新特性:並行復制原理(MTS)
這里其中引入了另一個問題,如果主機上的負載不大,那么組提交的效率就不高,很有可能發生每組提交的事務數量僅有1個,那么在從機的回放時,雖然開啟了並行復制,但會出現性能反而比原先的單線程還要差的現象,即延遲反而增大了。聰明的小伙伴們,有想過對這個進行優化嗎?
slave_preserve_commit_order
MySQL 5.7后的MTS可以實現更小粒度的並行復制,但需要將slave_parallel_type設置為LOGICAL_CLOCK,但僅僅設置為LOGICAL_CLOCK也會存在問題,因為此時在slave上應用事務的順序是無序的,和relay log中記錄的事務順序不一樣,這樣數據一致性是無法保證的,為了保證事務是按照relay log中記錄的順序來回放,就需要開啟參數slave_preserve_commit_order。開啟該參數后,執行線程將一直等待, 直到提交之前所有的事務。當從線程正在等待其他工作人員提交其事務時, 它報告其狀態為等待前面的事務提交。所以雖然MySQL 5.7添加MTS后,雖然slave可以並行應用relay log,但commit部分仍然是順序提交,其中可能會有等待的情況。
當開啟slave_preserve_commit_order參數后,slave_parallel_type只能是LOGICAL_CLOCK,如果你有使用級聯復制,那LOGICAL_CLOCK可能會使離master越遠的slave並行性越差。
但是經過測試,這個參數在MySQL 5.7.18中設置之后,也無法保證slave上事務提交的順序與relay log一致。 在MySQL 5.7.19設置后,slave上事務的提交順序與relay log中一致(所以生產要想使用MTS特性,版本大於等於MySQL 5.7.19才是安全的)。
要開啟enhanced multi-threaded slave其實很簡單,slave只需根據如下設置:
slave-parallel-type=LOGICAL_CLOCK
slave-parallel-workers=16
slave_pending_jobs_size_max = 2147483648
slave_preserve_commit_order=1
master_info_repository=TABLE
relay_log_info_repository=TABLE
relay_log_recovery=ON
在使用了MTS后,復制的監控依舊可以通過SHOW SLAVE STATUS\G,但是MySQL 5.7在performance_schema架構下多了以下這些元數據表,用戶可以更細力度的進行監控:
mysql> show tables like 'replication%';
+---------------------------------------------+
| Tables_in_performance_schema (replication%) |
+---------------------------------------------+
| replication_applier_configuration |
| replication_applier_status |
| replication_applier_status_by_coordinator |
| replication_applier_status_by_worker |
| replication_connection_configuration |
| replication_connection_status |
| replication_group_member_stats |
| replication_group_members |
+---------------------------------------------+
8 rows in set (0.00 sec)
通過replication_applier_status_by_worker可以看到worker進程的工作情況:
mysql> select * from replication_applier_status_by_worker;
+--------------+-----------+-----------+---------------+--------------------------------------------+-------------------+--------------------+----------------------+
| CHANNEL_NAME | WORKER_ID | THREAD_ID | SERVICE_STATE | LAST_SEEN_TRANSACTION | LAST_ERROR_NUMBER | LAST_ERROR_MESSAGE | LAST_ERROR_TIMESTAMP |
+--------------+-----------+-----------+---------------+--------------------------------------------+-------------------+--------------------+----------------------+
| | 1 | 32 | ON | 0d8513d8-00a4-11e6-a510-f4ce46861268:96604 | 0 | | 0000-00-00 00:00:00 |
| | 2 | 33 | ON | 0d8513d8-00a4-11e6-a510-f4ce46861268:97760 | 0 | | 0000-00-00 00:00:00 |
+--------------+-----------+-----------+---------------+--------------------------------------------+-------------------+--------------------+----------------------+
2 rows in set (0.00 sec)
那么怎樣知道從機MTS的並行程度又是一個難度不小。簡單的一種方法(姜總給出的),可以使用performance_schema庫來觀察,比如下面這條SQL可以統計每個Worker Thread執行的事務數量,在此基礎上再做一個聚合分析就可得出每個MTS的並行度:
SELECT thread_id,count_star FROM performance_schema.events_transactions_summary_by_thread_by_event_name
WHERE thread_id IN (
SELECT thread_id FROM performance_schema.replication_applier_status_by_worker);
如果線程並行度太高,不夠平均,其實並行效果並不會好,可以試着優化。這種場景下,可以通過調整主服務器上的參數binlog_group_commit_sync_delay、binlog_group_commit_sync_no_delay_count。前者表示延遲多少時間提交事務,后者表示組提交事務湊齊多少個事務再一起提交。總體來說,都是為了增加主服務器組提交的事務比例,從而增大從機MTS的並行度。
雖然MySQL 5.7推出的Enhanced Multi-Threaded Slave在一定程度上解決了困擾MySQL長達數十年的復制延遲問題。然而,目前MTS機制基於組提交實現,簡單來說在主上是怎樣並行執行的,從服務器上就怎么回放。這里存在一個可能,即若主服務器的並行度不夠,則從機的並行機制效果就會大打折扣。MySQL 8.0最新的基於writeset的MTS才是最終的解決之道。即兩個事務,只要更新的記錄沒有重疊(overlap),則在從機上就可並行執行,無需在一個組,即使主服務器單線程執行,從服務器依然可以並行回放。相信這是最完美的解決之道,MTS的最終形態。
最后,如果MySQL 5.7要使用MTS功能,必須使用最新版本,最少升級到5.7.19版本,修復了很多Bug。