Redis的主從數據一致性


我們學習了 AOF 和 RDB,如果 Redis 發生了宕機,它們可以分別通過回放日志和重新讀入 RDB 文件的方式恢復數據,從而保證盡量少丟失數據,提升可靠性。不過,即使用了這兩種方法,也依然存在服務不可用的問題。比如說,我們在實際使用時只運行了一個 Redis 實例,那么,如果這個實例宕機了,它在恢復期間,是無法服務新來的數據存取請求的。

我們知道Redis 具有高可靠性,又是什么意思呢?其實,這里有兩層含義:一是數據盡量少丟失,二是服務盡量少中斷。AOF 和 RDB 保證了前者,而對於后者,Redis 的做法就是增加副本冗余量,將一份數據同時保存在多個實例上。即使有一個實例出現了故障,需要過一段時間才能恢復,其他實例也可以對外提供服務,不會影響業務使用。

實際上,Redis 提供了主從庫模式,以保證數據副本的一致,主從庫之間采用的是讀寫分離的方式。

  讀操作:主庫、從庫都可以接收;

  寫操作:首先到主庫執行,然后,主庫將寫操作同步給從庫。

 

 

 為什么要采用讀寫分離的方式呢?

如果在上圖中,不管是主庫還是從庫,都能接收客戶端的寫操作,那么,一個直接的問題就是:如果客戶端對同一個數據(例如 k1)前后修改了三次,每一次的修改請求都發送到不同的實例上,在不同的實例上執行,那么,這個數據在這三個實例上的副本就不一致了(分別是 v1、v2 和 v3)。在讀取這個數據的時候,就可能讀取到舊的值。

如果我們非要保持這個數據在三個實例上一致,就要涉及到加鎖、實例間協商是否完成修改等一系列操作,但這會帶來巨額的開銷,當然是不太能接受的。而主從庫模式一旦采用了讀寫分離,所有數據的修改只會在主庫上進行,不用協調三個實例。主庫有了最新的數據后,會同步給從庫,這樣,主從庫的數據就是一致的。

那么,主從庫同步是如何完成的呢?主庫數據是一次性傳給從庫,還是分批同步?要是主從庫間的網絡斷連了,數據還能保持一致嗎?

我們先來看看主從庫間的第一次同步是如何進行的,這也是 Redis 實例建立主從庫模式后的規定動作。

 

主從庫間如何進行第一次同步?

當我們啟動多個 Redis 實例的時候,它們相互之間就可以通過 replicaof(Redis 5.0 之前使用 slaveof)命令形成主庫和從庫的關系,之后會按照三個階段完成數據的第一次同步。

例如,現在有實例 1(ip:172.16.19.3)和實例 2(ip:172.16.19.5),我們在實例 2 上執行以下這個命令后,實例 2 就變成了實例 1 的從庫,並從實例 1 上復制數據:

replicaof 172.16.19.3 6379

第一階段是主從庫間建立連接、協商同步的過程,主要是為全量復制做准備。在這一步,從庫和主庫建立起連接,並告訴主庫即將進行同步,主庫確認回復后,主從庫間就可以開始同步了。具體來說,從庫給主庫發送 psync 命令,表示要進行數據同步,主庫根據這個命令的參數來啟動復制。psync 命令包含了主庫的 runID 和復制進度 offset 兩個參數。

  runID,是每個 Redis 實例啟動時都會自動生成的一個隨機 ID,用來唯一標記這個實例。當從庫和主庫第一次復制時,因為不知道主庫的 runID,所以將 runID 設為“?”。

  offset,此時設為 -1,表示第一次復制。

主庫收到 psync 命令后,會用 FULLRESYNC 響應命令帶上兩個參數:主庫 runID 和主庫目前的復制進度 offset,返回給從庫。從庫收到響應后,會記錄下這兩個參數。這里有個地方需要注意,FULLRESYNC 響應表示第一次復制采用的全量復制,也就是說,主庫會把當前所有的數據都復制給從庫。

 

在第二階段,主庫將所有數據同步給從庫。從庫收到數據后,在本地完成數據加載。這個過程依賴於內存快照生成的 RDB 文件。具體來說,主庫執行 bgsave 命令,生成 RDB 文件,接着將文件發給從庫。從庫接收到 RDB 文件后,會先清空當前數據庫,然后加載 RDB 文件。這是因為從庫在通過 replicaof 命令開始和主庫同步前,可能保存了其他數據。為了避免之前數據的影響,從庫需要先把當前數據庫清空。

在主庫將數據同步給從庫的過程中,主庫不會被阻塞,仍然可以正常接收請求。否則,Redis 的服務就被中斷了。但是,這些請求中的寫操作並沒有記錄到剛剛生成的 RDB 文件中。為了保證主從庫的數據一致性,主庫會在內存中用專門的 replication buffer,記錄 RDB 文件生成后收到的所有寫操作。

 

