滴滴 Redis 異地多活的演進歷程
爲了更好的做好容災保障,使業務能夠應對機房級別的故障,滴滴的存儲服務都在多機房進行部署。本文簡要分析了 Redis 實現異地多活的幾種思路,以及滴滴 Redis 異地多活架構演進過程中遇到的主要問題和解決方法,拋磚引玉,給小夥伴們一些參考。
Redis 異地多活的主要思路
業界實現 Redis 異地多活通常三種思路:主從架構、Proxy 雙寫架構、數據層雙向同步架構。
主從架構
主從架構的思路:
-
各機房的 Redis 通過 Proxy 對外提供讀寫服務,業務流量讀寫本機房的 Redis-proxy
-
主機房裏的 Redis-master 實例承擔所有機房的寫流量
-
從機房裏的 Redis-slave 實例只讀,承擔本機房裏的讀流量
主從架構的優點:
-
實現簡單,在 Proxy 層開發讀寫分流功能就可以實現
-
Redis 層使用原生主從複製,可以保證數據一致性
主從架構的缺點:
-
從機房裏的 Redis-proxy 需要跨機房寫,受網絡延時影響,業務在從機房裏的寫耗時高於主機房
-
主機房故障時,從機房的寫流量也會失敗,需要把從機房切換爲主機房,切換 Redis-master
-
網絡故障時,從機房的寫流量會全部失敗,爲了保障數據一致性,這種場景比較難處理
Proxy 雙寫架構
Proxy 雙寫架構的思路:
-
各機房的 Redis 通過 Proxy 對外提供讀寫服務,業務流量讀寫本機房的 Redis-proxy
-
不區分主從機房,每個機房都是獨立的 Redis 集羣
-
各機房的讀寫流量都是訪問本機房的 Redis 集羣
-
Proxy 層在寫本機房成功後,將寫請求異步發送到對端機房
Proxy 雙寫架構的優點:
-
實現簡單,在 Proxy 層開發雙寫功能就可以實現
-
一個機房故障時,其他機房的流量不受影響
-
網絡故障時,各機房內部的流量也不受影響
Proxy 雙寫架構的缺點:
-
不能保證數據一致性,Proxy 異步 write 請求可能會失敗,失敗丟棄請求後,導致雙機房數據不一致
-
假設機房 - A 的集羣先上線,機房 - B 後上線,Proxy 雙寫架構不能支持把機房 - A 的存量數據同步到機房 - B
-
網絡故障時,異步 write 會失敗後丟棄,網絡恢復後,之前失敗的數據已經丟棄,導致雙機房數據不一致
數據層雙向同步架構
數據層雙向同步架構的思路:
-
Proxy 不關心底層 Redis 數據同步
-
業務流量只訪問本機房裏的 Redis 集羣
-
在 RedisServer 層面實現數據同步
數據層雙向同步架構的優點:
-
機房 - A 故障時,機房 - B 不受影響,反向如是
-
網絡故障時,本機房流量不受影響,網絡恢復後,數據層面可以拉取增量數據繼續同步,數據不丟
-
支持存量數據的同步
-
業務訪問 Redis 延時低,訪問鏈路不受機房間網絡延時影響
-
業務單元化部署時,雙機房 Redis 會有較高的數據一致性
數據層雙向同步架構的缺點:
- 實現相對比較複雜,RedisServer 改動比較大
滴滴 Redis 架構
Codis 架構(早期架構,現已廢棄)
Kedis 架構(線上架構)
滴滴 Redis 異地多活架構的演進
第一代多活架構
第一代 Redis 多活基於 Codis 架構在 proxy 層實現了雙寫,即本機房的 Proxy 將寫流量轉發到對端機房的 Proxy,這個方案的特點是快速實現,儘快滿足了業務多機房同步的需求。如前面 Proxy 雙向架構思路所講,本方案還存在着諸多缺點,最主要的是網絡故障時,同步數據丟失的問題,爲了解決這些問題,我們開發了第二代多活架構。
第二代多活架構
第二代多活基於 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 的依賴,降低多活成本。
第三代架構中,我們去掉了 MQ 和 consumer,新增了 syncer 組件。syncer 組件模擬 Redis-slave 從 Redis-master 中拉取增量數據,這樣把數據同步和 Redis 進行解耦,便於後續多機房擴展。
在第三代架構中,Redis 遇到了迴環、重試、數據衝突、增量數據存儲和讀取等問題,接下來一一介紹我們應對這些問題的解決方案。
1、迴環問題
機房 - A 寫入的數據同步到機房 - B,防止數據再傳回機房 - A。
爲了解決迴環問題,我們開發了防迴環機制:
-
Redis 增加 shardID 配置,標識唯一分片號
-
Redis 請求中增加 opinfo,記錄元信息,包含 shardID
-
機房 - 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 增加 opid,標識唯一請求號
-
Redis 請求中增加 opinfo,記錄元信息 [opid]
-
機房 - 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 記錄 key 的最後 write 時間
-
Redis 請求中增加 opinfo,記錄元信息 [timestamp]
-
如果 opinfo.timestamp<=key_write_time,則記錄衝突 key
時間 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 的 aof 機制進行了改造,可以在網絡故障時,增量數據都堆積在 Redis 的磁盤上,在網絡恢復後,syncer 從 Redis 里拉取增量 aof 數據發送到對端機房,避免數據丟失。
aof 機制改造有:aof 文件切分、aof 增量複製、aof 異步寫盤
-
將 aof 文件切分爲多個小文件,保存增量數據
-
當增量數據超過配置的閾值時,Redis 自動刪除最舊的 aof 文件
-
當 Redis 重啓時,加載 rdb 文件和 rdb 之後的 aof 文件,可以恢復全部數據
-
當網絡故障恢復後,syncer 根據故障前的 opid 向 Redis 請求拉取增量數據,發送到對端機房
開源 Redis 是在主線程中進行 aof 寫盤,當磁盤 IO 過高時,Redis 寫盤可能造成業務訪問 Redis 耗時抖動。因此我們開發了 aof 異步寫盤機制:
-
Redis 的主線程將 aof 數據寫入 queue 中
-
bio 線程來消費 queue
-
bio 線程將 aof 數據寫入磁盤
這樣 Redis 的訪問耗時不受磁盤 IO 的影響,更好的保證穩定性。
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/UcyO3J0XEEe1sH3frmmNDg