滴滴 Redis 異地多活的演進歷程

陶然陶然發表於2023-11-20

來源:滴滴技術

為了更好的做好容災保障,使業務能夠應對機房級別的故障,滴滴的儲存服務都在多機房進行部署。本文簡要分析了 Redis 實現異地多活的幾種思路,以及滴滴 Redis 異地多活架構演進過程中遇到的主要問題和解決方法,拋磚引玉,給小夥伴們一些參考。

Redis 異地多活的主要思路

業界實現 Redis 異地多活通常三種思路:主從架構、Proxy雙寫架構、資料層雙向同步架構。

主從架構

滴滴 Redis 異地多活的演進歷程

主從架構的思路:

  1. 各機房的 Redis 透過 Proxy 對外提供讀寫服務,業務流量讀寫本機房的 Redis-proxy

  2. 主機房裡的 Redis-master 例項承擔所有機房的寫流量

  3. 從機房裡的 Redis-slave 例項只讀,承擔本機房裡的讀流量

主從架構的優點

  • 實現簡單,在 Proxy 層開發讀寫分流功能就可以實現

  • Redis 層使用原生主從複製,可以保證資料一致性

主從架構的缺點

  • 從機房裡的 Redis-proxy 需要跨機房寫,受網路延時影響,業務在從機房裡的寫耗時高於主機房

  • 主機房故障時,從機房的寫流量也會失敗,需要把從機房切換為主機房,切換 Redis-master

  • 網路故障時,從機房的寫流量會全部失敗,為了保障資料一致性,這種場景比較難處理

Proxy 雙寫架構  

滴滴 Redis 異地多活的演進歷程

Proxy 雙寫架構的思路:

  1. 各機房的 Redis 透過 Proxy 對外提供讀寫服務,業務流量讀寫本機房的 Redis-proxy

  2. 不區分主從機房,每個機房都是獨立的 Redis 叢集

  3. 各機房的讀寫流量都是訪問本機房的 Redis 叢集

  4. Proxy 層在寫本機房成功後,將寫請求非同步傳送到對端機房

Proxy 雙寫架構的優點:

  • 實現簡單,在 Proxy 層開發雙寫功能就可以實現

  • 一個機房故障時,其他機房的流量不受影響

  • 網路故障時,各機房內部的流量也不受影響

Proxy 雙寫架構的缺點:

  • 不能保證資料一致性,Proxy 非同步 write 請求可能會失敗,失敗丟棄請求後,導致雙機房資料不一致

  • 假設機房-A的叢集先上線,機房-B 後上線,Proxy 雙寫架構不能支援把機房-A的存量資料同步到機房-B

  • 網路故障時,非同步 write 會失敗後丟棄,網路恢復後,之前失敗的資料已經丟棄,導致雙機房資料不一致


資料層雙向同步架構   

滴滴 Redis 異地多活的演進歷程

     

資料層雙向同步架構的思路:

  1. Proxy 不關心底層 Redis 資料同步

  2. 業務流量只訪問本機房裡的 Redis 叢集

  3. 在 RedisServer 層面實現資料同步

資料層雙向同步架構的優點:

  • 機房-A故障時,機房-B不受影響,反向如是

  • 網路故障時,本機房流量不受影響,網路恢復後,資料層面可以拉取增量資料繼續同步,資料不丟

  • 支援存量資料的同步

  • 業務訪問 Redis 延時低,訪問鏈路不受機房間網路延時影響

  • 業務單元化部署時,雙機房 Redis 會有較高的資料一致性

資料層雙向同步架構的缺點:

  • 實現相對比較複雜,RedisServer 改動比較大

滴滴 Redis 架構

Codis 架構(早期架構,現已廢棄)

滴滴 Redis 異地多活的演進歷程

Kedis 架構(線上架構)

滴滴 Redis 異地多活的演進歷程

滴滴 Redis 異地多活架構的演進

第一代多活架構       

滴滴 Redis 異地多活的演進歷程


