探索MySQL的高效數(shù)據(jù)同步:并行復(fù)制原理
在聊 MySQL 的并行原理之前,我們要了解什么會(huì)有這個(gè)概念,以及這個(gè)要解決什么問(wèn)題。這就不得不提到主從延遲這個(gè)概念了。
什么是數(shù)據(jù)庫(kù)的主從延遲,如何解決?
數(shù)據(jù)庫(kù)的主從延遲指的是主服務(wù)器(Master)和從服務(wù)器(Slave)之間數(shù)據(jù)同步的時(shí)間差或延遲。導(dǎo)致主從延遲的常見(jiàn)原因包括:
- 網(wǎng)絡(luò)延遲:主節(jié)點(diǎn)和從節(jié)點(diǎn)之間的網(wǎng)絡(luò)延遲導(dǎo)致數(shù)據(jù)復(fù)制延遲。
- 從節(jié)點(diǎn)性能問(wèn)題:從服務(wù)器性能不足,如 CPU、內(nèi)存、磁盤(pán)資源不足,無(wú)法及時(shí)處理復(fù)制事件,導(dǎo)致延遲增加。
- 復(fù)制線程不足:從節(jié)點(diǎn)的復(fù)制線程不足或配置不合理,無(wú)法有效地處理接收到的復(fù)制事件,造成數(shù)據(jù)回放速度較慢,進(jìn)而導(dǎo)致主從延遲。
解決主從延遲可以考慮以下幾個(gè)方面:
- 優(yōu)化網(wǎng)絡(luò):確保主從節(jié)點(diǎn)之間的網(wǎng)絡(luò)連接穩(wěn)定,盡量在同一城市或同一數(shù)據(jù)中心部署,以減小網(wǎng)絡(luò)延遲。
- 提升從服務(wù)器性能:增加從服務(wù)器的硬件資源,例如增加 CPU 核數(shù)、內(nèi)存容量和改善磁盤(pán)性能,以提升從服務(wù)器處理復(fù)制事件的能力。
- 并行復(fù)制:利用 MySQL 提供的并行復(fù)制能力,同時(shí)處理多個(gè)復(fù)制事件,提高復(fù)制效率,從而降低主從延遲。
這些措施可以有助于減少主從延遲,提升數(shù)據(jù)庫(kù)復(fù)制的效率和穩(wěn)定性。
上面提到了并行復(fù)制這個(gè)概念,接下來(lái)我們就簡(jiǎn)單聊聊并行復(fù)制。
在 MySQL 的主從復(fù)制中,我們已經(jīng)介紹過(guò)其基本原理。在復(fù)制過(guò)程中,主庫(kù)的 binlog 會(huì)不斷地同步到從庫(kù),而從庫(kù)則通過(guò)一個(gè) SQL 線程不斷地拉取并重放這些 SQL 語(yǔ)句。然而,當(dāng)日志內(nèi)容過(guò)多時(shí),單個(gè)線程的執(zhí)行會(huì)產(chǎn)生延遲,導(dǎo)致主從延遲。
為了解決這一問(wèn)題,MySQL 提供了并行復(fù)制的方案。在多個(gè)版本中,MySQL 相繼推出了多種并行復(fù)制的方案:
- MySQL 5.6 引入了基于庫(kù)級(jí)別的并行復(fù)制。
- MySQL 5.7 推出了基于組提交的并行復(fù)制。
- MySQL 8.0 推出了基于 WRITESET 的并行復(fù)制。
庫(kù)級(jí)別并行復(fù)制
在 MySQL 5.6 中,并行復(fù)制是基于 Schema(即基于庫(kù))的,可以配置多個(gè)庫(kù)并行進(jìn)行復(fù)制。每個(gè)庫(kù)都可以有自己的復(fù)制線程,并行處理來(lái)自不同庫(kù)的寫(xiě)入,從而提升并行復(fù)制的性能和效率。
然而,實(shí)際上大多數(shù)業(yè)務(wù)都是單庫(kù)的,這使得這一方案在推出后并未獲得廣大開(kāi)發(fā)者和 DBA 的認(rèn)可,認(rèn)為其實(shí)用性不足。
組提交的的并行復(fù)制
由于 MySQL 5.6 的并行復(fù)制飽受詬病,MySQL 5.7 推出了基于組提交的并行復(fù)制,這才是真正意義上的并行復(fù)制,即著名的 MTS(Enhanced Multi-Threaded Slave)??蓞⒖脊俜轿臋n:
https://dev.mysql.com/blog-archive/multi-threaded-replication-performance-in-mysql-5-7/
這里先簡(jiǎn)單了解下組提交,然后繼續(xù)往下看。
在介紹組提交時(shí)我們提到,一個(gè)組中的多個(gè)事務(wù)在處于 Prepare 階段之后,才會(huì)被優(yōu)化成組提交。這意味著,如果多個(gè)事務(wù)能夠在同一個(gè)組內(nèi)提交,這些事務(wù)在鎖上一定是沒(méi)有沖突的。
binlog_transaction_dependency_tracking = WRITESET # COMMIT_ORDER
transaction_write_set_extraction = XXHASH64
換句話說(shuō),這幾個(gè)事務(wù)修改的記錄一定不是同一行,因此它們之間才能互不影響地同時(shí)進(jìn)入 Prepare 階段,并進(jìn)行組提交。
那么,沒(méi)有沖突的多條 SQL,是不是就可以在主備同步過(guò)程中,在備庫(kù)上并行執(zhí)行回放呢?
答案是肯定的。因?yàn)橐粋€(gè)組中的多條 SQL 之間互不影響,無(wú)論先執(zhí)行哪一條,結(jié)果都是相同的。
因此,Slave 可以使用多個(gè) SQL 線程來(lái)并行執(zhí)行一個(gè)組提交中的多條 SQL,從而提高效率,減少主從延遲。
基于 WRITESET 的并行復(fù)制
前面的組提交大大提升了主從復(fù)制的效率,但它有一個(gè)特點(diǎn),即依賴于主庫(kù)的并行度。如果主庫(kù)的并發(fā)度較高,才可以進(jìn)行組提交,從而利用組提交的并行復(fù)制優(yōu)化。
如果主庫(kù)的 SQL 執(zhí)行并不頻繁,時(shí)間間隔可能會(huì)超過(guò)組提交的參數(shù)閾值,就不會(huì)進(jìn)行組提交,這樣在復(fù)制時(shí)就無(wú)法使用并行復(fù)制。
為了解決這個(gè)問(wèn)題,MySQL 8.0 引入了基于 WriteSet 的并行復(fù)制。在這種情況下,即使主庫(kù)是串行提交的事務(wù),只要這些事務(wù)之間互不沖突,備庫(kù)就可以并行回放,從而提升復(fù)制效率。
開(kāi)啟 WRITESET:
binlog_transaction_dependency_tracking = WRITESET # COMMIT_ORDER
transaction_write_set_extraction = XXHASH64
實(shí)際上,WriteSet 是一個(gè)集合,使用的是 C++ STL 中的 set 容器。
std::set<uint64> write_set_unique;
集合中的每一個(gè)元素都是哈希值,這個(gè)哈希值與 transaction_write_set_extraction 參數(shù)指定的算法有關(guān)(可選值為 OFF、MURMUR32、XXHASH64,默認(rèn)值為 XXHASH64),其來(lái)源是行數(shù)據(jù)的主鍵和唯一鍵。
WriteSet 通過(guò)檢測(cè)兩個(gè)事務(wù)是否更新了相同的記錄來(lái)判斷事務(wù)能否并行回放,因此需要在運(yùn)行時(shí)保存已提交的事務(wù)信息以記錄歷史事務(wù)更新了哪些行。在進(jìn)行更新時(shí),需要進(jìn)行沖突檢測(cè),將新更新的記錄計(jì)算出的哈希值與 WriteSet 進(jìn)行比較,如果不存在沖突,則認(rèn)為是不沖突的,這樣就可以共用同一個(gè) last_committed。
last_committed 指的是該事務(wù)提交時(shí),上一個(gè)事務(wù)提交的編號(hào)。
就這樣,能夠確保同一個(gè) write_set 中的變更都是不沖突的,因此可以通過(guò)多個(gè)線程并行地回放 SQL。