26 | 備庫為什么會(huì)延遲好幾個(gè)小時(shí)?

[TOC]
在上一篇文章中,我和你介紹了幾種可能導(dǎo)致備庫延遲的原因。你會(huì)發(fā)現(xiàn),這些場景里,不論是偶發(fā)性的查詢壓力,還是備份,對(duì)備庫延遲的影響一般是分鐘級(jí)的,而且在備庫恢復(fù)正常以后都能夠追上來。

但是,如果備庫執(zhí)行日志的速度持續(xù)低于主庫生成日志的速度,那這個(gè)延遲就有可能成了小時(shí)級(jí)別。而且對(duì)于一個(gè)壓力持續(xù)比較高的主庫來說,備庫很可能永遠(yuǎn)都追不上主庫的節(jié)奏。

這就涉及到今天我要給你介紹的話題:備庫并行復(fù)制能力。

為了便于你理解,我們再一起看一下第 24 篇文章《MySQL 是怎么保證主備一致的?》的主備流程圖。

image.png

談到主備的并行復(fù)制能力,我們要關(guān)注的是圖中黑色的兩個(gè)箭頭。一個(gè)箭頭代表了客戶端寫入主庫,另一箭頭代表的是備庫上 sql_thread 執(zhí)行中轉(zhuǎn)日志(relay log)。如果用箭頭的粗細(xì)來代表并行度的話,那么真實(shí)情況就如圖 1 所示,第一個(gè)箭頭要明顯粗于第二個(gè)箭頭。

在主庫上,影響并發(fā)度的原因就是各種鎖了。由于 InnoDB 引擎支持行鎖,除了所有并發(fā)事務(wù)都在更新同一行(熱點(diǎn)行)這種極端場景外,它對(duì)業(yè)務(wù)并發(fā)度的支持還是很友好的。所以,你在性能測試的時(shí)候會(huì)發(fā)現(xiàn),并發(fā)壓測線程 32 就比單線程時(shí),總體吞吐量高。

而日志在備庫上的執(zhí)行,就是圖中備庫上 sql_thread 更新數(shù)據(jù) (DATA) 的邏輯。如果是用單線程的話,就會(huì)導(dǎo)致備庫應(yīng)用日志不夠快,造成主備延遲。

在官方的 5.6 版本之前,MySQL 只支持單線程復(fù)制,由此在主庫并發(fā)高、TPS 高時(shí)就會(huì)出現(xiàn)嚴(yán)重的主備延遲問題。

從單線程復(fù)制到最新版本的多線程復(fù)制,中間的演化經(jīng)歷了好幾個(gè)版本。接下來,我就跟你說說 MySQL 多線程復(fù)制的演進(jìn)過程。

其實(shí)說到底,所有的多線程復(fù)制機(jī)制,都是要把圖 1 中只有一個(gè)線程的 sql_thread,拆成多個(gè)線程,也就是都符合下面的這個(gè)模型:

image.png

圖 2 中,coordinator 就是原來的 sql_thread, 不過現(xiàn)在它不再直接更新數(shù)據(jù)了,只負(fù)責(zé)讀取中轉(zhuǎn)日志和分發(fā)事務(wù)。真正更新日志的,變成了 worker 線程。而 work 線程的個(gè)數(shù),就是由參數(shù) slave_parallel_workers 決定的。根據(jù)我的經(jīng)驗(yàn),把這個(gè)值設(shè)置為 8~16 之間最好(32 核物理機(jī)的情況),畢竟備庫還有可能要提供讀查詢,不能把 CPU 都吃光了。

接下來,你需要先思考一個(gè)問題:事務(wù)能不能按照輪詢的方式分發(fā)給各個(gè) worker,也就是第一個(gè)事務(wù)分給 worker_1,第二個(gè)事務(wù)發(fā)給 worker_2 呢?

其實(shí)是不行的。因?yàn)?,事?wù)被分發(fā)給 worker 以后,不同的 worker 就獨(dú)立執(zhí)行了。但是,由于 CPU 的調(diào)度策略,很可能第二個(gè)事務(wù)最終比第一個(gè)事務(wù)先執(zhí)行。而如果這時(shí)候剛好這兩個(gè)事務(wù)更新的是同一行,也就意味著,同一行上的兩個(gè)事務(wù),在主庫和備庫上的執(zhí)行順序相反,會(huì)導(dǎo)致主備不一致的問題。

接下來,請你再設(shè)想一下另外一個(gè)問題:同一個(gè)事務(wù)的多個(gè)更新語句,能不能分給不同的 worker 來執(zhí)行呢?

