天天看點

《MySQL實戰45講》——學習筆記28 “讀寫分離/主從延遲的解決方案/GTID“

讀寫分離架構下,發生主從延遲時,可能出現主庫已落表而從庫因為主從延遲還查不到最新資料的問題;這種"在從庫上讀到過期資料"的現象,在本文裡暫且稱之為"過期讀";

本篇主要介紹從業務角度和MySQL架構角度處理主從延遲問題的一些方案,包括:讀寫分離架構、強制路由主庫方案、延遲請求從庫方案、設計庫表時采用分庫分表方案、判斷是否存在主從延遲方案、GTID的概念,以及判斷指定的事務是否已經在從庫完成執行的方案(等主庫位點/GTID方案);

讀寫分離的基本結構

大多數的網際網路應用場景都是讀多寫少,是以業務在發展過程中很可能先會遇到讀性能的問題;而在資料庫層解決讀性能問題,常用的架構就是——一主多從/讀寫分離;讀寫分離的主要目标就是通過寫主庫讀從庫的方式來讓從庫分攤主庫的壓力;

讀寫分離通常有2種實作方案:在用戶端主動做負載均衡和使用中間代理層proxy;

  1. 用戶端主動做負載均衡
《MySQL實戰45講》——學習筆記28 “讀寫分離/主從延遲的解決方案/GTID“

圖中的結構是用戶端(client)主動做負載均衡,這種模式下一般會把資料庫的連接配接資訊放在用戶端的DAO層;也就是說,由用戶端來選擇将目前請求路由到某個資料庫;

用戶端直連方案,因為少了一層proxy轉發,沒有額外的邏輯處理和網絡傳輸損耗,是以查詢性能稍微好一點兒(現公司proxy損失20%性能),并且整體架構簡單,排查問題更友善;

但是如果采用這種方案,需要用戶端熟悉MySQL的部署細節,在出現主備切換、庫表遷移等操作的時候,用戶端都會感覺到,至少用戶端需要維護資料庫連接配接資訊;

為了讓服務端專注于業務邏輯開發,往往會使用ORM架構負責維護資料庫連接配接和請求路由,使用類似nacos的配置中心來分發資料庫連接配接資訊;

總結下來,優點就是:性能無損、靈活,缺點就是對用戶端(業務端)的要求更高;一般使用此方案的系統,是因為公司DBA團隊未開發出功能強大且穩定的proxy層;

  1. 使用中間代理層proxy
《MySQL實戰45講》——學習筆記28 “讀寫分離/主從延遲的解決方案/GTID“

另一種架構是,在MySQL和用戶端之間有一個中間代理層proxy,用戶端隻連接配接proxy,由proxy根據請求類型和上下文決定請求的分發路由;

帶proxy的架構,對用戶端比較友好,用戶端不需要關注MySQL的部署細節,連接配接維護、路由轉發等工作,都是由proxy完成的;但是采用這種方案,對DB維護團隊的要求會更高,proxy不僅需要有豐富的功能,還需要有高可用架構來保證穩定性;是以,帶proxy架構的整體就相對比較複雜;

目前看,趨勢是往帶 proxy 的架構方向發展的;

不論哪種結構,用戶端都希望查詢從庫的資料結果,跟查主庫的資料結果是一樣的,但是主從延遲還是不能 100% 避免的(主從機器性能、主庫長事務、從庫并行複制能力等);下面分别從業務角度和MySQL架構角度介紹下處理"過期讀"的思路;

強制走主庫方案

強制走主庫方案其實就是,将查詢請求做分類;通常情況下,我們可以将查詢請求分為這麼兩類:

  • 對于必須要拿到最新結果的請求,強制将其發到主庫上;比如,在一個交易平台上,使用者購買商品成功後立即檢視已支付訂單資訊,這個請求需要拿到最新的訂單記錄,就必須走主庫;
  • 對于可以讀到舊資料的請求,才将其發到從庫上;在這個交易平台上,使用者來逛商鋪頁面,就算晚幾秒看到最新釋出的商品,也是可以接受的;那麼,這類請求就可以走從庫;

實際上,這個方案是用得最多的;

分庫分表方案

