原生 Redis 跨資料中心雙向同步最佳化實踐

撈起月亮的漁民發表於2022-09-29

一、背景

公司基於業務發展以及戰略部署,需要實現在多個資料中心單元化部署,一方面可以實現多資料中心容災,另外可以提升使用者請求訪問速度。需要保證多資料中心容災或者實現使用者就近訪問的話,需要各個資料中心擁有一致的全量資料,如果真正實現使用者就近讀寫,也就是實現真正的業務異地多活,資料同步是異地多活的基礎,這就需要多資料中心間資料能夠雙向同步。

二、原生 redis 遇到的問題

1、不支援雙主同步

原生 redis 並沒有提供跨機房的主主同步機制,僅支援主從同步;如果僅利用 redis 的主從資料同步機制,只能將主節點與從節點部署在不同的機房。當主節點所在機房出現故障時,從節點可以升級為主節點,應用可以持續對外提供服務。但這種模式下,若要寫資料,則只能透過主節點寫,異地機房無法實現就近寫入,所以不能做到真正的異地多活,只能做到備份容災。而且機房故障切換時,需要運維手動介入。

因此,想要實現主主同步機制,需要同步工具模擬從節點方式,將本地機房中資料同步到其他機房,其他機房亦如此。同時,使用同步工具實現跨資料中心資料同步,會遇到以下一些問題。

(1)資料迴環

資料迴環的意思是,A 機房就近寫入的資料,透過同步工具同步到 B 機房後,然後又透過 B 機房同步工具同步回 A 機房了。所以在同步的過程中需要識別本地就近寫入的資料還是其他資料中心同步過來的資料,只有本地就近寫入的資料需要同步到其他資料中心。

(2)冪等性

同步過程中的命令可能因斷點續傳等原因導致重複同步了,此時需要保證同一命令多次執行保證冪等。

(3)多寫衝突

以雙寫衝突為例,如下圖所示:

原生 Redis 跨資料中心雙向同步最佳化實踐

 

DC1 寫入 set a 1,同時 DC2 寫入 set a 2,當這兩條命令透過同步工具同步到對方機房時,導致最終 DC1 中儲存的 a 為 2,DC2 中儲存的 a 為 1,也就是說兩個機房最終資料不一致。

2、斷點續傳

針對瞬時的斷開重連、從節點重啟等場景,redis 為了提高該場景下的主從同步效率,在主節點中增加了環形複製緩衝區,主節點往從節點寫資料的同時也往復制緩衝區中也寫入一份資料,當從節點斷開重連時,則只需要透過複製緩衝區把斷開期間新增的增量資料傳送給從節點即可,避免了全量同步,提升了這些場景下的同步效率。

但是,該記憶體複製緩衝區一般來說不會太大,生產目前預設設定為 64M,跨資料中心同步場景下,網路環境複雜,斷線的頻率和時長可能比同機房更頻繁和更長;同時,跨資料中心同步資料也是為了機房級故障容災,所以要求能夠支援更長時間的斷點續傳,無限增大記憶體複製緩衝區大小顯然不是一個好主意。

下面來看看我們支援 redis 跨資料中心同步的最佳化工作。

三、redis 節點改造

為了支援異地多活場景,我們對原生 redis 程式碼進行了最佳化改造,主要包括以下幾個方面:

1、對 RESP 協議進行擴充套件

為了支援更高效的斷點續傳,以及為了解決資料迴環問題,我們在 redis 主節點中對每條需要同步給從節點的命令(大部分為寫命令)增加了 id,並且擴充套件了 RESP 協議,在每條相關命令的頭部增加了形如 #{id}\r\n 形式的協議。

本地業務客戶端寫入的資料依然遵循原生 RESP 協議,主節點執行完命令後,同步到從節點的寫命令在同步前會進行協議擴充套件,增加頭部 id 協議;非本地業務客戶端(即來自其他資料中心同步)寫入的資料均使用擴充套件的 RESP 協議。

2、寫命令實時寫日誌

為了支援更長時間的斷點續傳,容忍長時間的機房級故障,本地業務客戶端寫入的寫命令在進行協議擴充套件後,會順序寫入日誌檔案,同時生成對應的索引檔案;為了減少日誌檔案大小,以及提高透過日誌檔案斷點續傳的效率,來自其他資料中心同步過來的資料不寫入日誌檔案中。

3、同步流程改造

原生 redis 資料同步分為全量同步和部分同步,並且每個主節點有一個記憶體環形複製緩衝區;初次同步使用全量同步,斷點續傳時使用部分同步,即先嚐試從主節點環形複製緩衝區中進行同步,同步成功的話則同步完緩衝區中的資料後即可進行增量資料同步,如果不成功,則仍然需要先進行全量同步再增量同步。

由於全量同步需要生成一個子程式,並且在子程式中生成一個 RDB 檔案,所以對主節點效能影響比較大,我們應該儘量減少全量同步的次數。