答案是,也不行。舉個(gè)例子,一個(gè)事務(wù)更新了表 t1 和表 t2 中的各一行,如果這兩條更新語句被分到不同 worker 的話,雖然最終的結(jié)果是主備一致的,但如果表 t1 執(zhí)行完成的瞬間,備庫上有一個(gè)查詢,就會(huì)看到這個(gè)事務(wù)“更新了一半的結(jié)果”,破壞了事務(wù)邏輯的隔離性。

所以,coordinator 在分發(fā)的時(shí)候,需要滿足以下這兩個(gè)基本要求:

  1. 不能造成更新覆蓋。這就要求更新同一行的兩個(gè)事務(wù),必須被分發(fā)到同一個(gè) worker 中。
  2. 同一個(gè)事務(wù)不能被拆開,必須放到同一個(gè) worker 中。

各個(gè)版本的多線程復(fù)制,都遵循了這兩條基本原則。接下來,我們就看看各個(gè)版本的并行復(fù)制策略。

MySQL 5.5 版本的并行復(fù)制策略

官方 MySQL 5.5 版本是不支持并行復(fù)制的。但是,在 2012 年的時(shí)候,我自己服務(wù)的業(yè)務(wù)出現(xiàn)了嚴(yán)重的主備延遲,原因就是備庫只有單線程復(fù)制。然后,我就先后寫了兩個(gè)版本的并行策略。

這里,我給你介紹一下這兩個(gè)版本的并行策略,即按表分發(fā)策略和按行分發(fā)策略,以幫助你理解 MySQL 官方版本并行復(fù)制策略的迭代。

這里,我給你介紹一下這兩個(gè)版本的并行策略,即按表分發(fā)策略和按行分發(fā)策略,以幫助你理解 MySQL 官方版本并行復(fù)制策略的迭代。

按表分發(fā)策略
按表分發(fā)事務(wù)的基本思路是,如果兩個(gè)事務(wù)更新不同的表,它們就可以并行。因?yàn)閿?shù)據(jù)是存儲(chǔ)在表里的,所以按表分發(fā),可以保證兩個(gè) worker 不會(huì)更新同一行。

當(dāng)然,如果有跨表的事務(wù),還是要把兩張表放在一起考慮的。如圖 3 所示,就是按表分發(fā)的規(guī)則。

image.png

可以看到,每個(gè) worker 線程對(duì)應(yīng)一個(gè) hash 表,用于保存當(dāng)前正在這個(gè) worker 的“執(zhí)行隊(duì)列”里的事務(wù)所涉及的表。hash 表的 key 是“庫名. 表名”,value 是一個(gè)數(shù)字,表示隊(duì)列中有多少個(gè)事務(wù)修改這個(gè)表。

在有事務(wù)分配給 worker 時(shí),事務(wù)里面涉及的表會(huì)被加到對(duì)應(yīng)的 hash 表中。worker 執(zhí)行完成后,這個(gè)表會(huì)被從 hash 表中去掉。

圖 3 中,hash_table_1 表示,現(xiàn)在 worker_1 的“待執(zhí)行事務(wù)隊(duì)列”里,有 4 個(gè)事務(wù)涉及到 db1.t1 表,有 1 個(gè)事務(wù)涉及到 db2.t2 表;hash_table_2 表示,現(xiàn)在 worker_2 中有一個(gè)事務(wù)會(huì)更新到表 t3 的數(shù)據(jù)。

假設(shè)在圖中的情況下,coordinator 從中轉(zhuǎn)日志中讀入一個(gè)新事務(wù) T,這個(gè)事務(wù)修改的行涉及到表 t1 和 t3。

現(xiàn)在我們用事務(wù) T 的分配流程,來看一下分配規(guī)則。

....

小結(jié)

為什么要有多線程復(fù)制呢?這是因?yàn)閱尉€程復(fù)制的能力全面低于多線程復(fù)制,對(duì)于更新壓力較大的主庫,備庫是可能一直追不上主庫的。從現(xiàn)象上看就是,備庫上 seconds_behind_master 的值越來越大。

從這些分析中,你也會(huì)發(fā)現(xiàn)大事務(wù)不僅會(huì)影響到主庫,也是造成備庫復(fù)制延遲的主要原因之一。因此,在平時(shí)的開發(fā)工作中,我建議你盡量減少大事務(wù)操作,把大事務(wù)拆成小事務(wù)。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容