第一代 Redis 多活基於 Codis 架構在 proxy 層實現了雙寫,即本機房的 Proxy 將寫流量轉發到對端機房的 Proxy,這個方案的特點是快速實現,儘快滿足了業務多機房同步的需求。如前面 Proxy 雙向架構思路所講,本方案還存在著諸多缺點,最主要的是網路故障時,同步資料丟失的問題,為了解決這些問題,我們開發了第二代多活架構。


第二代多活架構

滴滴 Redis 異地多活的演進歷程

滴滴 Redis 異地多活的演進歷程


第二代多活基於 Kedis 架構,對 Redis-server 進行改造,可以把增量資料從 Redis 直接寫入本機房的 MQ 中,由對端機房的 consumer 來消費 MQ,consumer 將資料寫入對端 Redis 中。網路故障時,資料會在 MQ 堆積,待網路恢復後,consumer 可以基於故障前的 offset 繼續進行消費,寫入對端 Redis,從而保證在網路故障時 Redis 活不會丟資料。


但這一代架構仍不夠完美,存在以下問題:

  • ProducerThread 把資料寫入 MQ 時,如果觸發 MQ 限流,資料會被丟掉

  • RedisServer 內部包含了 ProducerThread,當中間內部 queue 累積資料量超過10000條時,資料會被 MainThread 丟掉

  • 中間同步資料寫入 MQ,增加了跨部門依賴,同步鏈路長,不利於系統穩定性

  • 中間同步鏈路重試會造成非冪等命令執行多次,例如 incrby 重試可能造成命令執行多次造成資料不一致

  • 對於新建雙活鏈路,不支援同步存量資料,只能從當前增量資料開始同步

  • Redis 增量資料寫入 MQ,導致成本增加


為了解決以上問題,我們開發了第三代架構。


第三代多活架構

在第三代架構中,我們細化了設計目標,主要思路是保證同步鏈路中的資料不丟不重,同時去掉對 MQ 的依賴,降低多活成本。

       

滴滴 Redis 異地多活的演進歷程


第三代架構中,我們去掉了 MQ 和 consumer,新增了 syncer 元件。syncer 元件模擬 Redis-slave 從 Redis-master 中拉取增量資料,這樣把資料同步和 Redis 進行解耦,便於後續多機房擴充套件。


第三代架構中,Redis 遇到了迴環、重試、資料衝突、增量資料儲存和讀取等問題,接下來一一介紹我們應對這些問題的解決方案。


1、迴環問題

機房-A 寫入的資料同步到機房-B,防止資料再傳回機房-A。

滴滴 Redis 異地多活的演進歷程

為了解決迴環問題,我們開發了防迴環機制:

  1. Redis 增加 shardID 配置,標識唯一分片號

  2. Redis 請求中增加 opinfo,記錄元資訊,包含 shardID   

滴滴 Redis 異地多活的演進歷程

  • 機房-A 的 Proxy 寫入了 set k v 請求

  • 機房-A 的 Redis-master 向 syncer 同步 set k v opinfo[shardID-1] 請求

  • syncer 向機房-B 寫入 set k v opinfo[shardID-1] 請求

  • 這樣機房-B 根據 shardID-1 識別出這條請求是機房-A 生產的資料,因此不會再向機房-A 同步本條請求


2、重試問題

機房-A 寫入的 incrby 請求同步到機房-B,由於中間鏈路的重試,導致機房-B 可能執行了多次。 

滴滴 Redis 異地多活的演進歷程

為了解決重試問題,我們開發了防重放機制:

  1. Redis 增加 opid,標識唯一請求號

  2. Redis 請求中增加 opinfo,記錄元資訊[opid]

       