第三個階段,主庫會把第二階段執行過程中新收到的寫命令,再發送給從庫。具體的操作是,當主庫完成 RDB 文件發送后,就會把此時 replication buffer 中的修改操作發給從庫,從庫再重新執行這些操作。這樣一來,主從庫就實現同步了。

 

主從級聯模式分擔全量復制時的主庫壓力

通過分析主從庫間第一次數據同步的過程,你可以看到,一次全量復制中,對於主庫來說,需要完成兩個耗時的操作:生成 RDB 文件和傳輸 RDB 文件。

如果從庫數量很多,而且都要和主庫進行全量復制的話,就會導致主庫忙於 fork 子進程生成 RDB 文件,進行數據全量同步。fork 這個操作會阻塞主線程處理正常請求,從而導致主庫響應應用程序的請求速度變慢。此外,傳輸 RDB 文件也會占用主庫的網絡帶寬,同樣會給主庫的資源使用帶來壓力。那么,有沒有好的解決方法可以分擔主庫壓力呢?

其實是有的,這就是“主 - 從 - 從”模式。

主從庫模式中,所有的從庫都是和主庫連接,所有的全量復制也都是和主庫進行的。現在,我們可以通過“主 - 從 - 從”模式將主庫生成 RDB 和傳輸 RDB 的壓力,以級聯的方式分散到從庫上。

簡單來說,我們在部署主從集群的時候,可以手動選擇一個從庫(比如選擇內存資源配置較高的從庫),用於級聯其他的從庫。然后,我們可以再選擇一些從庫(例如三分之一的從庫),在這些從庫上執行如下命令,讓它們和剛才所選的從庫,建立起主從關系。

replicaof  所選從庫的IP 6379

這樣一來,這些從庫就會知道,在進行同步時,不用再和主庫進行交互了,只要和級聯的從庫進行寫操作同步就行了,這就可以減輕主庫上的壓力,如下圖所示:

 

 到這里,我們了解了主從庫間通過全量復制實現數據同步的過程,以及通過“主 - 從 - 從”模式分擔主庫壓力的方式。那么,一旦主從庫完成了全量復制,它們之間就會一直維護一個網絡連接,主庫會通過這個連接將后續陸續收到的命令操作再同步給從庫,這個過程也稱為基於長連接的命令傳播,可以避免頻繁建立連接的開銷。

聽上去好像很簡單,但不可忽視的是,這個過程中存在着風險點,最常見的就是網絡斷連或阻塞。如果網絡斷連,主從庫之間就無法進行命令傳播了,從庫的數據自然也就沒辦法和主庫保持一致了,客戶端就可能從從庫讀到舊數據。

 

主從庫間網絡斷了怎么辦?

在 Redis 2.8 之前,如果主從庫在命令傳播時出現了網絡閃斷,那么,從庫就會和主庫重新進行一次全量復制,開銷非常大。

從 Redis 2.8 開始,網絡斷了之后,主從庫會采用增量復制的方式繼續同步。聽名字大概就可以猜到它和全量復制的不同:全量復制是同步所有數據,而增量復制只會把主從庫網絡斷連期間主庫收到的命令,同步給從庫。

那么,增量復制時,主從庫之間具體是怎么保持同步的呢?這里的奧妙就在於 repl_backlog_buffer 這個緩沖區。我們先來看下它是如何用於增量命令的同步的。

當主從庫斷連后,主庫會把斷連期間收到的寫操作命令,這時候 replication buffer不存在的,會把這些操作命令也寫入 repl_backlog_buffer 這個緩沖區。

這里解釋一下repl_backlog_buffer和replication buffer概念:

  1、repl_backlog_buffer:就是上面我解釋到的,它是為了從庫斷開之后,如何找到主從差異數據而設計的環形緩沖區,從而避免全量同步帶來的性能開銷。如果從庫斷開時間太久,repl_backlog_buffer環形緩沖區被主庫的寫命令覆蓋了,那么從庫連上主庫后只能乖乖地進行一次全量同步,所以repl_backlog_buffer配置盡量大一些,可以降低主從斷開后全量同步的概率。而在repl_backlog_buffer中找主從差異的數據后,如何發給從庫呢?這就用到了replication buffer。

  2、replication buffer:Redis和客戶端通信也好,和從庫通信也好,Redis都需要給分配一個 內存buffer進行數據交互,客戶端是一個client,從庫也是一個client,我們每個client連上Redis后,Redis都會分配一個client buffer,所有數據交互都是通過這個buffer進行的:Redis先把數據寫到這個buffer中,然后再把buffer中的數據發到client socket中再通過網絡發送出去,這樣就完成了數據交互。所以主從在增量同步時,從庫作為一個client,也會分配一個buffer,只不過這個buffer專門用來傳播用戶的寫命令到從庫,保證主從數據一致,我們通常把它叫做replication buffer。

