騰訊的 Tendis 能否干掉 Redis 呢?

來源:jingjunli,騰訊 IEG 后臺開發(fā)工程師
Redis 作為高性能緩存被廣泛應(yīng)用到各個業(yè)務(wù), 比如游戲的排行榜, 分布式鎖等場景。
經(jīng)過在 IEG 的長期運營, 我們也遇到 Redis 一些痛點問題, 比如內(nèi)存占用高, 數(shù)據(jù)可靠性差, 業(yè)務(wù)維護緩存和存儲的一致性繁瑣。
由 騰訊互娛 CROS DBA 團隊 & 騰訊云數(shù)據(jù)庫團隊聯(lián)合研發(fā)的 Tendis 推出了: 緩存版 、 混合存儲版 和 存儲版 三種不同產(chǎn)品形態(tài), 針對不同的業(yè)務(wù)需求, 本文主要介紹 混合存儲版 的整體架構(gòu), 并且詳細揭秘內(nèi)部的原理。
導語
本文首先介紹騰訊 IEG 運營 Redis 遇到的一些痛點問題, 然后介紹由 騰訊互娛 CROS DBA 團隊 & 騰訊云數(shù)據(jù)庫團隊聯(lián)合研發(fā)的 Tendis 的三種不同的產(chǎn)品形態(tài)。最后重點介紹冷熱混合存儲版的架構(gòu), 并且重點介紹各個組件的功能特性。
背景介紹
Redis 有哪些痛點 ?
在使用的過程中, 主要遇到以下一些痛點問題:
內(nèi)存成本高
業(yè)務(wù)不同階段對 QPS 要求不同 比如游戲業(yè)務(wù), 剛上線的新游戲特別火爆, 為了支持上千萬同時在線, 需要不斷的進行擴容增加機器。運營一段時間后, 游戲玩家可能變少, 訪問頻率(QPS)沒那么高, 依然占用大量機器, 維護成本很高。 需要為 Fork 預(yù)留內(nèi)存 Redis 保存全量數(shù)據(jù)時, 需要 Fork 一個進程。Linux 的 fork 系統(tǒng)調(diào)用基于 Copy On Write 機制, 如果在此期間 Redis 有大量的寫操作, 父子進程就需要各自維護一份內(nèi)存。因此部署 Redis 的機器往往需要預(yù)留一半的內(nèi)存。 緩存一致性的問題 對于 Redis + MySQL 的架構(gòu)需要業(yè)務(wù)方花費大量的精力來維護緩存和數(shù)據(jù)庫的一致性。
數(shù)據(jù)可靠性 Redis 本質(zhì)上是一個內(nèi)存數(shù)據(jù)庫, 用戶雖然可以使用 AOF 的 Always 來落盤保證數(shù)據(jù)可靠性, 但是會帶來性能的大幅下降, 因此生產(chǎn)環(huán)境很少有使用。另外 不支持 回檔, Master 故障后, 異步復(fù)制會造成數(shù)據(jù)的丟失。
異步復(fù)制 Redis 主備使用異步復(fù)制, 這個是異步復(fù)制固有的問題。主備使用異步復(fù)制, 響應(yīng)延遲低, 性能高, 但是 Master 故障后, 會造成數(shù)據(jù)丟失。
Tendis 是什么 ?
Tendis 是集騰訊眾多海量 KV 存儲優(yōu)勢于一身的 Redis 存儲解決方案, 并 100% 兼容 Redis 協(xié)議和 Redis4.0 所有數(shù)據(jù)模型。作為一個高可用、高性能的分布式 KV 存儲數(shù)據(jù)庫, 從訪問時延、持久化需求、整體成本等不同維度的考量, Tendis 推出了 緩存版 、 混合存儲版和 存儲版 三種不同產(chǎn)品形態(tài),并將存儲版開源。感興趣的小伙伴 可以去 Github 關(guān)注我們的項目: Tencent/Tendis

