Redis vs Tendis:冷热混合存储版架构揭秘
作者:jingjunli,騰訊 IEG 后臺開發工程師
Redis 作為高性能緩存被廣泛應用到各個業務, 比如游戲的排行榜, 分布式鎖等場景。經過在 IEG 的長期運營, 我們也遇到 Redis 一些痛點問題, 比如內存占用高, 數據可靠性差, 業務維護緩存和存儲的一致性繁瑣。由 騰訊互娛 CROS DBA 團隊 & 騰訊云數據庫團隊聯合研發的 Tendis 推出了: 緩存版 、 混合存儲版 和 存儲版 三種不同產品形態, 針對不同的業務需求, 本文主要介紹 混合存儲版 的整體架構, 并且詳細揭秘內部的原理。
導語
本文首先介紹騰訊 IEG 運營 Redis 遇到的一些痛點問題, 然后介紹由 騰訊互娛 CROS DBA 團隊 & 騰訊云數據庫團隊聯合研發的 Tendis 的三種不同的產品形態。最后重點介紹冷熱混合存儲版的架構, 并且重點介紹各個組件的功能特性。
背景介紹
Redis 有哪些痛點 ?
在使用的過程中, 主要遇到以下一些痛點問題:
內存成本高
業務不同階段對 QPS 要求不同 比如游戲業務, 剛上線的新游戲特別火爆, 為了支持上千萬同時在線, 需要不斷的進行擴容增加機器。運營一段時間后, 游戲玩家可能變少, 訪問頻率(QPS)沒那么高, 依然占用大量機器, 維護成本很高。
需要為 Fork 預留內存 Redis 保存全量數據時, 需要 Fork 一個進程。Linux 的 fork 系統調用基于 Copy On Write 機制, 如果在此期間 Redis 有大量的寫操作, 父子進程就需要各自維護一份內存。因此部署 Redis 的機器往往需要預留一半的內存。
緩存一致性的問題 對于 Redis + MySQL 的架構需要業務方花費大量的精力來維護緩存和數據庫的一致性。
數據可靠性 Redis 本質上是一個內存數據庫, 用戶雖然可以使用 AOF 的 Always 來落盤保證數據可靠性, 但是會帶來性能的大幅下降, 因此生產環境很少有使用。另外 不支持 回檔, Master 故障后, 異步復制會造成數據的丟失。
異步復制 Redis 主備使用異步復制, 這個是異步復制固有的問題。主備使用異步復制, 響應延遲低, 性能高, 但是 Master 故障后, 會造成數據丟失。
Tendis 是什么 ?
Tendis 是集騰訊眾多海量 KV 存儲優勢于一身的 Redis 存儲解決方案, 并 100% 兼容 Redis 協議和 Redis4.0 所有數據模型。作為一個高可用、高性能的分布式 KV 存儲數據庫, 從訪問時延、持久化需求、整體成本等不同維度的考量, Tendis 推出了 緩存版 、 混合存儲版 和 存儲版 三種不同產品形態,并將存儲版開源。感興趣的小伙伴 可以去 Github 關注我們的項目: Tencent/Tendis
Tendis 緩存版適用于對延遲要求特別敏感, 并且對 QPS 要求很高的業務。基于社區 Redis 4.0 版本進行定制開發。
Tendis 存儲版適用于大容量, 延遲不敏感型業務, 數據全部存儲在 磁盤, 適合溫冷數據的存儲。Tendis 存儲版是騰訊互娛 CROS DBA 團隊 & 騰訊云數據庫團隊 自主設計和研發的開源分布式高性能 KV 存儲系統。另外在 可靠性、復制機制、并發控制、gossip 實現以及數據搬遷等做了大量的優化, 并且解決了一些 Redis cluster 比較棘手的問題。完全兼容 Redis 協議, 并使用 RocksDB 作為底層存儲引擎。
Tendis 冷熱混合存儲版冷熱混合存儲 綜合了緩存版和存儲版的優點, 緩存層存放熱數據, 全量數據存放在存儲層。這既保證了熱數據的訪問性能,同時保證了全量數據的可靠性,同時熱數據支持自動降冷。
Tendis 冷熱混合存儲版 整體架構
Tendis 冷熱混合存儲版主要由 Proxy 、緩存層 Redis、 存儲層 Tendis 存儲版 和 同步層 Redis-sync 組成, 其中每個組件的功能如下:
Proxy 組件: 負責對客戶端請求進行路由分發,將不同的 Key 的命令分發到正確的分片,同時 Proxy 還負責了部分監控數據的采集,以及高危命令在線禁用等功能。
緩存層 Redis Cluster: 緩存層 Redis 基于 社區 Redis 4.0 進行開發。Redis 具有以下功能: 1) 版本控制 2) 自動將 冷數據從緩存層中淘汰, 將熱數據從存儲層加載到緩存層; 3) 使用 Cuckoo Filter 表示全量 Keys, 防止緩存穿透; 4) 基于 RDB+AOF 擴縮容方式, 擴縮容更加高效便捷。
存儲層 Tendis Cluster: Tendis 存儲版 是騰訊基于 RocksDB 自研的 兼容 Redis 協議的 KV 存儲引擎, 該引擎已經在騰訊集團內部運營多年, 性能和穩定性得到了充分的驗證。在混合存儲系統中主要負責全量數據的存儲和讀取, 以及數據備份, 增量日志備份等功能。
同步層 Redis-sync: 1) 并行數據導入 存儲層 Tendis; 2) 服務無狀態, 故障重新拉起; 3) 數據自動路由。
Tendis 冷熱混合存儲的一些重要特性介紹:
緩存層 Redis Cluster 和 存儲層 Tendis Cluster 分別進行擴縮容, 集群自治管理等。
冷數據自動降冷, 降低內存成本; 熱數據自動緩存, 降低訪問延遲
緩存層 Redis Cluster
冷熱混合存儲緩存層 Redis 在社區版的基礎上增加了以下功能:
版本控制
冷熱數據交互
Cuckoo Filter 避免緩存穿透
智能淘汰算法
基于 RDB+AOF 擴縮容
下面分別對這幾個特性進行詳細的講解。
版本控制
首先基于社區版 Redis 改動是版本控制。我們為每個 Key 和 每條 Aof 增加一個 Version , 并且 Version 是單調遞增的。在每次更新/新增一個 Key 后, 將當前節點的 Version 賦值給 Key 和 Value, 然后對全局的 Version++; 如下所示, 在 redisObject 中添加 64bits, 其中 48bits 用于版本控制。
typedef?struct?redisObject?{unsigned?type:4;unsigned?encoding:4;unsigned?lru:LRU_BITS;?/*?LRU?time?(relative?to?global?lru_clock)?or*?LFU?data?(least?significant?8?bits?frequency*?and?most?significant?16?bits?access?time).?*/int?refcount;/*?for?hybrid?storage?*/unsigned?flag:4;???????????????????????????/*?OBJ_FLAG_...?*/unsigned?reserved:4;unsigned?counter:8;????????????????????????/*?for?cold-data-cache-policy?*/unsigned?long?long?revision:REVISION_BITS;?/*?for?value?version?*/void?*ptr; }?robj;引入版本控制主要帶來以下優勢:
增量 RDB
Aof 的冪等
首先判斷 Key 是否在緩存層, 如果緩存層存在, 則執行命令; 如果緩存層不存在, 查詢 Cuckoo Filter, 判斷 Key 是否有可能在存儲層;
如果 Key 可能在存儲層, 則向存儲層發送 dumpx dbid key withttl 命令嘗試從存儲層獲取數據, 并且阻塞當前請求的客戶端;
存儲層收到 dumpx , 如果 Key 在存儲層, 則向緩存層返回 RESTOREEX dbid key ttl value; 如果 Key 不在存儲層(Cuckoo Filter 的誤判), 則向緩存層返回 DUMPXERROR key;
存儲層收到 RESTOREEX 或者 DUMPXERROR 后, 將冷數據恢復。然后就可以喚醒阻塞的客戶端, 執行客戶端的請求。
Key 降冷的背景介紹2020 年 6 月份上線的 1:1 版的冷熱混合存儲, 緩存層 Redis 存儲全量的 Keys 和熱 Values(All Keys + Hot values), 存儲層 Tendis 存儲全量的 Keys 和 Values(All Keys + All values)。在上線運行了一段時間后, 發現全量 Keys 的內存開銷特別大, 冷熱混合的收益并不明顯。為了進一步釋放內存空間, 提高緩存的效率, 我們放棄了 Redis 緩存全量 Keys 的方案, 驅逐的時候將 key 和 Value 都從緩存層淘汰。
Cuckoo Filter 解決緩存擊穿和緩存穿透如果緩存層不存儲全量的 Keys, 就會出現緩存擊穿和緩存穿透的問題。為了解決這一問題, 緩存層引入 Cuckoo Filter 表示全量的 keys 。我們需要一個支持刪除、可動態伸縮并且空間利用率高的 Membership Query 結構, 經過我們的調研和對比分析, 最終選擇 Dynamic Cuckoo Filter。
Dynamic Cuckoo Filter 實現項目初期參考了 RedisBloom 中 Cuckoo Filter 的實現, 在開發的過程中也遇到了一些坑, RedisBloom 實現的 Cuckoo Filter 在刪除的時候會出現誤刪, 最終給 RedisBloom 提 PR(Fix Cuckoo filter compact cause deleted by mistake #260) 修復了問題。
Key 降冷的收益最終采用將 Key 和 Value 同時從緩存層淘汰, 降低內存的收益很大。比如現網的一個業務, 總共有 6620 W 個 Keys , 在緩存全量 Keys 的時候 占用 18408 MB 的內存, 在 Key 降冷后 僅僅占用 593MB 。
首先介紹混合存儲的淘汰策略, 主要有以下兩個淘汰策略:
maxmemory-policy 當緩存層 Redis 內存使用到達 maxmemory, 系統將按照 maxmemory-policy 的內存策略將 Key/Value 從緩存層驅逐, 釋放內存空間。(驅逐是指將 Key/Value 從緩存層中淘汰掉, 存儲層 和 緩存層的 Cuckoo Filter 依然存在該 Key; )
value-eviction-policy 如果配置 value-eviction-policy, 后臺會定期將用戶 N 天未訪問的 Key/Value 被驅逐出內存;
緩存加載策略 為了避免緩存污染的問題(比如類似 Scan 的訪問, 遍歷存儲層的數據, 將緩存層真正的熱數據淘汰, 從而造成了緩存效率低下) 。我們實現緩存加載策略: 僅僅將規定時間內訪問頻率超過某個閾值的數據加載到緩存中, 這里的時間和閾值都是可配置的。
importing 和 migrating 的設置不是原子的
搬遷以 Key 為粒度, 效率較低
大 Key 問題
管控添加新節點, 規劃待搬遷 slots;
管控端向目標節點下發 slot 同步命令: cluster slotsync beginSlot endSlot [[beginSlot endSlot]...]
目標節點向源節點發送 sync [slot ...], 命令請求同步 slot 數據
源節點生成指定 slot 數據的一致性快照全量數據(RDB), 并將其發送給目標節點
源節點開始持續發送增量數據(Aof)
管控端定位獲取源節點和目標節點的落后值 (diff_bytes), 如果落后值在指定的閾值內, 管控端向目標節點發送 cluster slotfailover (流程類似 Redis 的 cluster failover, 首先阻塞源節點寫入, 然后等待目標節點和源節點的落后值為 0, 最后將 搬遷的 slots 歸屬目標節點)
并發地導入到存儲層 Tendis, 如何保證時序正確 ?
特殊命令的處理, 比如 FLUSHALL/FLUSHDB/SWAPDB/SELECT/MULTI 等 ?
作為一個無狀態的同步組件, 如何保證故障后, 數據斷點續傳 ?
緩存層和存儲層 分別進行擴縮容, 如何將請求路由到正確的 Tendis 節點 ?
Slot 內串行, Slot 間并行 針對問題 1, Redis-sync 中采用與 Redis 相同的計算 Slot 的算法, 解析到具體的命令后, 根據 Key 所屬的 slot, 將其放到對應的 隊列中( slot%QueueSize )。因此同一個 Slot 的數據是串行寫入, 不同 slot 的數據可以并行寫入, 不會出現時序錯亂的行為。
串并轉換 針對問題 2, Redis-sync 會在并行和串行模式之間進行轉換。比如收到 FLUSHDB 命令, 這是需要將 FLUSHDB 命令 前的命令都執行完, 再執行 FLUSHDB 命令。
定期上報 針對問題 3, Redis-sync 會定期將已發送給存儲層的 aof 的 Version 持久化到 存儲層。如何 Redis-sync 故障, 首先從 存儲層獲取上次已發送的位置, 然后向對應的 Redis 節點發送 psync, 請求同步。
數據自動路由 針對問題 4, Redis-sync 會定期從存儲層獲取 Slot 到 Tendis 節點的映射關系, 并且維護這些 Tendis 節點的連接池。請求從 緩存層到達, 然后計算請求所屬的 slot, 然后發送到正確的 Tendis 節點。
兼容 Redis 協議 完全兼容 redis 協議,支持 redis 主要數據結構和接口,兼容大部分原生 Redis 命令。
持久化存儲 使用 rocksdb 作為存儲引擎,所有數據以特定格式存儲在 rocksdb 中,最大支持 PB 級存儲。
去中心化架構 類似于 redis cluster 的分布式實現,所有節點通過 gossip 協議通訊,可指定 hashtag 來控制數據分布和訪問,使用和運維成本極低。
水平擴展 集群支持增刪節點,并且數據可以按照 slot 在任意兩節點之間遷移,擴容和縮容過程中對應用運維人員透明,支持擴展至 1000 個節點。
故障自動切換 自動檢測故障節點,當故障發生后,slave 會自動提升為 master 繼續對外提供服務。
社區版 Redis 主備在斷線重連后, 如果 slave 發送的 psync_offset 對應的數據不在當前的 Master 的 repl_backlog 中, 則主備需要重新進行全量同步。再引入 Version 之后, slave 斷線重連, 給 Master 發送 帶 Version 的 PSYNC replid psync_offset version命令。如果出現上述情況, Master 將大于等于 Version 的數據生成增量 RDB, 發給 Slave, 進而解決需要增量, 同步比較慢的問題。
如果同步層 Redis-sync 出現網絡瞬斷(短暫的和緩存層或者存儲層斷開), 作為一個無狀態的同步組件, Redis-sync 會重新拉取未同步到 Tendis 的增量數據, 重新發送給 Tendis。每條 Aof 都具有一個 Version, Tendis 在執行的時候僅會執行比當前 Version 大的 Aof, 避免 aof 執行多次導致的數據不一致。
冷熱數據交互
冷數據的恢復指當用戶訪問的 Key 不在緩存層, 需要將數據從存儲層重新加載到緩存層。數據恢復這里是緩存層直接和存儲層直接交互, 當冷 Keys 訪問的請求比較大, 數據恢復很容易成為瓶頸, 因此為每個 Tendis 節點建立一個連接池, 專門負責與這個 Tendis 節點進行冷熱數據恢復。
用戶訪問一個 Key 的具體流程如下:
Key 降冷 與 Cuckoo Filter
這里主要講解混合存儲從 1:1 版的緩存層緩存全量 Keys, 到 N:M 版的緩存層將 Key 和 Value 同時驅逐的演進, 以及我們引入 Cuckoo Filter 避免緩存穿透, 同時節省大量內存。
智能淘汰/加載策略
作為冷熱混合存儲系統, 熱數據在緩存層, 全量數據在存儲層。關鍵的問題是淘汰和加載策略, 這里直接影響緩存的效率, 細分主要有兩點: 1) 當緩存層內存滿時, 選擇哪些數據淘汰; 2) 當用戶訪問存儲層的數據時, 是否需要將其放入緩存層。
基于 RDB+AOF 擴縮容
社區版 Redis 的擴容流程:
社區版 Redis 擴容存在的一些問題:
先設置目標節點 slot 為 importing 狀態, 再設置源節點的 slot 為 migrating 狀態。如果反過來, 由于兩次操作非原子: 源節點設置為 migrating , 目標節點還未設置 migrating 狀態, 請求在這兩個節點間反復 Move 。
Migrate 命令每次搬遷一個或者多個 Keys, 將整個 Slot 搬遷到目標節點需要多次網絡交互。
由于 Migrate 命令是同步命令, 在搬遷過程中是不能處理其他用戶請求的, 因此可能會影響業務。(延遲時間波動較大)
由于社區版 Redis 存在的上述問題, 我們實現了基于 RDB+Aof 的擴縮容方式, 大致流程如下:
同步層 Redis-sync
同步層 Redis-sync 模擬 Redis Slave 的行為, 接收 RDB 和 Aof, 然后并行地導入到存儲層 Tendis。同步層主要需要解決以下問題:
為了解決上述的三個問題, 我們實現了下面的功能:
存儲層 Tendis Cluster
Tendis 是兼容 Redis 核心數據結構與協議的分布式高性能 KV 數據庫, 主要具有以下特性:
想要更深入了解 Tendis 的特性和使用可以去 Github 關注我們的項目:
https://github.com/Tencent/Tendis
我們的官方文檔: http://tendis.cn/#/?
總結
以上是生活随笔為你收集整理的Redis vs Tendis:冷热混合存储版架构揭秘的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Facebook、谷歌、微软和亚马逊的网
- 下一篇: 深入解读无服务器架构下的数据库