為了減少全量同步的次數,我們對 redis 同步流程進行改造,當部分同步中無法使用環形複製緩衝區完成同步時,增加先嚐試使用日誌 rlog 進行同步,如果同步成功,則同步完日誌中資料後即可進行增量同步,否則需要先進行全量同步。

四、rLog 日誌設計

分為索引檔案與日誌檔案,均採用順序寫的方式,提高效能,經測試與原生 redis 開啟 aof 持久化效能一致;但是 rlog 會定期刪除,原生 redis 為了防止 aof 檔案無限膨脹,會定期透過子程式執行 aof 檔案重寫,這個對主節點效能比較大,所以實質上 rlog 對 redis 的效能相對於 aof 會更小。

索引檔案和日誌檔案檔名均為檔案中儲存的第一條命令的 id。

索引檔案與日誌檔案均先寫記憶體緩衝區,然後批次寫入作業系統緩衝區,並每秒定期重新整理作業系統緩衝區真正落入磁碟檔案中。相比較於 aof 檔案緩衝區,我們對 rlog 緩衝區進行了預分配最佳化,達到提升效能目的。

1、索引檔案格式

索引檔案格式如下所示,每條命令對應的索引資料包含三部分:

原生 Redis 跨資料中心雙向同步最佳化實踐

  • pos:該條命令第一個位元組在對應的日誌檔案中相對於該日誌檔案起始位置的偏移

  • len:該條命令的長度

  • offset:該條命令第一個位元組在主節點複製緩衝區中累積的偏移

2、日誌檔案拆分

為了防止單個檔案無限膨脹,redis 在寫檔案時會定期對檔案進行拆分,拆分依據兩個維度,分別是檔案大小和時間。

預設拆分閾值分別為,當日志檔案大小達到 128M 或者每隔一小時同時並且日誌條目數大於 10w 時,寫新的日誌檔案和索引檔案。

在每次迴圈處理中,當記憶體緩衝區的資料全部寫入檔案時,判斷是否滿足日誌檔案拆分條件,如果滿足,加上一個日誌檔案拆分標誌,下一次迴圈處理中,將記憶體緩衝區資料寫入檔案之前,先關閉當前的索引檔案和日誌,同時新建索引檔案和日誌檔案。

3、日誌檔案刪除

為了防止日誌檔案數量無限增長並且消耗磁碟儲存空間,以及由於未做日誌重寫、透過過多的檔案進行斷點續傳效率低下、意義不大,所以 redis 定期對日誌檔案和相應的索引檔案進行刪除。

預設日誌檔案最多保留一天,redis 定期刪除一天以前的日誌檔案和索引檔案,也就是最多容忍一天時間的機房級故障,否則需要進行機房間資料全量同步。

在斷點續傳時,如果需要從日誌檔案中同步資料,在同步開始前會臨時禁止日誌檔案刪除邏輯,待同步完成後恢復正常,避免出現在同步的資料被刪除的情況。

五、redis 資料同步

1、斷點續傳

如前所述,為了容忍更長時間的機房級故障,提高跨資料中心容災能力,提升機房間故障恢復效率,我們對 redis 同步流程進行改造,當部分同步中無法使用環形複製緩衝區完成同步時,增加先嚐試使用日誌 rlog 進行同步,流程圖如下所示:

原生 Redis 跨資料中心雙向同步最佳化實踐

 

首先,同步工具連線上主節點後,除了傳送認證外,需要先透過 replconf capa 命令告知主節點具備透過 rlog 斷點續傳的能力。

  1. 從節點先傳送 psync runId offset,如果是第一次啟動,則先傳送 psync ? -1,主節點會返回一個 runId 和 offset

  2. 如果能夠透過複製緩衝區同步,主節點給從節點返回 +CONTINUE runId

  3. 如果不能夠透過複製緩衝區同步,主節點給從節點返回 +LPSYNC

  4. 如果從節點收到 + CONTINUE,則繼續接收增量資料即可,並繼續更新 offset 和命令 id

  5. 如果從節點收到 + LPSYNC,則從節點繼續給主節點傳送 LPSYNC runId id

  6. 主節點收到 LPSYNC 命令後,如果能夠透過 rlog 繼續同步資料,則給從節點傳送 +LCONTINUE runId;

  7. 從節點收到 + LCONTINUE 後,可以把 offset 設定為 LONG_LONG_MIN,或者後續資料不更新 offset;繼續接收透過 rlog 同步的增量資料即可;

  8. 透過 rlog 同步的增量資料傳輸完畢後,主節點會給從節點傳送 lcommit offset 命令;

  9. 從節點在解析資料的過程中,收到 lcommit 命令時,更新本地 offset,後續的增量資料繼續增加 offset,同時 lcommit 命令無需同步到對端(透過 id<0 識別即可,所有 id<0 的命令均無需同步到對端)

  10. 如果不能,此時主節點給從節點返回 +FULLRESYNC runId offset;後續進行全量同步;

2、冪等性

遷移工具為了提高效能,並不是實時往 zk 儲存同步偏移 offset 和 id,而是定期(預設每秒)向 zk 進行同步,所以當斷點續傳時,遷移工具從 zk 獲取斷線前同步的偏移,嘗試向主節點繼續同步資料,這中間可能會有部分資料重複傳送,所以為了保證資料一致性,需要保證命令多次執行具備冪等性。

為了保證 redis 命令具備冪等性,對 redis 中部分非冪等性命令進行了改造,具體設計改造的命令如下所示:

原生 Redis 跨資料中心雙向同步最佳化實踐

 

注:list 型別命令暫未改造,不具備冪等性

3、資料迴環處理

資料迴環主要是指,當同步工具從 A 機房 redis 讀取的資料,透過 MQ 同步到 B 機房寫入後,B 機房的同步工具又獲取到,再次同步到 A 機房,導致資料迴圈複製問題。

對於同步到從節點以及遷移工具的資料,會在頭部新增 id 欄位,針對不同來源的資料或者無需同步到遠端的資料透過 id 來標識區分;本地業務客戶端寫入的資料需要同步到遠端資料中心,分配 id 大於 0;來源於其他資料中心的資料分配 id 小於 0;一些僅用於主從心跳互動的命令資料分配 id 也小於 0。

同步工具解析完資料後,過濾掉 id 小於 0 的命令,只需要向遠端寫入 id 大於 0 的資料,即本地業務客戶端寫入的資料。來源於其他資料中心的資料均不回寫到遠端資料中心。

4、過期與淘汰資料

目前過期與淘汰均由各資料中心 redis 節點分別獨立處理,由過期與淘汰刪除的資料不進行同步;即由過期與淘汰產生的刪除命令其 id 分配為小於 0,並由同步工具過濾掉。

(1)同步產生的問題

為什麼不同步過去?因為在記憶體中 hash 表裡面儲存的資料沒有標記資料中心來源,過期與淘汰的資料有可能來自於其他資料中心,如果來自於其他資料中心的資料被過期或淘汰並且又同步到遠端其他資料中心,就會出現資料雙寫衝突的場景。雙寫衝突可能會導致資料不一致。

(2)不同步產生的問題

對於過期資料來說,不同步刪除可能會導致不同資料中心資料顯示不一致,但是一定會最終一致,且不會出現髒讀;

對於淘汰資料來說,目前的不同步刪除的方案,假如出現淘汰,會導致不同資料中心資料不一致;目前只有透過運維手段,比如充足預分配、及時關注記憶體使用率告警,來規避淘汰資料現象發生。

5、資料遷移

在 redis 叢集模式中,一般是在發生橫向擴容增加叢集主節點數時,需要進行槽以及資料的遷移。

redis 叢集中資料遷移以槽為維度進行遷移,將槽中所有資料從源節點遷移到目標節點,然後將槽號標記為由新的目標節點負責,同時每遷移完一個 Key,會在源節點中進行刪除,將 migrate 命令替換為 del 命令;同時遷移資料是在源節點中給目標節點傳送 restore 命令實現。

我們資料遷移的策略依然是,各個資料中心獨立的完成擴容與資料遷移工作,遷移過程產生的 del 和 restore 命令不進行跨資料中心同步;把替換後的 del 命令和傳送給目標節點的 restore 命令都分配小於 0 的 id,於是同步過程中會由同步工具進行過濾掉。

六、redis 效能

經測試,redis 多活例項(預設開啟 rlog 日誌),相對於原生 redis 例項(開啟 aof 持久化)效能基本一致;如下圖所示:

原生 Redis 跨資料中心雙向同步最佳化實踐

 注:以上圖表使用 redis benchmark 進行壓測,壓測時,客戶端和服務端在同一個機器上

七、待最佳化項

1、多寫衝突

多個資料中心同時寫,key 衝突問題暫未解決。

後續解決方案為使用 CRDT 協議;CRDT (Conflict-Free Replicated Data Type) 是各種基礎資料結構最終一致演算法的理論總結,能根據一定的規則自動合併,解決衝突,達到強最終一致的效果。

目前解決方案為業務對寫入不同機房的資料進行拆分,以保證不會出現衝突。

2、list 型別冪等性

五種基本型別裡面,list 型別大部分操作都是非冪等的,暫時未做冪等性改造最佳化。不建議使用或者業務自身保證使用 list 的資料操作冪等。

3、過期與淘汰資料一致性問題

正如前文所述,淘汰資料不進行跨資料中心同步會導致資料不一致,如果同步資料可能會出現同一個 Key 多寫衝突,也可能出現資料不一致情況。

目前解決方案為業務儘量合理提前預估所需記憶體容量、充足預分配、及時關注記憶體使用率告警,來規避淘汰資料現象發生。

作者:羅明


相關文章