Tendis 緩存版 適用于對延遲要求特別敏感, 并且對 QPS 要求很高的業(yè)務(wù)?;谏鐓^(qū) Redis 4.0 版本進行定制開發(fā)。
Tendis 存儲版 適用于大容量, 延遲不敏感型業(yè)務(wù), 數(shù)據(jù)全部存儲在 磁盤, 適合溫冷數(shù)據(jù)的存儲。Tendis 存儲版是騰訊互娛 CROS DBA 團隊 & 騰訊云數(shù)據(jù)庫團隊 自主設(shè)計和研發(fā)的開源分布式高性能 KV 存儲系統(tǒng)。另外在 可靠性、復(fù)制機制、并發(fā)控制、gossip 實現(xiàn)以及數(shù)據(jù)搬遷等做了大量的優(yōu)化, 并且解決了一些 Redis cluster 比較棘手的問題。完全兼容 Redis 協(xié)議, 并使用 RocksDB 作為底層存儲引擎。
Tendis 冷熱混合存儲版 冷熱混合存儲 綜合了緩存版和存儲版的優(yōu)點, 緩存層存放熱數(shù)據(jù), 全量數(shù)據(jù)存放在存儲層。這既保證了熱數(shù)據(jù)的訪問性能,同時保證了全量數(shù)據(jù)的可靠性,同時熱數(shù)據(jù)支持自動降冷。
Tendis 冷熱混合存儲版 整體架構(gòu)
Tendis 冷熱混合存儲版主要由 Proxy 、緩存層 Redis 、 存儲層 Tendis 存儲版 和 同步層 Redis-sync 組成, 其中每個組件的功能如下:
Proxy 組件 : 負責對客戶端請求進行路由分發(fā),將不同的 Key 的命令分發(fā)到正確的分片,同時 Proxy 還負責了部分監(jiān)控數(shù)據(jù)的采集,以及高危命令在線禁用等功能。
緩存層 Redis Cluster : 緩存層 Redis 基于 社區(qū) Redis 4.0 進行開發(fā)。Redis 具有以下功能: 1) 版本控制 2) 自動將 冷數(shù)據(jù)從緩存層中淘汰, 將熱數(shù)據(jù)從存儲層加載到緩存層; 3) 使用 Cuckoo Filter 表示全量 Keys, 防止緩存穿透; 4) 基于 RDB+AOF 擴縮容方式, 擴縮容更加高效便捷。
存儲層 Tendis Cluster : Tendis 存儲版 是騰訊基于 RocksDB 自研的 兼容 Redis 協(xié)議的 KV 存儲引擎, 該引擎已經(jīng)在騰訊集團內(nèi)部運營多年, 性能和穩(wěn)定性得到了充分的驗證。在混合存儲系統(tǒng)中主要負責全量數(shù)據(jù)的存儲和讀取, 以及數(shù)據(jù)備份, 增量日志備份等功能。
同步層 Redis-sync : 1) 并行數(shù)據(jù)導入 存儲層 Tendis; 2) 服務(wù)無狀態(tài), 故障重新拉起; 3) 數(shù)據(jù)自動路由。

Tendis 冷熱混合存儲的一些重要特性介紹:
緩存層 Redis Cluster和存儲層 Tendis Cluster分別進行擴縮容, 集群自治管理等。冷數(shù)據(jù)自動降冷, 降低內(nèi)存成本; 熱數(shù)據(jù)自動緩存, 降低訪問延遲
緩存層 Redis Cluster
冷熱混合存儲緩存層 Redis 在社區(qū)版的基礎(chǔ)上增加了以下功能:
版本控制 冷熱數(shù)據(jù)交互 Cuckoo Filter 避免緩存穿透 智能淘汰算法 基于 RDB+AOF 擴縮容
下面分別對這幾個特性進行詳細的講解。
版本控制
首先基于社區(qū)版 Redis 改動是版本控制。我們?yōu)槊總€ Key 和 每條 Aof 增加一個 Version , 并且 Version 是單調(diào)遞增的。在每次更新/新增一個 Key 后, 將當前節(jié)點的 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
社區(qū)版 Redis 主備在斷線重連后, 如果 slave 發(fā)送的 psync_offset 對應(yīng)的數(shù)據(jù)不在當前的 Master 的 repl_backlog 中, 則主備需要重新進行全量同步。再引入 Version 之后, slave 斷線重連, 給 Master 發(fā)送 帶 Version 的
PSYNC replid psync_offset version命令。如果出現(xiàn)上述情況, Master 將大于等于 Version 的數(shù)據(jù)生成增量 RDB, 發(fā)給 Slave, 進而解決需要增量, 同步比較慢的問題。
Aof 的冪等
如果同步層 Redis-sync 出現(xiàn)網(wǎng)絡(luò)瞬斷(短暫的和緩存層或者存儲層斷開), 作為一個無狀態(tài)的同步組件, Redis-sync 會重新拉取未同步到 Tendis 的增量數(shù)據(jù), 重新發(fā)送給 Tendis。每條 Aof 都具有一個 Version, Tendis 在執(zhí)行的時候僅會執(zhí)行比當前 Version 大的 Aof, 避免 aof 執(zhí)行多次導致的數(shù)據(jù)不一致。
冷熱數(shù)據(jù)交互
冷數(shù)據(jù)的恢復(fù)指當用戶訪問的 Key 不在緩存層, 需要將數(shù)據(jù)從存儲層重新加載到緩存層。數(shù)據(jù)恢復(fù)這里是緩存層直接和存儲層直接交互, 當冷 Keys 訪問的請求比較大, 數(shù)據(jù)恢復(fù)很容易成為瓶頸, 因此為每個 Tendis 節(jié)點建立一個連接池, 專門負責與這個 Tendis 節(jié)點進行冷熱數(shù)據(jù)恢復(fù)。

用戶訪問一個 Key 的具體流程如下:
首先判斷 Key 是否在緩存層, 如果緩存層存在, 則執(zhí)行命令; 如果緩存層不存在, 查詢 Cuckoo Filter, 判斷 Key 是否有可能在存儲層; 如果 Key 可能在存儲層, 則向存儲層發(fā)送 dumpx dbid key withttl命令嘗試從存儲層獲取數(shù)據(jù), 并且阻塞當前請求的客戶端;存儲層收到 dumpx , 如果 Key 在存儲層, 則向緩存層返回 RESTOREEX dbid key ttl value; 如果 Key 不在存儲層(Cuckoo Filter 的誤判), 則向緩存層返回DUMPXERROR key;存儲層收到 RESTOREEX 或者 DUMPXERROR 后, 將冷數(shù)據(jù)恢復(fù)。然后就可以喚醒阻塞的客戶端, 執(zhí)行客戶端的請求。
Key 降冷 與 Cuckoo Filter
這里主要講解混合存儲從 1:1 版的緩存層緩存全量 Keys, 到 N:M 版的緩存層將 Key 和 Value 同時驅(qū)逐的演進, 以及我們引入 Cuckoo Filter 避免緩存穿透, 同時節(jié)省大量內(nèi)存。
Key 降冷的背景介紹 2020 年 6 月份上線的 1:1 版的冷熱混合存儲, 緩存層 Redis 存儲全量的 Keys 和熱 Values(All Keys + Hot values ), 存儲層 Tendis 存儲全量的 Keys 和 Values(All Keys + All values )。在上線運行了一段時間后, 發(fā)現(xiàn)全量 Keys 的內(nèi)存開銷特別大, 冷熱混合的收益并不明顯。為了進一步釋放內(nèi)存空間, 提高緩存的效率, 我們放棄了 Redis 緩存全量 Keys 的方案, 驅(qū)逐的時候?qū)?key 和 Value 都從緩存層淘汰。 Cuckoo Filter 解決緩存擊穿和緩存穿透 如果緩存層不存儲全量的 Keys, 就會出現(xiàn)緩存擊穿和緩存穿透的問題。為了解決這一問題, 緩存層引入 Cuckoo Filter 表示全量的 keys 。我們需要一個支持刪除、可動態(tài)伸縮并且空間利用率高的 Membership Query 結(jié)構(gòu), 經(jīng)過我們的調(diào)研和對比分析, 最終選擇 Dynamic Cuckoo Filter。Dynamic Cuckoo Filter 實現(xiàn) 項目初期參考了 RedisBloom 中 Cuckoo Filter 的實現(xiàn), 在開發(fā)的過程中也遇到了一些坑, RedisBloom 實現(xiàn)的 Cuckoo Filter 在刪除的時候會出現(xiàn)誤刪, 最終給 RedisBloom 提 PR(Fix Cuckoo filter compact cause deleted by mistake #260 ) 修復(fù)了問題。 Key 降冷的收益 最終采用將 Key 和 Value 同時從緩存層淘汰, 降低內(nèi)存的收益很大。比如現(xiàn)網(wǎng)的一個業(yè)務(wù), 總共有 6620 W 個 Keys , 在緩存全量 Keys 的時候 占用 18408 MB 的內(nèi)存, 在 Key 降冷后 僅僅占用 593MB 。
智能淘汰/加載策略
作為冷熱混合存儲系統(tǒng), 熱數(shù)據(jù)在緩存層, 全量數(shù)據(jù)在存儲層。關(guān)鍵的問題是淘汰和加載策略, 這里直接影響緩存的效率, 細分主要有兩點: 1) 當緩存層內(nèi)存滿時, 選擇哪些數(shù)據(jù)淘汰; 2) 當用戶訪問存儲層的數(shù)據(jù)時, 是否需要將其放入緩存層 。
首先介紹混合存儲的淘汰策略, 主要有以下兩個淘汰策略:
maxmemory-policy 當緩存層 Redis 內(nèi)存使用到達 maxmemory, 系統(tǒng)將按照 maxmemory-policy 的內(nèi)存策略將 Key/Value 從緩存層驅(qū)逐, 釋放內(nèi)存空間。(驅(qū)逐是指將 Key/Value 從緩存層中淘汰掉, 存儲層 和 緩存層的 Cuckoo Filter 依然存在該 Key; ) value-eviction-policy 如果配置 value-eviction-policy, 后臺會定期將用戶 N 天未訪問的 Key/Value 被驅(qū)逐出內(nèi)存;
緩存加載策略 為了避免緩存污染的問題(比如類似 Scan 的訪問, 遍歷存儲層的數(shù)據(jù), 將緩存層真正的熱數(shù)據(jù)淘汰, 從而造成了緩存效率低下) 。我們實現(xiàn)緩存加載策略: 僅僅將規(guī)定時間內(nèi)訪問頻率超過某個閾值的數(shù)據(jù)加載到緩存中, 這里的時間和閾值都是可配置的。
基于 RDB+AOF 擴縮容
社區(qū)版 Redis 的擴容流程:

社區(qū)版 Redis 擴容存在的一些問題:
importing 和 migrating 的設(shè)置不是原子的
先設(shè)置目標節(jié)點 slot 為 importing 狀態(tài), 再設(shè)置源節(jié)點的 slot 為 migrating 狀態(tài)。如果反過來, 由于兩次操作非原子: 源節(jié)點設(shè)置為 migrating , 目標節(jié)點還未設(shè)置 migrating 狀態(tài), 請求在這兩個節(jié)點間反復(fù) Move 。
搬遷以 Key 為粒度, 效率較低
Migrate 命令每次搬遷一個或者多個 Keys, 將整個 Slot 搬遷到目標節(jié)點需要多次網(wǎng)絡(luò)交互。
大 Key 問題
由于 Migrate 命令是同步命令, 在搬遷過程中是不能處理其他用戶請求的, 因此可能會影響業(yè)務(wù)。(延遲時間波動較大)
由于社區(qū)版 Redis 存在的上述問題, 我們實現(xiàn)了基于 RDB+Aof 的擴縮容方式, 大致流程如下:
管控添加新節(jié)點, 規(guī)劃待搬遷 slots; 管控端向目標節(jié)點下發(fā) slot 同步命令: cluster slotsync beginSlot endSlot [[beginSlot endSlot]...]目標節(jié)點向源節(jié)點發(fā)送 sync [slot ...], 命令請求同步 slot 數(shù)據(jù)源節(jié)點生成指定 slot 數(shù)據(jù)的一致性快照全量數(shù)據(jù)(RDB), 并將其發(fā)送給目標節(jié)點 源節(jié)點開始持續(xù)發(fā)送增量數(shù)據(jù)(Aof) 管控端定位獲取源節(jié)點和目標節(jié)點的落后值 (diff_bytes), 如果落后值在指定的閾值內(nèi), 管控端向目標節(jié)點發(fā)送 cluster slotfailover(流程類似 Redis 的 cluster failover, 首先阻塞源節(jié)點寫入, 然后等待目標節(jié)點和源節(jié)點的落后值為 0, 最后將 搬遷的 slots 歸屬目標節(jié)點)