但是,有時候會碰到"所有查詢都不能是過期讀"的需求,比如一些訂單、金融類的業務;這樣的話,你就要放棄讀寫分離,所有讀寫壓力都在主庫,等同于放棄了擴充性;

針對這種場景,可以采用分庫分表的方案;将資料量多、讀寫性能壓力大的資料表水準拆分到多個資料庫中,這些庫被稱為分庫,分庫中的表被稱為分表;

拆分後,每個分庫負責一份資料的讀寫操作,進而有效的分散了單庫單表的通路壓力;在系統擴容時,隻需要水準增加分庫的數量,并且遷移相關資料,就可以提高資料庫的總體容量;

需要注意,如果是業務設計之初,可以在表設計時就考慮好使用此方案,若是業務不斷擴充過程中發現性能瓶頸,則還需要做資料遷移(關于資料遷移可參考我的文章《資料遷移方案【建議收藏】》);

延遲請求從庫方案

思路就是,主庫更新後,讀從庫之前先delay一下;類似于請查詢從庫之前先執行一條sleep指令;

這個方案如果在服務端實作,看起來很不靠譜;确實,使用者體驗很不友好,是以這種方案一般在使用者端實作(前端/APP用戶端),通過類似絲滑的界面切換動畫讓使用者感覺不到這個"故意延遲";

例如,以賣家釋出商品為例,商品釋出後,用前端使用Ajax(AsynchronousJavaScript+XML,異步JavaScript和XML)直接把用戶端輸入的内容作為"新的商品"顯示在頁面上,而不是真正地立即去資料庫做查詢;這樣,對于賣家來說,從界面上看到産品已經釋出成功了;等到賣家下次重新整理頁面,其實已經過了一段時間,也就達到了sleep的目的,進而也就解決了過期讀的問題;

類似的做法在使用者發彈幕、發評論的場景都在使用,先讓使用者看到自己的"本地操作結果"而非立即查詢從庫結果,來避免"過期讀"問題;

不過,當主從延遲很大時,超過這個delay,還是會出現"過期讀";

上面幾種方案是從業務端的視角出發,方案實作依賴業務前端/業務服務端,也是實作簡單、很常用的方案;接下來從MySQL架構的角度,介紹一些"更準确"的方案;在此之前,先介紹下GTID的概念;

GTID的概念

GTID的全稱是Global Transaction Identifier,也就是全局事務ID,是一個事務在送出的時候生成的,是這個事務的唯一辨別;

注意區分與MVCC版本号的差別,MVCC版本号是事務開啟時生成的;它跟MySQL的事務id也是不一樣的,事務id是在事務執行過程中配置設定的,盡管事務id遞增,但由于事務可以會滾,是以事務id不一定連續,而GTID是事務送出時才生成,是以是連續的;

GTID模式的啟動也很簡單,我們隻需要在啟動一個MySQL執行個體的時候,加上參數gtid_mode=on和enforce_gtid_consistency=on就可以了;

在GTID模式下,每個送出的事務都會跟一個GTID一一對應,是以GTID是唯一的;每個 MySQL 執行個體都維護了一個 GTID 集合,用來對應"這個執行個體執行過的所有事務";這個GTID有兩種生成方式:

  • 自動配置設定,設定gtid_next=automatic;這種模式下GTID的數值是連續的;錄binlog的時候,先自動生成目前的GTID,再把這個GTID加入本執行個體的GTID集合;
  • 手動指定,如setgtid_next='current_gtid';current_gtid已經存在于執行個體的GTID集合中,那麼接下來執行的這個事務會直接被忽略;如果current_gtid沒有存在于執行個體的GTID集合中,就将這個current_gtid配置設定給接下來要執行的事務;

利用上面這個機制,可以保證不會出現主鍵沖突,并且可以判斷在主庫已經送出的某個事務在從庫是否已經執行過了,而後面将會介紹基于GTID機制的處理"過期讀"的方案;

判斷主備無延遲方案

這種方案的核心思想就是,如果此時不存在主從延遲,那麼從庫資料與主庫一緻,就可以放心的讀從庫;關鍵就是判斷是否存在主從延遲的方法,有下面幾種:

  • 根據sbm(seconds_behind_master)來判斷;

每次從庫執行查詢請求前,先判斷seconds_behind_master是否已經等于0;如果還不等于0,那就等到這個參數變為0才能執行查詢請求;但是seconds_behind_master的機關是秒,精度太不夠;

  • 對比位點確定主備無延遲;
《MySQL實戰45講》——學習筆記28 “讀寫分離/主從延遲的解決方案/GTID“

主備的位點資訊通過showslavestatus指令檢視,上面是部分截圖;

Master_Log_File和Read_Master_Log_Pos,表示的是讀到的主庫的最新位點;Relay_Master_Log_File和Exec_Master_Log_Pos,表示的是備庫執行的最新位點;

如果像圖中一樣,這兩組值完全相同,就表示接收到的日志已經同步完成;

  • 對比GTID集合確定主備無延遲:

Auto_Position=1,表示這對主備關系使用了GTID協定;Retrieved_Gtid_Set,是備庫收到的所有日志的GTID集合;Executed_Gtid_Set,是備庫所有已經執行完成的GTID集合;

如果主備關系使用了GTID協定,并且這兩個集合相同,也表示備庫接收到的日志都已經同步完成;

上面這個"等到沒有主從延遲"的方案有個很明顯的問題:目前這條查詢為了在從庫讀到最新的資料,其實并不需要等到主從沒有延遲,也就是并不需要主庫上最新的事務已經在從庫執行,而是隻需要跟這條資料相關的最新事務在從庫執行就可以了;并且對于主庫持續有事務執行的業務,幾乎等不到主從完全一緻的時機;

判斷指定的事務是否已經在從庫完成執行的方案

下面介紹這種更合理的"判斷指定的事務trx1是否已經在從庫完成執行"的方案;

方案1:等主庫位點方案

要了解等主庫位點方案,要依賴一條指令:

select master_pos_wait(file, pos[, timeout]);
           

這條指令在從庫執行,邏輯:參數file和pos指的是主庫上的檔案名和位置;timeout可選,設定為正整數N表示這個函數最多等待N秒;

  • 這個指令正常傳回的結果是一個正整數M,表示從指令開始執行,到應用完file和pos表示的binlog位置,這期間執行了多少事務;
  • 如果逾時傳回-1,如果異常傳回NULL;
  • 如果執行指令時,從庫已經執行過這個位置了,則傳回0;

如果此時能拿到主庫事務的File和Position,就可以在從庫執行這個指令來判斷目前事務是否在從庫執行完成(傳回值>=1);方案示例:

  1. 主庫trx1事務完成後,馬上執行show master status得到目前主庫執行到的File和Position;
  1. 標明一個從庫執行查詢語句;在從庫上執行select master_pos_wait(File,Position,1);
  1. 如果傳回值是>=0的正整數,則在這個從庫執行查詢語句;否則,到主庫執行查詢語句;

如果此時還逾時,要麼逾時放棄,要麼切到主庫查詢,具體業務具體分析;

方案2:GTID 方案

與等主庫位點方案類似,如果開啟了GTID模式,則可以嘗試下面這個指令:

select wait_for_executed_gtid_set(gtid_set, 1);
           

這條指令邏輯:

  • 等待,直到這個庫執行的事務中包含傳入的gtid_set,傳回0;
  • 若逾時,傳回1;

MySQL5.7.6版本開始,允許在執行完更新類事務後,把這個事務的GTID傳回給用戶端,這樣相比等位點方案,就少了一步主庫查GTID的步驟;在從庫執行這個指令來判斷目前事務是否在從庫執行完成(傳回值=0);方案示例:

  • 開啟GTID模式;
  • trx1事務更新完成後,從執行指令的傳回結果中直接擷取這個事務的GTID,記為gtid1;
  • 標明一個從庫執行查詢語句;在從庫上執行select wait_for_executed_gtid_set(gtid1,1);
  • 如果傳回值是0,則在這個從庫執行查詢語句;否則,到主庫執行查詢語句;

跟等主庫位點的方案一樣,等待逾時後是否直接到主庫查詢,需要業務評估;

下篇文章:待定

本章參考:28 | 讀寫分離有哪些坑?

繼續閱讀