一、主從架構(gòu)
為什么我們要進行讀寫分離?個人覺得還是業(yè)務(wù)發(fā)展到一定的規(guī)模,驅(qū)動技術(shù)架構(gòu)的改革,讀寫分離可以減輕單臺服務(wù)器的壓力,將讀請求和寫請求分流到不同的服務(wù)器,分攤單臺服務(wù)的負載,提高可用性,提高讀請求的性能。
上面這個圖是一個基礎(chǔ)的Mysql的主從架構(gòu),1主1備3從。這種架構(gòu)是客戶端主動做的負載均衡,數(shù)據(jù)庫的連接信息一般是放到客戶端的連接層,也就是說由客戶端來選擇數(shù)據(jù)庫進行讀寫
上圖是一個帶proxy的主從架構(gòu),客戶端只和proxy進行連接,由proxy根據(jù)請求類型和上下文決定請求的分發(fā)路由。
兩種架構(gòu)方案各有什么特點:
1.客戶端直連架構(gòu),由于少了一層proxy轉(zhuǎn)發(fā),所以查詢性能會比較好點兒,架構(gòu)簡單,遇到問題好排查。但是這種架構(gòu),由于要了解后端部署細節(jié),出現(xiàn)主備切換,庫遷移的時候客戶端都會感知到,并且需要調(diào)整庫連接信息
2.帶proxy的架構(gòu),對客戶端比較友好,客戶端不需要了解后端部署細節(jié),連接維護,后端信息維護都由proxy來完成。這樣的架構(gòu)對后端運維團隊要求比較高,而且proxy本身也要求高可用,所以整體架構(gòu)相對來說比較復(fù)雜
但是不論使用哪種架構(gòu),由于主從之間存在延遲,當一個事務(wù)更新完成后馬上發(fā)起讀請求,如果選擇讀從庫的話,很有可能讀到這個事務(wù)更新之前的狀態(tài),我們把這種讀請求叫做過期讀。出現(xiàn)主從延遲的情況有多種,有興趣的同學可以自己了解一下,雖然出現(xiàn)主從延遲我們同樣也有應(yīng)對策略,但是不能100%避免,這些不是我們本次討論的范圍,我們主要討論一下如果出現(xiàn)主從延遲,剛好我們的讀走的都是從庫,我們應(yīng)該怎么應(yīng)對?
首先我把應(yīng)對的策略總結(jié)一下:
- 強制走主庫
- sleep方案
- 判斷主從無延遲
- 等主庫位點
- 等GTID方案
接下來基于上述的幾種方案,我們逐個討論一下怎么實現(xiàn)和有什么問題。
二、主從同步
在開始介紹主從延遲解決方案前先簡單的回顧一下主從的同步
上圖表示了一個update語句從節(jié)點A同步到節(jié)點B的完整過程
備庫B和主庫A維護了一個長連接,主庫A內(nèi)部有一個線程,專門用來服務(wù)備庫B的連接。一個事務(wù)日志同步的完整流程是:
1.在備庫 B 上通過 change master 命令,設(shè)置主庫 A 的 IP、端口、用戶名、密碼,以及要從哪個位置開始請求 binlog,這個位置包含文件名和日志偏移量。
2.在備庫 B 上執(zhí)行 start slave 命令,這時候備庫會啟動兩個線程,就是圖中的 io_thread 和 sql_thread。
3.其中 io_thread 負責與主庫建立連接。
4.主庫 A 校驗完用戶名、密碼后,開始按照備庫 B 傳過來的位置,從本地讀取 binlog,發(fā)給 B。備庫 B 拿到 binlog 后,寫到本地文件,稱為中轉(zhuǎn)日志(relay log)。
5.sql_thread 讀取中轉(zhuǎn)日志,解析出日志里的命令,并執(zhí)行。
上圖中紅色箭頭,如果用顏色深淺表示并發(fā)度的話,顏色越深并發(fā)度越高,所以主從延遲時間的長短取決于備庫同步線程執(zhí)行中轉(zhuǎn)日志(圖中的relay log)的快慢??偨Y(jié)一下可能出現(xiàn)主從延遲的原因:
1.主庫并發(fā)高,TPS大,備庫壓力大執(zhí)行日志慢
2.大事務(wù),一個事務(wù)在主庫執(zhí)行5s,那么同樣的到備庫也得執(zhí)行5s,比如一次性刪除大量的數(shù)據(jù),大表DDL等都是大事務(wù)
3.從庫的并行復(fù)制能力,Msyql5.6之前的版本是不支持并行復(fù)制的也就是上圖的模型。并行復(fù)制也比較復(fù)雜,就不在這兒贅述了,大家可以自行復(fù)習了解一下。
三、主從延遲解決方案
1.強制走主庫
這種方案就是要對我們的請求進行分類,通??梢詫⒄埱蠓殖蓛深悾?/p>
1.對于必須要拿到最新結(jié)果的請求,可以強制走主庫
2.對于可以讀到舊數(shù)據(jù)的請求,可以分配到從庫
這種方案是最簡單的方案,但是這種方案有一個缺點就是,對于所有的請求都不能是過期讀的請求,那么所有的壓力就又來到了主庫,就得放棄讀寫分離,放棄擴展性
2.sleep方案
sleep方案就是每次查詢從庫之前都先執(zhí)行一下:select sleep(1),類似這樣的命令,這種方式有兩個問題:
1.如果主從延遲大于1s,那么依然讀到的是過期狀態(tài)
2.如果這個請求可能0.5s就能在從庫拿到結(jié)果,仍然要等1s
這種方案看起來十分的不靠譜,不專業(yè),但是這種方案確實也有使用的場景。
之前在做項目的時候,有這樣么一種場景,就是我們先寫主庫,寫完后,發(fā)送一個MQ消息,然后消費方接到消息后,調(diào)用我們的查詢接口查數(shù)據(jù),當然我們也是讀寫分離的模式,就出現(xiàn)了查不到數(shù)據(jù)的情況,這個時候建議消費方對消息進行一個延遲消費,比如延遲30ms,然后問題就解決了,這種方式類似sleep方案,只不過把sleep放到了調(diào)用方
3.判斷主從無延遲方案
- 命令判斷
show slave status,這個命令是在從庫上執(zhí)行的,執(zhí)行的結(jié)果里面有個seconds_behind_master字段,這個字段表示主從延遲多少s,注意單位是秒。所以這種方案就是通過判斷當前這個值是否為0,如果為0則直接查詢獲取結(jié)果,如果不為0,則一直等待,直到主從延遲變?yōu)?
因為這個值是秒級的,但是我們的一些場景下是毫秒級的請求,所以通過這個方式判斷,不是特別精確
- 對比位點判斷主從無延遲
上圖是執(zhí)行一次show slave status 部分結(jié)果
- Master_Log_File和Read_Master_Log_Pos表示讀到的主庫的最新的位點
- Relay_Master_Log_File和Exec_Master_Log_Pos表示備庫執(zhí)行的最新的位點
如果Master_Log_File和Relay_Master_Log_File,Read_Master_Log_Pos和Exec_Master_Log_Pos這兩組值完全一致,表示主從之間是沒有延遲的
3)對比GTID判斷主從無延遲
- Auto_Position:1表示這對主從之間啟用了GTID協(xié)議
- Retrieved_Gtid_Set:表示從庫接收到的所有的GTID的集合
- Executed_Gtid_Set:表示從庫執(zhí)行完成的所有的GTID集合
通過比較Retrieved_Gtid_Set和Executed_Gtid_Set集合是否一致,來確定主從是否存在延遲。
可見對比位點和對比GTID集合,比sleep要準確一點兒,在查詢之前都可以先判斷一下是否接收到的日志都執(zhí)行完成了,雖然準確度提升了,但是還達不到精確,為啥這么說呢?
先回顧一下binlog在一個事物下的狀態(tài)
1.主庫執(zhí)行完成,寫入binlog,反饋給客戶端
2.binlog被從主庫發(fā)送到備庫,備庫接收到日志
3.備庫執(zhí)行binlog
我們上面判斷主備無延遲方案,都是判斷備庫收到的日志都執(zhí)行過了,但是從binlog在主備之間的狀態(tài)分析,可以看出,還有一部分日志處于客戶端已經(jīng)收到提交確認,但是備庫還沒有收到日志的狀態(tài)
這個時候主庫執(zhí)行了3個事物,trx1,trx2,trx3,其中
- trx1,trx2已經(jīng)傳到從庫,并且從庫已經(jīng)執(zhí)行完成
- trx3主庫已經(jīng)執(zhí)行完成,并且已經(jīng)給客戶端回復(fù),但是還沒有傳給從庫
這個時候如果在從庫B執(zhí)行查詢,按照上面我們判斷位點的方式,這個時候主從是沒有延遲的,但是還查不到trx3,嚴格說就是出現(xiàn)了"過期讀"。那么這個問題有什么方法可以解決么?
要解決這個問題,可以引入半同步復(fù)制,也就是semi-sync repliacation(參考:https://dev.mysql.com/doc/refman/8.0/en/replication-semisync.html)。
可以通過
show variables like '%rpl_semi_sync_master_enabled%'
show variables like '%rpl_semi_sync_slave_enabled%'
這兩個命令來查看主從是否都開啟了半同步復(fù)制。
semi-sync做了這樣的設(shè)計:
1.事物提交的時候,主庫把binlog發(fā)給從庫
2.從庫接收到主庫發(fā)過來的binlog,給主庫一個ack確認,表示收到了
3.主庫收到這個ack確認后,才給客戶端返回一個事物完成的確認
也就是啟用了semi-sync,表示所有返回給客戶端已經(jīng)確認完成的事物,從庫都收到了binlog日志,這樣通過semi-sync配合判斷位點的方式,就可以確定在從庫上的查詢,避免了過期讀的出現(xiàn)。
但是semi-sync配合判斷位點的方式,只適用一主一備的情況,在一主多從的情況下,主庫只要收到一個從庫的ack確認,就給客戶端返回事物執(zhí)行完成的確認,這個時候在從庫上執(zhí)行查詢就有兩種情況
- 如果查詢剛好是在給主庫響應(yīng)ack確認的從庫上,那么可以查詢到正確的數(shù)據(jù)
- 但是如果請求落到其他的從庫上,他們可能還沒收到日志,所以依然可能存在過期讀
其實通過判斷同步位點或者GTID集合的方案,還存在一個潛在的問題,就是業(yè)務(wù)高峰期,主庫的位點或者GITD集合更新的非常快,那么兩個位點的判斷一直不相等,很可能出現(xiàn)從庫一直無法響應(yīng)查詢請求的情況。
上面的兩種方案在靠譜程度和精確性上都差了一點兒,接下來介紹兩種相對靠譜和精確一點兒的方案
4.等主庫位點
要理解等主庫位點,先介紹一條命令
select master_pos_wait(file, pos[, timeout]);
這條命令執(zhí)行的邏輯是:
1.首先是在從庫執(zhí)行的
2.參數(shù)file和pos是主庫的binlog文件名和執(zhí)行到的位置
3.timeout參數(shù)是非必須,設(shè)置為正整數(shù)N,表示這個函數(shù)最多等到N秒
這個命令執(zhí)行結(jié)果M可能存在的情況:
- M>0表示從命令執(zhí)行開始,到應(yīng)用完file和pos表示的binlog位置,一共執(zhí)行了M個事務(wù)
- 如果執(zhí)行期間,備庫的同步線程發(fā)生異常,則返回null
- 如果等待超過N秒,返回-1
- 如果剛開始執(zhí)行的時候,發(fā)現(xiàn)已經(jīng)執(zhí)行了過了這個pos,則返回0
當一個事務(wù)執(zhí)行完成后,我們要馬上發(fā)起一個查詢請求,可以通過下面的步驟實現(xiàn):
1.當一個事務(wù)執(zhí)行完成后,馬上執(zhí)行show master status,獲取主庫的File和Position
2.選擇一個從庫執(zhí)行查詢
3.在從庫上執(zhí)行 select master_pos_wait(File,Poistion,1)
4.如果返回的值>=0,則在這個從庫上執(zhí)行
5.否則回主庫查詢
這里我們假設(shè),這條查詢請求在從庫上最多等待1s,那么如果1s內(nèi)master_pos_wait返回一個大于等于0的數(shù),那么就能保證在這個從庫上能查到剛執(zhí)行完的事務(wù)的最新的數(shù)據(jù)。
上述的步驟5是這類方案的兜底方案,因為從庫的延遲時間不可控,不能無限等待,所以如果超時,就應(yīng)該放棄,到主庫查詢。
可能有同學會覺的,如果所有的延遲都超過1s,那么所有的壓力都到了主庫,確實是這樣的,但是按照我們設(shè)定的不允許出現(xiàn)過期讀,那么就只有兩種選擇,要么超時放棄,要么轉(zhuǎn)到主庫,具體選擇哪種,需要我們根據(jù)業(yè)務(wù)進行具體的分析。
5.等GTID方案
如果數(shù)據(jù)庫開啟的GTID模式,那么相應(yīng)的也有等GTID的方案
select wait_for_executed_gtid_set(gtid_set, 1);
這條命令的邏輯是:
1.等待,直到這個庫執(zhí)行的事務(wù)中包含傳入的giid_set集合,返回0
2.超時返回1
在前面等待主庫位點的方案中,執(zhí)行完事務(wù)后,需要到主庫執(zhí)行show master status。從mysql5.7.6開始,允許事務(wù)執(zhí)行完成后,把這個事務(wù)執(zhí)行的GTID返回給客戶端,這樣等待GTIID的方案就減少了一次查詢。
這時等GTID方案的流程就變成這樣:
1.事務(wù)執(zhí)行完成后,從返回包解析獲取這個事務(wù)的GTID,記為gtid1
2.選定一個從庫執(zhí)行查詢
3.在從庫上執(zhí)行select wait_for_executed_gtid_set(gtid1,1)
4.如果返回0,則在這個從庫上執(zhí)行查詢
5.否則回到主庫查詢
和等待主庫位點方案一樣,最后的兜底方案都是轉(zhuǎn)到主庫查詢了,需要綜合業(yè)務(wù)考慮確定方案
上面的事物執(zhí)行完成后,從返回的包中解析GTID,mysql其實沒有提供對應(yīng)的命令,可以參考Mysql提供的api(https://dev.mysql.com/doc/c-api/8.0/en/mysql-session-track-get-first.html),在我們的客戶端可以調(diào)用這個函數(shù)獲取GTID
四、總結(jié)
以上簡單介紹了讀寫分離架構(gòu),和出現(xiàn)主從延遲后,如果我們用的讀寫分離的架構(gòu),那么我們應(yīng)該怎么處理這種情況,相信在日常我們的主從還是或多或少的存在延遲。上面介紹的幾種方案,有些方案看上去十分不靠譜,有些方案做了一些妥協(xié),但是都有實際的應(yīng)用場景,需要我們根據(jù)自身的業(yè)務(wù)情況,合理選擇對應(yīng)的方案。
但話說回來,導(dǎo)致過期讀的本質(zhì)還是一寫多讀導(dǎo)致的,在實際的應(yīng)用中,可能有別的不用等待就可以水平擴展的數(shù)據(jù)庫方案,但這往往都是通過犧牲寫性能獲得的,也就是需要我們在讀性能和寫性能之間做個權(quán)衡。
文中有不太嚴謹或者錯誤的地方還望大家多多指正。
作者:京東零售 尚有智文章來源:http://www.zghlxwxcb.cn/news/detail-694983.html
來源:京東云開發(fā)者社區(qū) 轉(zhuǎn)載請注明來源文章來源地址http://www.zghlxwxcb.cn/news/detail-694983.html
到了這里,關(guān)于淺談Mysql讀寫分離的坑以及應(yīng)對的方案 | 京東云技術(shù)團隊的文章就介紹完了。如果您還想了解更多內(nèi)容,請在右上角搜索TOY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!