MySQL 8.0新特性:徹底解決困擾運(yùn)維的復(fù)制延遲問(wèn)題,你信嗎?
MySQL 8.0可以說(shuō)是MySQL發(fā)展歷史上里程碑式的一個(gè)版本,包括了多個(gè)重大更新,目前Generally Available版本已經(jīng)已經(jīng)發(fā)布,在此將介紹8.0版本中引入的一個(gè)重要的新特性——基于WriteSet的并行復(fù)制方案,此方案號(hào)稱是徹底解決困擾MySQL運(yùn)維人員多年的復(fù)制延遲問(wèn)題。
說(shuō)到并行復(fù)制,這里簡(jiǎn)單的回顧一下各個(gè)版本的MySQL復(fù)制的演進(jìn),以幫助理解8.0版本中對(duì)并行復(fù)制MTS的優(yōu)化。
一、MySQL主從復(fù)制模型
一切都要從MySQL的主從復(fù)制模型開(kāi)始說(shuō)起,下圖是最經(jīng)典的MySQL主從復(fù)制模型架構(gòu)圖:
MySQL復(fù)制模型
MySQL的主從架構(gòu)依賴于MySQL Binlog功能,Master節(jié)點(diǎn)上產(chǎn)生Binlog并將Binlog寫(xiě)入到Binlog文件中。
Slave節(jié)點(diǎn)上啟動(dòng)兩個(gè)線程:一個(gè)IO線程,從MySQL上撈取Binlog日志并寫(xiě)入到本地的RelayLog日志;另一個(gè)SQL線程,不斷從RelayLog日志中讀取日志,并解析執(zhí)行,這樣通過(guò)在主機(jī)和從機(jī)上增加幾個(gè)文件的順序讀寫(xiě)操作,就可以保證所有在主機(jī)上執(zhí)行過(guò)的SQL語(yǔ)句都在從機(jī)上一摸一樣的執(zhí)行過(guò)一遍。
復(fù)制延遲,指的就是一個(gè)事務(wù)在Master執(zhí)行完成以后,要多久以后才能在Slave上執(zhí)行完成。
由于對(duì)Binlog文件以及RelayLog文件的讀寫(xiě)均為順序操作,在生產(chǎn)環(huán)境中,Slave上的IO線程對(duì)Binlog文件的Dump操作是很少產(chǎn)生延遲的。實(shí)際上,從MySQL 5.5開(kāi)始,MySQL官方提供了半同步復(fù)制插件,每個(gè)事務(wù)的Binlog需要保證傳輸?shù)絊lave寫(xiě)入 RelayLog 后才能提交,這種架構(gòu)在主從之間提供了數(shù)據(jù)完整性,保證了主機(jī)在發(fā)生故障后從機(jī)可以擁有完整的數(shù)據(jù)副本。因此,復(fù)制延遲通常發(fā)生在SQL線程執(zhí)行的過(guò)程中。
從架構(gòu)圖上可以看到,最早的主從復(fù)制模型中,只有一個(gè)線程負(fù)責(zé)執(zhí)行Relaylog,也就是說(shuō)所有在主機(jī)上的操作,在從機(jī)上是串行回放的。這就帶來(lái)一個(gè)問(wèn)題,如果主上寫(xiě)入壓力比較大,那么從上的回放速度很有可能會(huì)一直跟不上主。(除此之外,MySQL的架構(gòu)決定了Binlog只有在Commit階段才會(huì)寫(xiě)入Binlog文件并Dump給從機(jī),這也導(dǎo)致主從事務(wù)必然有執(zhí)行延遲,這個(gè)問(wèn)題在大事務(wù)中體現(xiàn)的特別明顯,不過(guò)這個(gè)問(wèn)題就不在本文的討論范圍內(nèi)了)
既然主從延遲的問(wèn)題是單線程回放RelayLog太慢,那么減少主從延遲的方案自然就是提高從機(jī)上回放RelayLog的并行度。
二、5.7中的并行復(fù)制
1、Schema級(jí)別的并行復(fù)制
MySQL官方在5.6中引入了一個(gè)比較簡(jiǎn)單并行復(fù)制方案,其架構(gòu)如下:
(圖片來(lái)自姜承堯老師的博客)
紅色框部分為并行回放的關(guān)鍵,5.6中若開(kāi)啟并行回放的功能,便會(huì)啟動(dòng)多個(gè)WorkThread ,而原來(lái)負(fù)責(zé)回放的SQLThread會(huì)轉(zhuǎn)變成Coordinator角色,負(fù)責(zé)判斷事務(wù)能否并行執(zhí)行并分發(fā)給WorkThread。
如果事務(wù)分別屬于不同的Schema,并且不是DDL語(yǔ)句,同時(shí)沒(méi)有跨Schema操作,那么就可以并行回放,否則需要等所有Worker線程執(zhí)行完成后再執(zhí)行當(dāng)前日志中的內(nèi)容。
這種并行回放是Schema級(jí)別的并行,如果實(shí)例上有多個(gè)Schema將會(huì)因此收益,而如果實(shí)例上只有一個(gè)Schema,那么事務(wù)將無(wú)法并行回放,而且還會(huì)因多了分發(fā)的操作導(dǎo)致效率略微下降。而在實(shí)際應(yīng)用中,單庫(kù)多表才是更常見(jiàn)的情況。
2、基于Group Commit的并行復(fù)制
雖然5.6中的并行復(fù)制在大多數(shù)應(yīng)用場(chǎng)景中對(duì)回放速度的提升不大,但是該架構(gòu)卻成為了后來(lái)MySQL并行復(fù)制的基礎(chǔ)——即在Slave上并行回放RelayLog,SQL線程負(fù)責(zé)判斷能否并行回放,并分配給Work線程回放。
5.6 中引入Group Commit技術(shù),是為了解決事務(wù)提交的時(shí)候需要fsync導(dǎo)致并發(fā)性不夠而引入的。簡(jiǎn)單來(lái)說(shuō),就是由于事務(wù)提交時(shí)必須將Binlog寫(xiě)入到磁盤(pán)上而調(diào)用fsync,這是一個(gè)代價(jià)比較高的操作,事務(wù)并發(fā)提交的情況下,每個(gè)事務(wù)各自獲取日志鎖并進(jìn)行fsync會(huì)導(dǎo)致事務(wù)實(shí)際上以串行的方式寫(xiě)入Binlog文件,這樣就大大降低了事務(wù)提交的并發(fā)程度。
5.6中采用的Group Commit技術(shù)將事務(wù)的提交階段分成了Flush、Sync、Commit三個(gè)階段,每個(gè)階段維護(hù)一個(gè)隊(duì)列,并且由該隊(duì)列中***個(gè)線程負(fù)責(zé)執(zhí)行該步驟,這樣實(shí)際上就達(dá)到了一次可以將一批事務(wù)的Binlog fsync到磁盤(pán)的目的,這樣的一批同時(shí)提交的事務(wù)稱為同一個(gè)Group的事務(wù)。
Group Commit雖然是屬于并行提交的技術(shù),但是卻意外解決了從機(jī)上事務(wù)并行回放的一個(gè)難題——即如何判斷哪些事務(wù)可以并行回放。如果一批事務(wù)是同時(shí)Commit的,那么這些事務(wù)必然不會(huì)有互斥的持有鎖,也不會(huì)有執(zhí)行上的相互依賴,因此這些事務(wù)必然可以并行的回放。
因此MySQL 5.7 中引入了新的并行回放類型, 由參數(shù) slave_parallel_type決定,默認(rèn)值DATABASE將會(huì)采用5.6版本中的SCHEMA級(jí)別的并行回放,設(shè)置為L(zhǎng)OGICAL_LOCK則會(huì)采用基于GroupCommit的并行回放,同一個(gè)Group內(nèi)的事務(wù)將會(huì)在Slave上并行回放。
為了標(biāo)記事務(wù)所屬的組,MySQL 5.7 版本在產(chǎn)生 Binlog 日志時(shí)會(huì)有兩個(gè)特殊的值記錄在 Binlog Event 中,last_committed 和 sequence_number,其中 last_committed指的是該事務(wù)提交時(shí),上一個(gè)事務(wù)提交的編號(hào),sequence_number是事務(wù)提交的序列號(hào),在一個(gè)Binlog文件內(nèi)單調(diào)遞增。如果兩個(gè)事務(wù)的last_committed值一致,這兩個(gè)事務(wù)就是在一個(gè)組內(nèi)提交的。
- 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
如上binlog文件中,sequence_number 1-6的事務(wù)last_committed都是0 ,因此屬于同一個(gè)組,可以在slave上并行回放,7-12的last_committed都是6,也屬于同一個(gè)組,因此可以并行回放。
5.7 中引入的基于Logical_Lock極大的提高了在主機(jī)并發(fā)壓力比較大的情況下從機(jī)上的回放速度,基本上做到了主機(jī)上如何提交的,在從機(jī)上如何回放。
三、MySQL MGR中的WriteSet
雖然如此,在 5.7 中,基于邏輯時(shí)鐘 Logical_Clock 的并行復(fù)制仍然有不盡人意的地方,比如必須是在主上并行提交的事務(wù)才能在從上并行回放,如果主上并發(fā)壓力不大,那么就無(wú)法享受到并行復(fù)制帶來(lái)的好處。5.7 中引入了binlog_group_commit_sync_delay和binlog_group_commit_sync_no_delay_count兩個(gè)參數(shù),通過(guò)讓Binlog在執(zhí)行fsync前等待一小會(huì)來(lái)提高M(jìn)aster上組提交的比率。但是無(wú)論如何,從上并行回放的速度還是取決于主上并行提交的情況。
MySQL 8.0中引入了一種新的機(jī)制來(lái)判斷事務(wù)能否并行回放,通過(guò)檢測(cè)事務(wù)在運(yùn)行過(guò)程中是否存在寫(xiě)沖突來(lái)決定從機(jī)上的回放順序,這使得從機(jī)上的并發(fā)程度不再依賴于主機(jī)。
事實(shí)上,該機(jī)制在MySQL 5.7.20版本中就已經(jīng)悄悄的應(yīng)用了。5.7.20版本引入了一個(gè)重要的特性:Group Replication,通過(guò)Paxso協(xié)議在多個(gè)MySQL節(jié)點(diǎn)間分發(fā)binlog,使得一個(gè)事務(wù)必須在集群內(nèi)大多數(shù)節(jié)點(diǎn)(N/2+1)上提交成功才能提交。
為了支持多主寫(xiě)入,MySQL MRG在Binlog分發(fā)節(jié)點(diǎn)完成后,通過(guò)一個(gè)Certify階段來(lái)決定Binlog中的事務(wù)是否寫(xiě)入RelayLog中。這個(gè)過(guò)程中,Certify階段采用的就是WriteSet的方式驗(yàn)證事務(wù)之間是否存在沖突,同時(shí),在寫(xiě)入RelayLog時(shí)會(huì)將沒(méi)有沖突的事務(wù)的last_committed值設(shè)置為相同的值。
比如在5.7.20中,進(jìn)行如下操作:
- > -- create a group replication cluster.
- > STOP GROUP_REPLICATION; START GROUP_REPLICATION;
- Query OK, 0 rows affected (9.10 sec)
- > -- All the next commands on the primary member of the group:
- > CREATE DATABASE test_ws_mgr ;
- Query OK, 1 row affected (0.01 sec)
- > CREATE TABLE test_ws_mgr.test ( id int primary key auto_increment, str varchar(64) not null );
- Query OK, 1 row affected (0.01 sec)
- > INSERT INTO test_ws_mgr.test(`str`) VALUES ("a");
- Query OK, 1 row affected (0.01 sec)
- > INSERT INTO test_ws_mgr.test(`str`) VALUES ("b");
- Query OK, 1 row affected (0.01 sec)
- > INSERT INTO test_ws_mgr.test(`str`) VALUES ("c");
- Query OK, 1 row affected (0.01 sec)
以上代碼在一個(gè)MGR集群中創(chuàng)建了一個(gè)數(shù)據(jù)庫(kù)和一個(gè)InnoDB表,并插入了三條記錄。這個(gè)時(shí)候,查詢Primary節(jié)點(diǎn)上的Binlog可能會(huì)得到如下結(jié)果:
- # mysqlbinlog mysql-bin.N | grep last_ | sed -e 's/server id.*last/[...] last/' -e 's/.rbr_only.*/ [...]/'
- #180106 19:31:59 [...] last_committed=0 sequence_number=1 [...] -- CREATE DB
- #180106 19:32:02 [...] last_committed=1 sequence_number=2 [...] -- CREATE TB
- #180106 19:32:05 [...] last_committed=2 sequence_number=3 [...] -- INSERT a
- #180106 19:32:08 [...] last_committed=3 sequence_number=4 [...] -- INSERT b
- #180106 19:32:11 [...] last_committed=4 sequence_number=5 [...] -- INSERT c
可以看到,由于是在一個(gè)Session中,這些操作按著串行的順序有著不同的 last_committed,正常情況下,這些BinlogEvent應(yīng)該在從機(jī)上同樣以串行的方式回放。我們看一下在MGR集群中的RelayLog情況:
- # mysqlbinlog mysql-relay.N | grep -e last_ | sed -e 's/server id.*last/[...] last/' -e 's/.rbr_only.*/ [...]/'
- #180106 19:31:36 [...] last_committed=0 sequence_number=0 [...]
- #180106 19:31:36 [...] last_committed=1 sequence_number=2 [...] -- CREATE DB
- #180106 19:31:36 [...] last_committed=2 sequence_number=3 [...] -- CREATE TB
- #180106 19:31:36 [...] last_committed=3 sequence_number=4 [...] -- INSERT a
- #180106 19:31:36 [...] last_committed=3 sequence_number=5 [...] -- INSERT b
- #180106 19:31:36 [...] last_committed=3 sequence_number=6 [...] -- INSERT c
有趣的是,在Secondary節(jié)點(diǎn)的RelayLog中, 這些事務(wù)有著相同的last_committed值,也就是說(shuō)這些事務(wù)在MGR集群中,回放的時(shí)候可以以并行的方式回放。
MGR中,使用的正是WriteSet技術(shù)檢測(cè)不同事務(wù)之間是否存在寫(xiě)沖突,并重規(guī)劃了事務(wù)的并行回放,這一技術(shù)在8.0中被移到了Binlog生成階段,并采用到了主從復(fù)制的架構(gòu)中。
四、MySQL 8.0中的并行復(fù)制
說(shuō)了這么多,終于講到了MySQL 8.0 ,通過(guò)以上描述,讀者應(yīng)該對(duì)MySQL 8.0中并行復(fù)制的優(yōu)化的原理有了一個(gè)大致的輪廓。通過(guò)基于WriteSet的沖突檢測(cè),在主機(jī)上產(chǎn)生Binlog的時(shí)候,不再基于組提交,而是基于事務(wù)本身的更新沖突來(lái)確定并行關(guān)系。
1、相關(guān)的MySQL參數(shù)
在MySQL 8.0中,該版本引入了參數(shù)binlog_transaction_depandency_tracking用于控制如何決定事務(wù)的依賴關(guān)系。
該值有三個(gè)選項(xiàng):
- 默認(rèn)的COMMIT_ORDERE表示繼續(xù)使用5.7中的基于組提交的方式?jīng)Q定事務(wù)的依賴關(guān)系;
- WRITESET表示使用寫(xiě)集合來(lái)決定事務(wù)的依賴關(guān)系;
- 還有一個(gè)選項(xiàng)WRITESET_SESSION表示使用WriteSet來(lái)決定事務(wù)的依賴關(guān)系,但是同一個(gè)Session內(nèi)的事務(wù)不會(huì)有相同的last_committed值。
在代碼實(shí)現(xiàn)上,MySQL采用一個(gè)vector<uint64>的變量存儲(chǔ)已經(jīng)提交的事務(wù)的HASH值,所有已經(jīng)提交的事務(wù)的所修改的主鍵和非空的UniqueKey的值經(jīng)過(guò)HASH后與該vector中的值對(duì)比,由此來(lái)判斷當(dāng)前提交的事務(wù)是否與已經(jīng)提交的事務(wù)更新了同一行,并以此確定依賴關(guān)系。該向量的大小由參數(shù)binlog_transaction_dependency_history_size控制,取值范圍為1-1000000 ,初始默認(rèn)值為25000。
同時(shí)參數(shù)transaction_write_set_extraction控制檢測(cè)事務(wù)依賴關(guān)系時(shí)采用的HASH算法有三個(gè)取值OFF|XXHASH64|MURMUR32, 如binlog_transaction_depandency_tracking取值為WRITESET或WRITESET_SESSION,那么該值取值不能為OFF,且不能變更。
2、WriteSet 依賴檢測(cè)條件
WriteSet是通過(guò)檢測(cè)兩個(gè)事務(wù)是否更新了相同的記錄來(lái)判斷事務(wù)能否并行回放的,因此需要在運(yùn)行時(shí)保存已經(jīng)提交的事務(wù)信息以記錄歷史事務(wù)更新了哪些行。記錄歷史事務(wù)的參數(shù)為binlog_transaction_dependency_history_size。該值越大可以記錄更多的已經(jīng)提交的事務(wù)信息,不過(guò)需要注意的是,這個(gè)值并非指事務(wù)大小,而是指追蹤的事務(wù)更新信息的數(shù)量。在開(kāi)啟了WRITESET或WRITESET_SESSION后,MySQL按以下的方式標(biāo)識(shí)并記錄事務(wù)的更新。
如果事務(wù)當(dāng)前更新的行有主鍵(Primary Key),則將HASH(DB名、TABLE名、KEY名稱、KEY_VALUE1、KEY_VALUE2……)加入到當(dāng)前事務(wù)的vector write_set中。
如果事務(wù)當(dāng)前更新的行有非空的唯一鍵 (Unique Key Not NULL), 同樣將 HASH(DB名、TABLE名、KEY名、KEY_VALUE1)……加入到當(dāng)前事務(wù)的write_set中。
如果事務(wù)更新的行有外鍵約束( FOREIGN KEY )且不為空,則將該外鍵信息與VALUE 的HASH加到當(dāng)前事務(wù)的 write_set 中;如果事務(wù)當(dāng)前更新的表的主鍵是其它某個(gè)表的外鍵,則設(shè)置當(dāng)前事務(wù) has_related_foreign_key = true;如果事務(wù)更新了某一行且沒(méi)有任何數(shù)據(jù)被加入到 write_set 中,則標(biāo)記當(dāng)前事務(wù) has_missing_key = true。
在執(zhí)行沖突檢測(cè)的時(shí)候,先會(huì)檢查has_related_foreign_key和has_missing_key , 如果為true,則退到COMMIT_ORDER模式;否則,會(huì)依照事務(wù)的write_set中的HASH值與已提交的事務(wù)的write_set進(jìn)行比對(duì)。
如果沒(méi)有沖突,則當(dāng)前事務(wù)與***一個(gè)已提交的事務(wù)共享相同的last_commited,否則將從全局已提交的write_set中刪除那個(gè)沖突的事務(wù)之前提交的所有write_set,并退化到COMMIT_ORDER計(jì)算last_committed 。
在每一次計(jì)算完事務(wù)的last_committed值以后,需要去檢測(cè)當(dāng)前全局已經(jīng)提交的事務(wù)的write_set是否已經(jīng)超過(guò)了binlog_transaction_dependency_history_size設(shè)置的值,如果超過(guò),則清空已提交事務(wù)的全局write_set。
從檢測(cè)條件上看,該特性依賴于主鍵和唯一索引,如果事務(wù)涉及的表中沒(méi)有主鍵且沒(méi)有唯一非空索引,那么將無(wú)法從此特性中獲得性能的提升。除此之外,還需要將Binlog格式設(shè)置為Row格式。
3、性能提升
MySQL High Availability對(duì)開(kāi)啟了WriteSet的復(fù)制性能做了測(cè)試,這里直接將測(cè)試結(jié)果搬運(yùn)過(guò)來(lái),有興趣的可以直接訪問(wèn)原博客。
測(cè)試時(shí)通過(guò)Sysbench先在主機(jī)上執(zhí)行100W條事務(wù),然后開(kāi)啟Slave的復(fù)制線程,測(cè)試環(huán)境在Xeon E5-2699-V3 16核主機(jī)上執(zhí)行,以下是測(cè)試結(jié)果:
可以看到,在客戶端線程比較少的時(shí)候WRITESET具有***的性能,在只有一個(gè)連接時(shí)WRITESET_SESSION 和 COMMIT_ORDER差別不大。
五、結(jié)論
從MySQL Hight Availability的測(cè)試中可以看到,開(kāi)啟了基于WriteSet的事務(wù)依賴后,對(duì)Slave上RelayLog回放速度提升顯著。Slave上的 RelayLog回放速度將不再依賴于Master上提交時(shí)的并行程度,使得Slave上可以發(fā)揮其***的吞吐能力,這個(gè)特性在Slave上復(fù)制停止一段時(shí)間后恢復(fù)復(fù)制時(shí)尤其有效。
這個(gè)特性使得Slave上可能擁有比Master上更大的吞吐量,同時(shí)可能在保證事務(wù)依賴關(guān)系的情況下,在Slave上產(chǎn)生Master上沒(méi)有產(chǎn)生過(guò)的提交場(chǎng)景,事務(wù)的提交順序可能會(huì)在Slave上發(fā)生改變。
雖然在5.7的并行復(fù)制中就可能發(fā)生這種情況,不過(guò)在8.0中由于Slave上更高的并發(fā)能力,會(huì)使該場(chǎng)景更加常見(jiàn)。
通常情況下這不是什么大問(wèn)題,不過(guò)如果在Slave上做基于Binlog的增量備份,可能就需要保證在Slave上與Master上一致的提交順序,這種情況下可以開(kāi)啟slave_preserve_commit_order,這是一個(gè)5.7就引入的參數(shù),可以保證Slave上并行回放的線程按RelayLog中寫(xiě)入的順序Commit。
參考
- http://jfg-mysql.blogspot.jp/2018/01/an-update-on-write-set-parallel-replication-bug-fix-in-mysql-8-0.html
- http://jfg-mysql.blogspot.jp/2018/01/write-set-in-mysql-5-7-group-replication.html
- https://mysqlhighavailability.com/improving-the-parallel-applier-with-writeset-based-dependency-tracking/