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