Redis的主從資料一致性

妮蔻發表於2021-07-06

我們學習了 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的。

相關文章