同步層 Redis-sync
同步層 Redis-sync 模擬 Redis Slave 的行為, 接收 RDB 和 Aof, 然后并行地導入到存儲層 Tendis。同步層主要需要解決以下問題:
并發(fā)地導入到存儲層 Tendis, 如何保證時序正確 ? 特殊命令的處理, 比如 FLUSHALL/FLUSHDB/SWAPDB/SELECT/MULTI 等 ? 作為一個無狀態(tài)的同步組件, 如何保證故障后, 數(shù)據(jù)斷點續(xù)傳 ? 緩存層和存儲層 分別進行擴縮容, 如何將請求路由到正確的 Tendis 節(jié)點 ?

為了解決上述的三個問題, 我們實現(xiàn)了下面的功能:
Slot 內(nèi)串行, Slot 間并行 針對問題 1, Redis-sync 中采用與 Redis 相同的計算 Slot 的算法, 解析到具體的命令后, 根據(jù) Key 所屬的 slot, 將其放到對應(yīng)的 隊列中( slot%QueueSize )。因此同一個 Slot 的數(shù)據(jù)是串行寫入, 不同 slot 的數(shù)據(jù)可以并行寫入, 不會出現(xiàn)時序錯亂的行為。 串并轉(zhuǎn)換 針對問題 2, Redis-sync 會在并行和串行模式之間進行轉(zhuǎn)換。比如收到 FLUSHDB 命令, 這是需要將 FLUSHDB 命令 前的命令都執(zhí)行完, 再執(zhí)行 FLUSHDB 命令。 定期上報 針對問題 3, Redis-sync 會定期將已發(fā)送給存儲層的 aof 的 Version 持久化到 存儲層。如何 Redis-sync 故障, 首先從 存儲層獲取上次已發(fā)送的位置, 然后向?qū)?yīng)的 Redis 節(jié)點發(fā)送 psync, 請求同步。 數(shù)據(jù)自動路由 針對問題 4, Redis-sync 會定期從存儲層獲取 Slot到Tendis 節(jié)點的映射關(guān)系, 并且維護這些 Tendis 節(jié)點的連接池。請求從 緩存層到達, 然后計算請求所屬的 slot, 然后發(fā)送到正確的 Tendis 節(jié)點。
存儲層 Tendis Cluster
Tendis 是兼容 Redis 核心數(shù)據(jù)結(jié)構(gòu)與協(xié)議的分布式高性能 KV 數(shù)據(jù)庫, 主要具有以下特性:
兼容 Redis 協(xié)議 完全兼容 redis 協(xié)議,支持 redis 主要數(shù)據(jù)結(jié)構(gòu)和接口,兼容大部分原生 Redis 命令。 持久化存儲 使用 rocksdb 作為存儲引擎,所有數(shù)據(jù)以特定格式存儲在 rocksdb 中,最大支持 PB 級存儲。 去中心化架構(gòu) 類似于 redis cluster 的分布式實現(xiàn),所有節(jié)點通過 gossip 協(xié)議通訊,可指定 hashtag 來控制數(shù)據(jù)分布和訪問,使用和運維成本極低。 水平擴展 集群支持增刪節(jié)點,并且數(shù)據(jù)可以按照 slot 在任意兩節(jié)點之間遷移,擴容和縮容過程中對應(yīng)用運維人員透明,支持擴展至 1000 個節(jié)點。 故障自動切換 自動檢測故障節(jié)點,當故障發(fā)生后,slave 會自動提升為 master 繼續(xù)對外提供服務(wù)。

推薦大家關(guān)注三個公眾號 專注于Java后端編程 專注于前端技術(shù)編程
專注于Python人工智能編程