repl_backlog_buffer 是一個環形緩沖區,主庫會記錄自己寫到的位置,從庫則會記錄自己已經讀到的位置。

剛開始的時候,主庫和從庫的寫讀位置在一起,這算是它們的起始位置。隨着主庫不斷接收新的寫操作,它在緩沖區中的寫位置會逐步偏離起始位置,我們通常用偏移量來衡量這個偏移距離的大小,對主庫來說,對應的偏移量就是 master_repl_offset。主庫接收的新寫操作越多,這個值就會越大。

同樣,從庫在復制完寫操作命令后,它在緩沖區中的讀位置也開始逐步偏移剛才的起始位置,此時,從庫已復制的偏移量 slave_repl_offset 也在不斷增加。正常情況下,這兩個偏移量基本相等。

 

 主從庫的連接恢復之后,從庫首先會給主庫發送 psync 命令,並把自己當前的 slave_repl_offset 發給主庫,主庫會判斷自己的 master_repl_offset 和 slave_repl_offset 之間的差距。

在網絡斷連階段,主庫可能會收到新的寫操作命令,所以,一般來說,master_repl_offset 會大於 slave_repl_offset。此時,主庫只用把 master_repl_offset 和 slave_repl_offset 之間的命令操作同步給從庫就行。

就像剛剛示意圖的中間部分,主庫和從庫之間相差了 put d e 和 put d f 兩個操作,在增量復制時,主庫只需要把它們同步給從庫,就行了。

回顧下增量復制的流程。

 

 有一個地方我要強調一下,因為 repl_backlog_buffer 是一個環形緩沖區,所以在緩沖區寫滿后,主庫會繼續寫入,此時,就會覆蓋掉之前寫入的操作。如果從庫的讀取速度比較慢,就有可能導致從庫還未讀取的操作被主庫新寫的操作覆蓋了,這會導致主從庫間的數據不一致。

因此,我們要想辦法避免這一情況,一般而言,我們可以調整 repl_backlog_size 這個參數。這個參數和所需的緩沖空間大小有關。緩沖空間的計算公式是:緩沖空間大小 = 主庫寫入命令速度 * 操作大小 - 主從庫間網絡傳輸命令速度 * 操作大小。在實際應用中,考慮到可能存在一些突發的請求壓力,我們通常需要把這個緩沖空間擴大一倍,即 repl_backlog_size = 緩沖空間大小 * 2,這也就是 repl_backlog_size 的最終值。

舉個例子,如果主庫每秒寫入 2000 個操作,每個操作的大小為 2KB,網絡每秒能傳輸 1000 個操作,那么,有 1000 個操作需要緩沖起來,這就至少需要 2MB 的緩沖空間。否則,新寫的命令就會覆蓋掉舊操作了。為了應對可能的突發壓力,我們最終把 repl_backlog_size 設為 4MB。

這樣一來,增量復制時主從庫的數據不一致風險就降低了。不過,如果並發請求量非常大,連兩倍的緩沖空間都存不下新操作請求的話,此時,主從庫數據仍然可能不一致。

針對這種情況,一方面,你可以根據 Redis 所在服務器的內存資源再適當增加 repl_backlog_size 值,比如說設置成緩沖空間大小的 4 倍,另一方面,你可以考慮使用切片集群來分擔單個主庫的請求壓力。

拓展問題:

AOF 記錄的操作命令更全,相比於 RDB 丟失的數據更少。那么,為什么主從庫間的復制不使用 AOF 呢?

  1、RDB文件內容是經過壓縮的二進制數據(不同數據類型數據做了針對性優化),文件很小。而AOF文件記錄的是每一次寫操作的命令,寫操作越多文件會變得很大,其中還包括很多對同一個key的多次冗余操作。在主從全量數據同步時,傳輸RDB文件可以盡量降低對主庫機器網絡帶寬的消耗,從庫在加載RDB文件時,一是文件小,讀取整個文件的速度會很快,二是因為RDB文件存儲的都是二進制數據,從庫直接按照RDB協議解析還原數據即可,速度會非常快,而AOF需要依次重放每個寫命令,這個過程會經歷冗長的處理邏輯,恢復速度相比RDB會慢得多,所以使用RDB進行主從全量同步的成本最低。

  2、假設要使用AOF做全量同步,意味着必須打開AOF功能,打開AOF就要選擇文件刷盤的策略,選擇不當會嚴重影響Redis性能。而RDB只有在需要定時備份和主從全量同步數據時才會觸發生成一次快照。而在很多丟失數據不敏感的業務場景,其實是不需要開啟AOF的。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM