一、背景
公司基于業務發展以及戰略部署,需要實現在多個數據中心單元化部署,一方面可以實現多數據中心容災,另外可以提升用戶請求訪問速度。需要保證多數據中心容災或者實現用戶就近訪問的話,需要各個數據中心擁有一致的全量數據,如果真正實現用戶就近讀寫,也就是實現真正的業務異地多活,數據同步是異地多活的基礎,這就需要多數據中心間數據能夠雙向同步。
二、原生redis遇到的問題
1、不支持雙主同步
原生redis并沒有提供跨機房的主主同步機制,僅支持主從同步;如果僅利用redis的主從數據同步機制,只能將主節點與從節點部署在不同的機房。當主節點所在機房出現故障時,從節點可以升級為主節點,應用可以持續對外提供服務。但這種模式下,若要寫數據,則只能通過主節點寫,異地機房無法實現就近寫入,所以不能做到真正的異地多活,只能做到備份容災。而且機房故障切換時,需要運維手動介入。
因此,想要實現主主同步機制,需要同步工具模擬從節點方式,將本地機房中數據同步到其他機房,其他機房亦如此。同時,使用同步工具實現跨數據中心數據同步,會遇到以下一些問題。
(1)數據回環
數據回環的意思是,A機房就近寫入的數據,通過同步工具同步到B機房后,然后又通過B機房同步工具同步回A機房了。所以在同步的過程中需要識別本地就近寫入的數據還是其他數據中心同步過來的數據,只有本地就近寫入的數據需要同步到其他數據中心。
(2)冪等性
同步過程中的命令可能因斷點續傳等原因導致重復同步了,此時需要保證同一命令多次執行保證冪等。
(3)多寫沖突
以雙寫沖突為例,如下圖所示:
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}rn形式的協議。
本地業務客戶端寫入的數據依然遵循原生RESP協議,主節點執行完命令后,同步到從節點的寫命令在同步前會進行協議擴展,增加頭部id協議;非本地業務客戶端(即來自其他數據中心同步)寫入的數據均使用擴展的RESP協議。
2、寫命令實時寫日志
為了支持更長時間的斷點續傳,容忍長時間的機房級故障,本地業務客戶端寫入的寫命令在進行協議擴展后,會順序寫入日志文件,同時生成對應的索引文件;為了減少日志文件大小,以及提高通過日志文件斷點續傳的效率,來自其他數據中心同步過來的數據不寫入日志文件中。
3、同步流程改造
原生redis數據同步分為全量同步和部分同步,并且每個主節點有一個內存環形復制緩沖區;初次同步使用全量同步,斷點續傳時使用部分同步,即先嘗試從主節點環形復制緩沖區中進行同步,同步成功的話則同步完緩沖區中的數據后即可進行增量數據同步,如果不成功,則仍然需要先進行全量同步再增量同步。
由于全量同步需要生成一個子進程,并且在子進程中生成一個RDB文件,所以對主節點性能影響比較大,我們應該盡量減少全量同步的次數。
為了減少全量同步的次數,我們對redis同步流程進行改造,當部分同步中無法使用環形復制緩沖區完成同步時,增加先嘗試使用日志rlog進行同步,如果同步成功,則同步完日志中數據后即可進行增量同步,否則需要先進行全量同步。
四、rLog日志設計
分為索引文件與日志文件,均采用順序寫的方式,提高性能,經測試與原生redis開啟aof持久化性能一致;但是rlog會定期刪除,原生redis為了防止aof文件無限膨脹,會定期通過子進程執行aof文件重寫,這個對主節點性能比較大,所以實質上rlog對redis的性能相對于aof會更小。
索引文件和日志文件文件名均為文件中保存的第一條命令的id。
索引文件與日志文件均先寫內存緩沖區,然后批量寫入操作系統緩沖區,并每秒定期刷新操作系統緩沖區真正落入磁盤文件中。相比較于aof文件緩沖區,我們對rlog緩沖區進行了預分配優化,達到提升性能目的。
1、索引文件格式
索引文件格式如下所示,每條命令對應的索引數據包含三部分:
- pos:該條命令第一個字節在對應的日志文件中相對于該日志文件起始位置的偏移
- len:該條命令的長度
- offset:該條命令第一個字節在主節點復制緩沖區中累積的偏移
2、日志文件拆分
為了防止單個文件無限膨脹,redis在寫文件時會定期對文件進行拆分,拆分依據兩個維度,分別是文件大小和時間。
默認拆分閾值分別為,當日志文件大小達到128M或者每隔一小時同時并且日志條目數大于10w時,寫新的日志文件和索引文件。
在每次循環處理中,當內存緩沖區的數據全部寫入文件時,判斷是否滿足日志文件拆分條件,如果滿足,加上一個日志文件拆分標志,下一次循環處理中,將內存緩沖區數據寫入文件之前,先關閉當前的索引文件和日志,同時新建索引文件和日志文件。
3、日志文件刪除
為了防止日志文件數量無限增長并且消耗磁盤存儲空間,以及由于未做日志重寫、通過過多的文件進行斷點續傳效率低下、意義不大,所以redis定期對日志文件和相應的索引文件進行刪除。
默認日志文件最多保留一天,redis定期刪除一天以前的日志文件和索引文件,也就是最多容忍一天時間的機房級故障,否則需要進行機房間數據全量同步。
在斷點續傳時,如果需要從日志文件中同步數據,在同步開始前會臨時禁止日志文件刪除邏輯,待同步完成后恢復正常,避免出現在同步的數據被刪除的情況。
五、redis數據同步
1、斷點續傳
如前所述,為了容忍更長時間的機房級故障,提高跨數據中心容災能力,提升機房間故障恢復效率,我們對redis同步流程進行改造,當部分同步中無法使用環形復制緩沖區完成同步時,增加先嘗試使用日志rlog進行同步,流程圖如下所示:
首先,同步工具連接上主節點后,除了發送認證外,需要先通過replconf capa命令告知主節點具備通過rlog斷點續傳的能力。
- 從節點先發送psync runId offset,如果是第一次啟動,則先發送psync ? -1,主節點會返回一個runId和offset
- 如果能夠通過復制緩沖區同步,主節點給從節點返回 +CONTINUE runId
- 如果不能夠通過復制緩沖區同步,主節點給從節點返回 +LPSYNC
- 如果從節點收到+CONTINUE,則繼續接收增量數據即可,并繼續更新offset和命令id
- 如果從節點收到+LPSYNC,則從節點繼續給主節點發送 LPSYNC runId id
- 主節點收到LPSYNC命令后,如果能夠通過rlog繼續同步數據,則給從節點發送 +LCONTINUE runId;
- 從節點收到+LCONTINUE后,可以把offset設置為LONG_LONG_MIN,或者后續數據不更新offset;繼續接收通過rlog同步的增量數據即可;
- 通過rlog同步的增量數據傳輸完畢后,主節點會給從節點發送 lcommit offset命令;
- 從節點在解析數據的過程中,收到lcommit命令時,更新本地offset,后續的增量數據繼續增加offset,同時lcommit命令無需同步到對端(通過id<0識別即可,所有id<0的命令均無需同步到對端)
- 如果不能,此時主節點給從節點返回 +FULLRESYNC runId offset;后續進行全量同步;
2、冪等性
遷移工具為了提高性能,并不是實時往zk保存同步偏移offset和id,而是定期(默認每秒)向zk進行同步,所以當斷點續傳時,遷移工具從zk獲取斷線前同步的偏移,嘗試向主節點繼續同步數據,這中間可能會有部分數據重復發送,所以為了保證數據一致性,需要保證命令多次執行具備冪等性。
為了保證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 benchmark進行壓測,壓測時,客戶端和服務端在同一個機器上
七、待優化項
1、多寫沖突
多個數據中心同時寫,key沖突問題暫未解決。
后續解決方案為使用CRDT協議;CRDT(Conflict-Free Replicated Data Type)是各種基礎數據結構最終一致算法的理論總結,能根據一定的規則自動合并,解決沖突,達到強最終一致的效果。
目前解決方案為業務對寫入不同機房的數據進行拆分,以保證不會出現沖突。
2、list類型冪等性
五種基本類型里面,list類型大部分操作都是非冪等的,暫時未做冪等性改造優化。不建議使用或者業務自身保證使用list的數據操作冪等。
3、過期與淘汰數據一致性問題
正如前文所述,淘汰數據不進行跨數據中心同步會導致數據不一致,如果同步數據可能會出現同一個Key多寫沖突,也可能出現數據不一致情況。
目前解決方案為業務盡量合理提前預估所需內存容量、充足預分配、及時關注內存使用率告警,來規避淘汰數據現象發生。
作者:羅明