在上一篇文章中,我和你介紹了一主多從的結構以及切換流程。今天咱們就繼續聊聊一主多從架構的應用場景:讀寫分離,以及怎麼處理主備延遲致使的讀寫分離問題。html
咱們在上一篇文章中提到的一主多從的結構,其實就是讀寫分離的基本結構了。這裏,我再把這張圖貼過來,方便你理解。mysql
圖 1 讀寫分離基本結構sql
讀寫分離的主要目標就是分攤主庫的壓力。圖 1 中的結構是客戶端(client)主動作負載均衡,這種模式下通常會把數據庫的鏈接信息放在客戶端的鏈接層。也就是說,由客戶端
來選擇後端數據庫進行查詢。數據庫
還有一種架構是,在 MySQL 和客戶端之間有一箇中間代理層 proxy,客戶端只鏈接proxy, 由 proxy 根據請求類型和上下文決定請求的分發路由。後端
圖 2 帶 proxy 的讀寫分離架構api
接下來,咱們就看一下客戶端直連和帶 proxy 的讀寫分離架構,各有哪些特色。bash
1. 客戶端直連方案,由於少了一層 proxy 轉發,因此查詢性能稍微好一點兒,而且總體架構簡單,排查問題更方便。可是這種方案,因爲要了解後端部署細節,因此在出現主備
切換、庫遷移等操做的時候,客戶端都會感知到,而且須要調整數據庫鏈接信息。你可能會以爲這樣客戶端也太麻煩了,信息大量冗餘,架構很醜。其實也未必,通常採
用這樣的架構,必定會伴隨一個負責管理後端的組件,好比 Zookeeper,儘可能讓業務端只專一於業務邏輯開發。
2. 帶 proxy 的架構,對客戶端比較友好。客戶端不須要關注後端細節,鏈接維護、後端信息維護等工做,都是由 proxy 完成的。但這樣的話,對後端維護團隊的要求會更高。而
且,proxy 也須要有高可用架構。所以,帶 proxy 架構的總體就相對比較複雜。理解了這兩種方案的優劣,具體選擇哪一個方案就取決於數據庫團隊提供的能力了。但目前看,趨勢是往帶 proxy 的架構方向發展的。session
可是,不論使用哪一種架構,你都會碰到咱們今天要討論的問題:因爲主從可能存在延遲,客戶端執行完一個更新事務後立刻發起查詢,若是查詢選擇的是從庫的話,就有可能讀到
剛剛的事務更新以前的狀態。架構
這種「在從庫上會讀到系統的一個過時狀態」的現象,在這篇文章裏,咱們暫且稱之爲「過時讀」。負載均衡
前面咱們說過了幾種可能致使主備延遲的緣由,以及對應的優化策略,可是主從延遲仍是不能 100% 避免的。
不論哪一種結構,客戶端都但願查詢從庫的數據結果,跟查主庫的數據結果是同樣的。接下來,咱們就來討論怎麼處理過時讀問題。
這裏,我先把文章中涉及到的處理過時讀的方案彙總在這裏,以幫助你更好地理解和掌握全文的知識脈絡。這些方案包括:
強制走主庫方案其實就是,將查詢請求作分類。一般狀況下,咱們能夠將查詢請求分爲這麼兩類:
1. 對於必需要拿到最新結果的請求,強制將其發到主庫上。好比,在一個交易平臺上,賣家發佈商品之後,立刻要返回主頁面,看商品是否發佈成功。那麼,這個請求須要拿到
最新的結果,就必須走主庫。
2. 對於能夠讀到舊數據的請求,纔將其發到從庫上。在這個交易平臺上,買家來逛商鋪頁面,就算晚幾秒看到最新發布的商品,也是能夠接受的。那麼,這類請求就能夠走從庫。
你可能會說,這個方案是否是有點畏難和取巧的意思,但其實這個方案是用得最多的。
固然,這個方案最大的問題在於,有時候你會碰到「全部查詢都不能是過時讀」的需求,
好比一些金融類的業務。這樣的話,你就要放棄讀寫分離,全部讀寫壓力都在主庫,等同於放棄了擴展性。
所以接下來,咱們來討論的話題是:能夠支持讀寫分離的場景下,有哪些解決過時讀的方案,並分析各個方案的優缺點。
主庫更新後,讀從庫以前先 sleep 一下。具體的方案就是,相似於執行一條 selectsleep(1) 命令。
這個方案的假設是,大多數狀況下主備延遲在 1 秒以內,作一個 sleep 能夠有很大機率拿到最新的數據。
這個方案給你的第一感受,極可能是不靠譜兒,應該不會有人用吧?而且,你還可能會說,直接在發起查詢時先執行一條 sleep 語句,用戶體驗很不友好啊。
但,這個思路確實能夠在必定程度上解決問題。爲了看起來更靠譜兒,咱們能夠換一種方式。
以賣家發佈商品爲例,商品發佈後,用 Ajax(Asynchronous JavaScript + XML,異步JavaScript 和 XML)直接把客戶端輸入的內容做爲「新的商品」顯示在頁面上,而不是真
正地去數據庫作查詢。
這樣,賣家就能夠經過這個顯示,來確認產品已經發布成功了。等到賣家再刷新頁面,去查看商品的時候,其實已通過了一段時間,也就達到了 sleep 的目的,進而也就解決了過
期讀的問題。
也就是說,這個 sleep 方案確實解決了相似場景下的過時讀問題。但,從嚴格意義上來講,這個方案存在的問題就是不精確。這個不精確包含了兩層意思:
1. 若是這個查詢請求原本 0.5 秒就能夠在從庫上拿到正確結果,也會等 1 秒;
2. 若是延遲超過 1 秒,仍是會出現過時讀。
看到這裏,你是否是有一種「你是否是在逗我」的感受,這個改進方案雖然能夠解決相似Ajax 場景下的過時讀問題,但仍是怎麼看都不靠譜兒。彆着急,接下來我就和你介紹一些
更準確的方案。
要確保備庫無延遲,一般有三種作法。
經過前面的第 25 篇文章,咱們知道 show slave status 結果裏的seconds_behind_master 參數的值,能夠用來衡量主備延遲時間的長短。
因此第一種確保主備無延遲的方法是,每次從庫執行查詢請求前,先判斷seconds_behind_master 是否已經等於 0。若是還不等於 0 ,那就必須等到這個參數變爲 0 才能執行查詢請求。
seconds_behind_master 的單位是秒,若是你以爲精度不夠的話,還能夠採用對比位點和 GTID 的方法來確保主備無延遲,也就是咱們接下來要說的第二和第三種方法。
如圖 3 所示,是一個 show slave status 結果的部分截圖。
圖 3 show slave status 結果
如今,咱們就經過這個結果,來看看具體如何經過對比位點和 GTID 來確保主備無延遲。
第二種方法,對比位點確保主備無延遲:
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 這兩組值徹底相同,就表示接收到的日誌已經同步完成。
Auto_Position=1 ,表示這對主備關係使用了 GTID 協議。 Retrieved_Gtid_Set,是備庫收到的全部日誌的 GTID 集合; Executed_Gtid_Set,是備庫全部已經執行完成的 GTID 集合。
若是這兩個集合相同,也表示備庫接收到的日誌都已經同步完成。
可見,對比位點和對比 GTID 這兩種方法,都要比判斷 seconds_behind_master 是否爲0 更準確。
在執行查詢請求以前,先判斷從庫是否同步完成的方法,相比於 sleep 方案,準確度確實提高了很多,但仍是沒有達到「精確」的程度。爲何這麼說呢?
咱們如今一塊兒來回顧下,一個事務的 binlog 在主備庫之間的狀態:
1. 主庫執行完成,寫入 binlog,並反饋給客戶端;
2. binlog 被從主庫發送給備庫,備庫收到;
3. 在備庫執行 binlog 完成。
咱們上面判斷主備無延遲的邏輯,是「備庫收到的日誌都執行完成了」。可是,從 binlog在主備之間狀態的分析中,不難看出還有一部分日誌,處於客戶端已經收到提交確認,而
備庫還沒收到日誌的狀態。
如圖 4 所示就是這樣的一個狀態。
圖 4 備庫還沒收到 trx3這時,主庫上執行完成了三個事務 trx一、trx2 和 trx3,其中:
1. trx1 和 trx2 已經傳到從庫,而且已經執行完成了;
2. trx3 在主庫執行完成,而且已經回覆給客戶端,可是尚未傳到從庫中。
若是這時候你在從庫 B 上執行查詢請求,按照咱們上面的邏輯,從庫認爲已經沒有同步延遲,但仍是查不到 trx3 的。嚴格地說,就是出現了過時讀。
那麼,這個問題有沒有辦法解決呢?
要解決這個問題,就要引入半同步複製,也就是 semi-sync replication。semi-sync 作了這樣的設計:
1. 事務提交的時候,主庫把 binlog 發給從庫;
2. 從庫收到 binlog 之後,發回給主庫一個 ack,表示收到了;
3. 主庫收到這個 ack 之後,才能給客戶端返回「事務完成」的確認。
也就是說,若是啓用了 semi-sync,就表示全部給客戶端發送過確認的事務,都確保了備庫已經收到了這個日誌。
在第 25 篇文章的評論區,有同窗問到:若是主庫掉電的時候,有些 binlog 還來不及發給從庫,會不會致使系統數據丟失?
答案是,若是使用的是普通的異步複製模式,就可能會丟失,但 semi-sync 就能夠解決這個問題。
這樣,semi-sync 配合前面關於位點的判斷,就可以肯定在從庫上執行的查詢請求,能夠
避免過時讀。
可是,semi-sync+ 位點判斷的方案,只對一主一備的場景是成立的。在一主多從場景中,主庫只要等到一個從庫的 ack,就開始給客戶端返回確認。這時,在從庫上執行查詢
請求,就有兩種狀況:
1. 若是查詢是落在這個響應了 ack 的從庫上,是可以確保讀到最新數據;
2. 但若是是查詢落到其餘從庫上,它們可能尚未收到最新的日誌,就會產生過時讀的問題。
其實,判斷同步位點的方案還有另一個潛在的問題,即:若是在業務更新的高峯期,主庫的位點或者 GTID 集合更新很快,那麼上面的兩個位點等值判斷就會一直不成立,很可
能出現從庫上遲遲沒法響應查詢請求的狀況。
實際上,回到咱們最初的業務邏輯裏,當發起一個查詢請求之後,咱們要獲得準確的結果,其實並不須要等到「主備徹底同步」。
爲何這麼說呢?咱們來看一下這個時序圖。
圖 5 主備持續延遲一個事務
圖 5 所示,就是等待位點方案的一個 bad case。圖中備庫 B 下的虛線框,分別表示relaylog 和 binlog 中的事務。能夠看到,圖 5 中從狀態 1 到狀態 4,一直處於延遲一個
事務的狀態。
備庫 B 一直到狀態 4 都和主庫 A 存在延遲,若是用上面必須等到無延遲才能查詢的方案,select 語句直到狀態 4 都不能被執行。
可是,其實客戶端是在發完 trx1 更新後發起的 select 語句,咱們只須要確保 trx1 已經執行完成就能夠執行 select 語句了。也就是說,若是在狀態 3 執行查詢請求,獲得的就是預
期結果了。
到這裏,咱們小結一下,semi-sync 配合判斷主備無延遲的方案,存在兩個問題:
1. 一主多從的時候,在某些從庫執行查詢請求會存在過時讀的現象;
2. 在持續延遲的狀況下,可能出現過分等待的問題。
接下來,我要和你介紹的等主庫位點方案,就能夠解決這兩個問題。
要理解等主庫位點方案,我須要先和你介紹一條命令:
select master_pos_wait(file, pos[, timeout]);
這條命令的邏輯以下:
1. 它是在從庫執行的;
2. 參數 file 和 pos 指的是主庫上的文件名和位置;
3. timeout 可選,設置爲正整數 N 表示這個函數最多等待 N 秒。
這個命令正常返回的結果是一個正整數 M,表示從命令開始執行,到應用完 file 和 pos表示的 binlog 位置,執行了多少事務。
固然,除了正常返回一個正整數 M 外,這條命令還會返回一些其餘結果,包括:
1. 若是執行期間,備庫同步線程發生異常,則返回 NULL;
2. 若是等待超過 N 秒,就返回 -1;
3. 若是剛開始執行的時候,就發現已經執行過這個位置了,則返回 0。
對於圖 5 中先執行 trx1,再執行一個查詢請求的邏輯,要保證可以查到正確的數據,咱們可使用這個邏輯:
我把上面這個流程畫出來。
圖 6 master_pos_wait 方案
這裏咱們假設,這條 select 查詢最多在從庫上等待 1 秒。那麼,若是 1 秒內master_pos_wait 返回一個大於等於 0 的整數,就確保了從庫上執行的這個查詢結果必定包含了 trx1 的數據。
步驟 5 到主庫執行查詢語句,是這類方案經常使用的退化機制。由於從庫的延遲時間不可控,不能無限等待,因此若是等待超時,就應該放棄,而後到主庫去查。
你可能會說,若是全部的從庫都延遲超過 1 秒了,那查詢壓力不就都跑到主庫上了嗎?確實是這樣。
可是,按照咱們設定不容許過時讀的要求,就只有兩種選擇,一種是超時放棄,一種是轉到主庫查詢。具體怎麼選擇,就須要業務開發同窗作好限流策略了。
若是你的數據庫開啓了 GTID 模式,對應的也有等待 GTID 的方案。MySQL 中一樣提供了一個相似的命令:
select master_pos_wait(file, pos[, timeout]);
這條命令的邏輯是:
1. 等待,直到這個庫執行的事務中包含傳入的 gtid_set,返回 0;
2. 超時返回 1。
在前面等位點的方案中,咱們執行完事務後,還要主動去主庫執行 show master status。而 MySQL 5.7.6 版本開始,容許在執行完更新類事務後,把這個事務的 GTID 返回給客戶
端,這樣等 GTID 的方案就能夠減小一次查詢。
這時,等 GTID 的執行流程就變成了:
1. trx1 事務更新完成後,從返回包直接獲取這個事務的 GTID,記爲 gtid1;
2. 選定一個從庫執行查詢語句;
3. 在從庫上執行 select wait_for_executed_gtid_set(gtid1, 1);
4. 若是返回值是 0,則在這個從庫執行查詢語句;
5. 不然,到主庫執行查詢語句。
跟等主庫位點的方案同樣,等待超時後是否直接到主庫查詢,須要業務開發同窗來作限流考慮。
我把這個流程圖畫出來。
圖 7 wait_for_executed_gtid_set 方案
在上面的第一步中,trx1 事務更新完成後,從返回包直接獲取這個事務的 GTID。問題是,怎麼可以讓 MySQL 在執行事務後,返回包中帶上 GTID 呢?
你只須要將參數 session_track_gtids 設置爲 OWN_GTID,而後經過 API 接口mysql_session_track_get_first 從返回包解析出 GTID 的值便可。
在專欄的第一篇文章中,我介紹 mysql_reset_connection 的時候,評論區有同窗留言問這類接口應該怎麼使用。
這裏我再回答一下。其實,MySQL 並無提供這類接口的 SQL 用法,是提供給程序的API(https://dev.mysql.com/doc/refman/5.7/en/c-api-functions.html)。
好比,爲了讓客戶端在事務提交後,返回的 GITD 可以在客戶端顯示出來,我對 MySQL客戶端代碼作了點修改,以下所示:
圖 8 顯示更新事務的 GTID-- 代碼
這樣,就能夠看到語句執行完成,顯示出 GITD 的值。
圖 9 顯示更新事務的 GTID-- 效果
固然了,這只是一個例子。你要使用這個方案的時候,仍是應該在你的客戶端代碼中調用mysql_session_track_get_first 這個函數。
在今天這篇文章中,我跟你介紹了一主多從作讀寫分離時,可能碰到過時讀的緣由,以及幾種應對的方案。
這幾種方案中,有的方案看上去是作了妥協,有的方案看上去不那麼靠譜兒,但都是有實際應用場景的,你須要根據業務需求選擇。
即便是最後等待位點和等待 GTID 這兩個方案,雖然看上去比較靠譜兒,但仍然存在須要權衡的狀況。若是全部的從庫都延遲,那麼請求就會所有落到主庫上,這時候會不會因爲
壓力忽然增大,把主庫打掛了呢?
其實,在實際應用中,這幾個方案是能夠混合使用的。
好比,先在客戶端對請求作分類,區分哪些請求能夠接受過時讀,而哪些請求徹底不能接受過時讀;而後,對於不能接受過時讀的語句,再使用等 GTID 或等位點的方案。
但話說回來,過時讀在本質上是由一寫多讀致使的。在實際應用中,可能會有別的不須要等待就能夠水平擴展的數據庫方案,但這每每是用犧牲寫性能換來的,也就是須要在讀性
能和寫性能中取權衡。
最後 ,我給你留下一個問題吧。
假設你的系統採用了咱們文中介紹的最後一個方案,也就是等 GTID 的方案,如今你要對主庫的一張大表作 DDL,可能會出現什麼狀況呢?爲了不這種狀況,你會怎麼作呢
你能夠把你的分析和方案設計寫在評論區,我會在下一篇文章跟你討論這個問題。感謝你的收聽,也歡迎你把這篇文章分享給更多的朋友一塊兒閱讀。
上期給你留的問題是,在 GTID 模式下,若是一個新的從庫接上主庫,可是須要的 binlog已經沒了,要怎麼作?
@某、人同窗給了很詳細的分析,我把他的回答略作修改貼過來。
1. 若是業務容許主從不一致的狀況,那麼能夠在主庫上先執行 show global variableslike ‘gtid_purged’,獲得主庫已經刪除的 GTID 集合,假設是 gtid_purged1;而後先在從庫上執行 reset master,再執行 set global gtid_purged=‘gtid_purged1’;最後執行 start slave,就會從主庫現存的 binlog 開始同步。binlog 缺失的那一部分,數據在從庫上就可能會有丟失,形成主從不一致。
2. 若是須要主從數據一致的話,最好仍是經過從新搭建從庫來作。
3. 若是有其餘的從庫保留有全量的 binlog 的話,能夠把新的從庫先接到這個保留了全量
binlog 的從庫,追上日誌之後,若是有須要,再接回主庫。
4. 若是 binlog 有備份的狀況,能夠先在從庫上應用缺失的 binlog,而後再執行 startslave。
@悟空 同窗級聯實驗,驗證了 seconds_behind_master 的計算邏輯。
@_CountingStars 問了一個好問題:MySQL 是怎麼快速定位 binlog 裏面的某一個 GTID 位置的?答案是,在 binlog 文件頭部的 Previous_gtids 可
以解決這個問題。
@王朋飛 同窗問了一個好問題,sql_slave_skip_counter 跳過的是一個event,因爲 MySQL 總不能執行一半的事務,因此既然跳過了一個event,就會跳到這個事務的末尾,所以 set globalsql_slave_skip_counter=1;start slave 是能夠跳過整個事務的。