滴滴 Redis 異地多活的演進歷程

  • 機房-A 的 Proxy 寫入了 incrby k 1 請求

  • 機房-A 的 Redis-master 向 syncer 同步了 incrby k 1 opinfo[opid=100] 請求, 之前同步的 opid=99 的請求已經成功

  • syncer 向機房-B 寫入 incrby k 1 opinfo[opid=100] 請求

  • 機房-B 的 Redis 裡儲存了防重放資訊 shardID-1->opid[99]

  • 機房-B 的 Redis 發現新請求的 opid=100>本地的99,判斷為新請求

  • 機房-B的 Redis 執行這條請求,並把防重放資訊更新為shardID-1->opid[100]

  • 假設機房-A 的 syncer 將本條請求進行了重試,又執行了一遍 incrby k 1 opinfo[opid=100]

  • 機房-B 的 Redis 發現新請求 opid=100 等於本地的100,判斷為重複請求

  • 機房-B 的 Redis 忽略掉本地請求,不執行


3、資料衝突問題

雙機房同時修改同一個 key 導致資料不一致

滴滴 Redis 異地多活的演進歷程

對於資料衝突,不同資料型別的不同操作的資料合併,如果單從儲存層解決,是一個非常複雜的話題。如果業務層做了單元化部署,則不會出現這種問題。如果業務層沒有做單元化,我們開發了衝突檢測功能,來幫助業務及時發現資料衝突,最後資料以哪邊為準來修正,需要業務同學來決策。


衝突檢測機制:

  1. Redis 記錄 key 的最後 write 時間

  2. Redis 請求中增加 opinfo,記錄元資訊 [timestamp]

  3. 如果 opinfo.timestamp<=key_write_time,則記錄衝突 key

滴滴 Redis 異地多活的演進歷程

時間T1<T2<T3

  • T1時間,使用者在機房-A 寫入請求 set k v1

  • T2時間,使用者在機房-B 寫入請求 set k v2,並記錄k的最後修改時間為T2

  • 由於網路同步延時,T3時間,syncer 把T1時間寫入的 set k v1請求傳送到了機房-B

  • 機房-B 的 Redis 執行 set k v1 時發現 timestamp 為T1,但 k 的最後修改時間為T2

  • 由於T1<T2,機房-B 的 Redis 判斷這是一次衝突,並記錄下來,然後執行該條請求


以上是衝突檢測的基本原理,這是一個旁路統計,幫助使用者發現一些潛在衝突資料。


4、增量資料儲存和讀取問題

因為 syncer 只是同步元件,不會儲存資料,所以需要考慮當網路故障時,增量資料的儲存和讀取問題。

滴滴 Redis 異地多活的演進歷程

為了解決這個問題,我們對 Redis 的 aof 機制進行了改造,可以在網路故障時,增量資料都堆積在 Redis 的磁碟上,在網路恢復後,syncer 從 Redis 里拉取增量 aof 資料傳送到對端機房,避免資料丟失。


aof 機制改造有:aof 檔案切分、aof 增量複製、aof 非同步寫盤

滴滴 Redis 異地多活的演進歷程

  • 將 aof 檔案切分為多個小檔案,儲存增量資料

  • 當增量資料超過配置的閾值時,Redis 自動刪除最舊的 aof 檔案

  • 當 Redis 重啟時,載入 rdb 檔案和 rdb 之後的 aof 檔案,可以恢復全部資料

  • 當網路故障恢復後,syncer 根據故障前的 opid 向 Redis 請求拉取增量資料,傳送到對端機房

滴滴 Redis 異地多活的演進歷程

開源 Redis 是在主執行緒中進行 aof 寫盤,當磁碟 IO 過高時,Redis 寫盤可能造成業務訪問 Redis 耗時抖動。因此我們開發了 aof 非同步寫盤機制:

  • Redis 的主執行緒將 aof 資料寫入 queue 中

  • bio 執行緒來消費 queue

  • bio 執行緒將 aof 資料寫入磁碟


這樣 Redis 的訪問耗時不受磁碟 IO 的影響,更好的保證穩定性。


來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/70024420/viewspace-2995380/,如需轉載,請註明出處,否則將追究法律責任。

相關文章