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