MySQL 5.7並行復制時代
眾所周知,MySQL的復制延遲是一直被詬病的問題之一,然而在Inside君之前的兩篇博客中(1,2)中都已經提到了MySQL 5.7版本已經支持“真正”的並行復制功能,官方稱為為enhanced multi-threaded slave(簡稱MTS),因此復制延遲問題已經得到了極大的改進,甚至在Inside君所在的網易電商應用中已經完全消除了之前延遲長達幾小時的問題。然而,Inside君發現還是有很部分小伙伴不了解這個足以載入史冊的“偉大”的特性,故作分享。總之, 5.7版本后,復制延遲問題永不存在 。
MySQL 5.6並行復制架構
誠然,MySQL 5.6版本也支持所謂的並行復制,但是其並行只是基於schema的,也就是基於庫的。如果用戶的MySQL數據庫實例中存在多個schema,對於從機復制的速度的確可以有比較大的幫助。MySQL 5.6並行復制的架構如下所示:
在上圖的紅色框框部分就是實現並行復制的關鍵所在。在MySQL 5.6版本之前,Slave服務器上有兩個線程I/O線程和SQL線程。I/O線程負責接收二進制日志(更准確的說是二進制日志的event),SQL線程進行回放二進制日志。如果在MySQL 5.6版本開啟並行復制功能,那么SQL線程就變為了coordinator線程,coordinator線程主要負責以前兩部分的內容:
- 若判斷可以並行執行,那么選擇worker線程執行事務的二進制日志
- 若判斷不可以並行執行,如該操作是DDL,亦或者是事務跨schema操作,則等待所有的worker線程執行完成之后,再執行當前的日志
這意味着 coordinator線程並不是僅將日志發送給worker線程,自己也可以回放日志,但是所有可以並行的操作交付由worker線程完成。coordinator線程與worker是典型的生產者與消費者模型。
(疑問:這里是在判斷不可以並行執行時,在等待所有的worker線程執行完成后,是由coordinator執行還是由worker進程非並行進行。從上面兩句話中看不出coordinator進程回放日志。)
上述機制實現了基於schema的並行復制存在兩個問題,首先是crash safe功能不好做,因為可能之后執行的事務由於並行復制的關系先完成執行,那么當發生crash的時候,這部分的處理邏輯是比較復雜的。從代碼上看,5.6這里引入了Low-Water-Mark標記來解決該問題,從設計上看( WL#5569 ),其是希望借助於日志的冪等性來解決該問題,不過5.6的二進制日志回放還不能實現冪等性。另一個最為關鍵的問題是這樣設計的並行復制效果並不高,如果用戶實例僅有一個庫,那么就無法實現並行回放,甚至性能會比原來的單線程更差。而 單庫多表是比多庫多表更為常見的一種情形 。
MySQL 5.7並行復制原理
MySQL 5.7基於組提交的並行復制
MySQL 5.7才可稱為真正的並行復制,這其中最為主要的原因就是slave服務器的回放與主機是一致的即master服務器上是怎么並行執行的slave上就怎樣進行並行回放。不再有庫的並行復制限制,對於二進制日志格式也無特殊的要求(基於庫的並行復制也沒有要求)。
從MySQL官方來看,其並行復制的原本計划是支持表級的並行復制和行級的並行復制,行級的並行復制通過解析ROW格式的二進制日志的方式來完成, WL#4648 。但是最終出現給小伙伴的確是在開發計划中稱為:MTS: Prepared transactions slave parallel applier,可見: WL#6314 。該並行復制的思想最早是由MariaDB的Kristain提出,並已在MariaDB 10中出現,相信很多選擇MariaDB的小伙伴最為看重的功能之一就是並行復制。
MySQL 5.7並行復制的思想簡單易懂,一言以蔽之: 一個組提交的事務都是可以並行回放 ,因為這些事務都已進入到事務的prepare階段,則說明事務之間沒有任何沖突(否則就不可能提交)。
為了兼容MySQL 5.6基於庫的並行復制,5.7引入了新的變量slave-parallel-type,其可以配置的值有:
- DATABASE:默認值,基於庫的並行復制方式
- LOGICAL_CLOCK:基於組提交的並行復制方式
支持並行復制的GTID
如何知道事務是否在一組中,又是一個問題,因為原版的MySQL並沒有提供這樣的信息。在MySQL 5.7版本中,其設計方式是將組提交的信息存放在GTID中。那么如果用戶沒有開啟GTID功能,即將參數gtid_mode設置為OFF呢?故MySQL 5.7又引入了稱之為Anonymous_Gtid的二進制日志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 | ......
這意味着在 MySQL 5.7版本中即使不開啟GTID,每個事務開始前也是會存在一個Anonymous_Gtid ,而這GTID中就存在着組提交的信息。
LOGICAL_CLOCK
然而,通過上述的SHOW BINLOG EVENTS,我們並沒有發現有關組提交的任何信息。但是通過mysqlbinlog工具,用戶就能發現組提交的內部信息:
root@localhost:~# 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 ...
可以發現較之原來的二進制日志內容多了last_committed和sequence_number,last_committed表示事務提交的時候,上次事務提交的編號,如果事務具有相同的last_committed,表示這些事務都在一組內,可以進行並行的回放。例如上述last_committed為0的事務有6個,表示組提交時提交了6個事務,而這6個事務在從機是可以進行並行回放的。
上述的last_committed和sequence_number代表的就是所謂的LOGICAL_CLOCK。先來看源碼中對於LOGICAL_CLOCK的定義:
class Logical_clock { private: int64 state; /* Offset is subtracted from the actual "absolute time" value at logging a replication event. That is the event holds logical timestamps in the "relative" format. They are meaningful only in the context of the current binlog. The member is updated (incremented) per binary log rotation. */ int64 offset; ......
state是一個自增的值,offset在每次二進制日志發生rotate時更新,記錄發生rotate時的state值。其實state和offset記錄的是全局的計數值,而存在二進制日志中的僅是當前文件的相對值。使用LOGICAL_CLOCK的場景如下:
class MYSQL_BIN_LOG: public TC_LOG { ... public: /* Committed transactions timestamp */ Logical_clock max_committed_transaction; /* "Prepared" transactions timestamp */ Logical_clock transaction_counter; ...
可以看到在類MYSQL_BIN_LOG中定義了兩個Logical_clock的變量:
- max_committed_transaction:記錄上次組提交時的logical_clock,代表上述mysqlbinlog中的last_committed
- transaction_counter:記錄當前組提交中各事務的logcial_clock,代表上述mysqlbinlog中的sequence_number
並行復制測試
下圖顯示了開啟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這個文件的更新將會大幅提升,資源的競爭也會變大。在之前 InnoSQL 的版本中,添加了參數來控制刷新master.info這個文件的頻率,甚至可以不刷新這個文件。因為刷新這個文件是沒有必要的,即根據master-info.log這個文件恢復本身就是不可靠的。在MySQL 5.7中,Inside君推薦將master_info_repository設置為TABLE,來減小這部分的開銷。
slave_parallel_workers
若將slave_parallel_workers設置為0,則MySQL 5.7退化為原單線程復制,但將slave_parallel_workers設置為1,則SQL線程功能轉化為coordinator線程,但是只有1個worker線程進行回放,也是單線程復制。然而,這兩種性能卻又有一些的區別,因為多了一次coordinator線程的轉發,因此slave_parallel_workers=1的性能反而比0還要差,在Inside君的測試下還有20%左右的性能下降,如下圖所示:
這里其中引入了另一個問題,如果主機上的負載不大,那么組提交的效率就不高,很有可能發生每組提交的事務數量僅有1個,那么在從機的回放時, 雖然開啟了並行復制,但會出現性能反而比原先的單線程還要差的現象,即延遲反而增大了 。聰明的小伙伴們,有想過對這個進行優化嗎?
Enhanced Multi-Threaded Slave配置
說了這么多,要開啟enhanced multi-threaded slave其實很簡單,只需根據如下設置:
# slave
slave-parallel-type=LOGICAL_CLOCK slave-parallel-workers=16 master_info_repository=TABLE relay_log_info_repository=TABLE relay_log_recovery=ON
並行復制監控
復制的監控依舊可以通過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)
總結
MySQL 5.7推出的Enhanced Multi-Threaded Slave解決了困擾MySQL長達數十年的復制延遲問題,再次提醒一些無知的PostgreSQL用戶,不要再停留在之前對於MySQL的印象,物理復制也不一定肯定比邏輯復制有優勢,而MySQL 5.7的MTS已經完全可以解決延遲問題。anyway,和Inside君一起見證划時代MySQL 5.7 GA版本的降臨吧.
注:
1、Coordinator thread on slave dispatches work across several worker threads。Each worker thread commits trx in isolation。
2、mysql 5.6的MTS是基於庫級別的並行,當有多個數據庫時,可以將slave_parallel_workers設置為數據庫的數量,為了避免新建庫后來回修改,也可以將該參數設置的大一些。設置為庫級別的事務時,不允許這樣做,會報錯。
3、mysql 5.7 后的MTS可以實現更小粒度的並行復制,但需要將slave_parallel_type設置為LOGICAL_CLOCK,但僅僅設置為LOGICAL_CLOCK也會存在問題,因為此時在slave上應用事務的順序是無序的,和relay log中記錄的事務順序不一樣,這樣數據一致性是無法保證的,為了保證事務是按照relay log中記錄的順序來回放,就需要開啟參數slave_preserve_commit_order。開啟該參數后,the executing thread waits until all previous transactions are committed before committing. While the slave thread is waiting for other workers to commit their transactions it reports its status as Waiting for preceding transaction to commit
.
所以雖然mysql5.7添加MTS后,雖然slave可以並行應用relay log,但commit部分仍然是順序提交,其中可能會有等待的情況。
當開啟slave_preserve_commit_order參數后,slave_parallel_type只能是LOGICAL_CLOCK,如果你有使用級聯復制,那LOGICAL_CLOCK可能會使離master越遠的slave並行性越差。
Regardless of the value of this variable, there is no special configuration required on the master. When slave_preserve_commit_order=1
, you can only use LOGICAL_CLOCK
. If your replication topology uses multiple levels of slaves, LOGICAL_CLOCK
may achieve less parallelization for each level the slave is away from the master.
4、5.7中會在binlog中額外加入元數據,來划分那些事務可以並行執行。
Additional metadata is stored in the binlogs to identify transactions that can be applied in parallel
而slave則通過 coordinator 線程從relay log中抽取元數據,進而分發給worker線程並行執行的事務組。
The coordinator thread is able to extract the metadata from the relay logs to dispatch the transactions across workers
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> 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)
其他各表的使用參考官方文檔。
Controls how many microseconds the binary log commit waits before synchronizing the binary log file to disk. By default binlog-group-commit-sync-delay
is set to 0, meaning that there is no delay. Setting binlog-group-commit-sync-delay
to a microsecond delay enables more transactions to be synchronized together to disk at once, reducing the overall time to commit a group of transactions because the larger groups require fewer time units per group. With the correct tuning, this can increase slave performance without compromising the master's throughput.
8、討論技術就討論技術,沒必要踩低別人來抬高自己,而且不了解pg就妄論pg是可笑的。類比Linus那句話:“Talk is cheap,show your test.”
參考:
http://mp.weixin.qq.com/s?__biz=MjM5MjIxNDA4NA==&mid=205236417&idx=1&sn=15281c834348911cea106478aa819175&scene=23&srcid=0525zwrE6gRYCIPgKxoq40iN#rd
http://blog.booking.com/better_parallel_replication_for_mysql.html
https://www.percona.com/resources/technical-presentations/multi-threaded-replication-mysql-56-and-57-percona-technical-mysql
https://dev.mysql.com/doc/refman/5.7/en/replication-options-slave.html#option_mysqld_slave-parallel-type
https://dev.mysql.com/doc/refman/5.7/en/replication-options-slave.html#sysvar_slave_preserve_commit_order
https://dev.mysql.com/doc/refman/5.7/en/replication-options-binary-log.html#sysvar_binlog_group_commit_sync_delay