Kvrocks 是基于 RocksDB 之上兼容 Redis 協(xié)議的 NoSQL 存儲(chǔ)服務(wù),設(shè)計(jì)目標(biāo)是提供一個(gè)低成本以及大容量的 Redis 服務(wù),作為 Redis 在大數(shù)據(jù)量場(chǎng)景的互補(bǔ)服務(wù),選擇兼容 Redis 協(xié)議是因?yàn)楹唵我子们覙I(yè)務(wù)遷移成本低。目前線上使用的公司包含: 美圖、攜程、百度以及白山云等,在線上經(jīng)過兩年多大規(guī)模實(shí)例的驗(yàn)證。
項(xiàng)目核心功能包含:
- 兼容 Redis 協(xié)議
- 支持主從復(fù)制
- 支持通過 Namespace 隔離不同業(yè)務(wù)的數(shù)據(jù)
- 高可用,支持 Redis Sentinel 自動(dòng)主從切換
- 集群模式 (進(jìn)行中,預(yù)計(jì)在 7-8 月份完成)
GitHub地址: https://github.com/kvrockslabs/kvrocks
實(shí)現(xiàn)方案對(duì)比
除了 Kvrocks 之外,社區(qū)也有一些類似的基于磁盤存儲(chǔ)兼容 Redis 協(xié)議的開源產(chǎn)品,從存儲(chǔ)設(shè)計(jì)來看可以分為幾類:
- 基于磁盤 KV 存儲(chǔ)引擎(比如 RocksDB/LevelDB) 實(shí)現(xiàn) Redis 協(xié)議
- 基于 Redis 存儲(chǔ)之上將冷數(shù)據(jù)交換到磁盤(類似早期 Redis VM 的方案)
- 基于分布式 KV(比如 TiKV) 實(shí)現(xiàn) Redis 協(xié)議代理,本地不做存儲(chǔ)

方案 1: 是基于磁盤 KV 之上兼容 Redis 協(xié)議,絕大多數(shù)的本地磁盤 KV 只提供最簡單的 Get/Set/Delete 方法,對(duì)于 Hash/Set/ZSet/List/Bitmap 等數(shù)據(jù)結(jié)構(gòu)需要基于磁盤 KV 之上去實(shí)現(xiàn)。優(yōu)點(diǎn)是可以規(guī)避下面方案 2 里提到的大 Key 問題,缺點(diǎn)是實(shí)現(xiàn)工作量大一些。
方案 2: 基于 Redis 把冷數(shù)據(jù)交換磁盤是以 Key 作為最小單元,在大 Key 的場(chǎng)景下是比較大的挑戰(zhàn)。大 Key 交換到磁盤會(huì)有嚴(yán)重讀寫放大,可能會(huì)導(dǎo)致整個(gè)服務(wù)不可用,所以這種實(shí)現(xiàn)只能限制 Value 大小,而優(yōu)點(diǎn)在于實(shí)現(xiàn)簡單且可按照 Key 維度來做冷熱數(shù)據(jù)分離。
方案 3: 是基于分布式 KV 之上實(shí)現(xiàn) Redis 協(xié)議,最大的區(qū)別在于所以的操作都是通過網(wǎng)絡(luò)。這種實(shí)現(xiàn)方式最大優(yōu)點(diǎn)是只需要實(shí)現(xiàn) Redis 協(xié)議的部分,服務(wù)本身是無狀態(tài)的,無須考慮數(shù)據(jù)復(fù)制以及擴(kuò)展性的問題。缺點(diǎn)也比較明顯,因?yàn)樗械拿疃际峭ㄟ^網(wǎng)絡(luò) IO,對(duì)于非 String 類型的讀寫一般都是需要多次網(wǎng)絡(luò) IO 且需要通過事務(wù)來保證原子,從而在延時(shí)和性能上都會(huì)比方案 1 和 2 差不少。
Kvrocks 設(shè)計(jì)的初衷是作為 Redis 場(chǎng)景的互補(bǔ),低成本、低延時(shí)和高吞吐是最重要的設(shè)計(jì)目標(biāo)。基于 Redis 實(shí)現(xiàn)冷熱數(shù)據(jù)交換的方式在大 Key 場(chǎng)景下可能導(dǎo)致不可用,從而需要限制單個(gè) Key 大小,這個(gè)對(duì)于我們想實(shí)現(xiàn)一個(gè)通用的 NoSQL 存儲(chǔ)服務(wù)是無法接受的。而對(duì)于方案 3 這種遠(yuǎn)程存儲(chǔ)的方案,延時(shí)和吞吐一定是無法滿足預(yù)期,所以我們最終選擇的方案 1 這種基于磁盤 KV 之上實(shí)現(xiàn) Redis 協(xié)議以及復(fù)制。除了數(shù)據(jù)存儲(chǔ)方式之外, Kvrocks 并沒有淘汰策略,所以一般是作為存儲(chǔ)服務(wù)而不是緩存,寫入達(dá)到實(shí)例最大容量或者磁盤容量不足時(shí)會(huì)失敗。
性能
需要注意的是以下提供的性能數(shù)據(jù)是基于特定的配置進(jìn)行壓測(cè),不同配置會(huì)有比較大的差異。壓測(cè)的硬件以及 Kvrocks 配置說明可參考: https://github.com/kvrockslabs/kvrocks#performance

這里提供性能數(shù)據(jù)只是為了給讀者更加直觀了解 Kvrocks 的性能情況,大部分命令由于可多線程并行執(zhí)行,從 QPS 的維度來看會(huì)比 Redis 更好一些,但延時(shí)肯定會(huì)比 Redis 略差。
功能
Kvrocks 支持 Redis String、 List、 Hash、Set、 ZSet 五種基本數(shù)據(jù)類型, 以及 Bitmap、Geo 和自定義的 Sorted Int 類型。支持大多數(shù)命令,也支持 Pub/Sub、事務(wù)以及備份等功能。
具體可參考: https://github.com/kvrockslabs/kvrocks/blob/unstable/docs/support-commands.md
快速體驗(yàn)
可以使用 Docker 的方式來啟動(dòng) Kvrocks:
docker run -it -p 6666:6666 kvrocks/kvrocks
接著可以跟使用 Redis 一樣使用:
? ~ redis-cli -p 6666
127.0.0.1:6666> set foo bar
OK
127.0.0.1:6666> get foo
"bar"
整體設(shè)計(jì)

Kvrocks 主要有兩類線程:
- Worker 線程,主要負(fù)責(zé)收發(fā)請(qǐng)求,解析 Redis 協(xié)議以及請(qǐng)求轉(zhuǎn)為 RocksDB 的讀寫
- 后臺(tái)線程,目前包含以下幾種后臺(tái)線程:
- Cron 線程,負(fù)責(zé)定期任務(wù),比如自動(dòng)根據(jù)寫入 KV 大小調(diào)整 Block Size、清理 Backup 等
- Compaction Checker 線程,如果開啟了增量 Compaction 檢查機(jī)制,那么會(huì)定時(shí)檢查需要 Compaction 的 SST 文件
- Task Runner 線程,負(fù)責(zé)異步的任務(wù)執(zhí)行,比如后臺(tái)全量 Compaction,Key/Value 數(shù)量掃描
- 主從復(fù)制線程,每個(gè) slave 都會(huì)對(duì)應(yīng)一個(gè)線程用來做增量同步
下面以 Hash 為例來說明 Kvrocks 是如何將復(fù)雜的數(shù)據(jù)結(jié)構(gòu)轉(zhuǎn)為 RocksDB 對(duì)應(yīng)的 KV?
最簡單的方式是將 Hash 所有的字段進(jìn)行序列化之后寫到同一個(gè) Key 里面,每次修改都需要將整個(gè) Value 讀出來之后修改再寫入,當(dāng) Value 比較大時(shí)會(huì)導(dǎo)致嚴(yán)重的讀寫放大問題。所以我們參考 blackwidow 的實(shí)現(xiàn),把 Hash 拆分成 Metadata 和 Subkey 兩個(gè)部分,Hash 里面的每個(gè)字段都是獨(dú)立的 KV,再使用 Metadata 來找到這些 Subkey:
+----------+------------+-----------+-----------+
key => | flags | expire | version | size |
| (1byte) | (4byte) | (8byte) | (8byte) |
+----------+------------+-----------+-----------+
(hash metadata)
+---------------+
key|version|field => | value |
+---------------+
(hash subkey)
里面的 flags 目前是來標(biāo)識(shí)當(dāng)前 Value 的類型,比如是 Hash/Set/List 等。expire 是 Key 的過期時(shí)間,size 是這個(gè) Key 包含的字段數(shù)量,這兩個(gè)比較好理解。version 是在 Key 創(chuàng)建時(shí)自動(dòng)生成的單調(diào)遞增的 id,每個(gè) Subkey 前綴會(huì)關(guān)聯(lián)上 version。當(dāng) Metadata 本刪除時(shí),這個(gè) version 就無法被找到,也意味著關(guān)聯(lián)這個(gè) version 的全部 Subkey 也無法找到,從而實(shí)現(xiàn)快速刪除,而這些無法找到的 Subkey 會(huì)在后臺(tái) Compact 的時(shí)候進(jìn)行回收。
以 HSET 命令為例,偽代碼如下:
HSET key, field, value:
// 先根據(jù) hash key 找到對(duì)應(yīng)的 metadata 并判斷是否過期
// 如果不存在或者過期則創(chuàng)建一個(gè)新的 metadata
metadata = rocksdb.Get(key)
if metadata == nil || metadata.Expired() {
metadata = createNewMetadata();
}
// 根據(jù) metadata 里面的版本組成 subkey
subkey = key + metadata.version+field
if rocksdb.Get(subkey) == nil {
metadata.size += 1
}
// 寫入 subkey 以及更新 metadata
rocksdb.Set(subkey, value)
rocksdb.Set(key, metadata)
更多的數(shù)據(jù)結(jié)構(gòu)設(shè)計(jì)可以參考: https://github.com/kvrockslabs/kvrocks/blob/unstable/docs/metadata-design.md
Kvrocks 是如何進(jìn)行數(shù)據(jù)復(fù)制?
Kvocks 的定位是作為大數(shù)據(jù)量場(chǎng)景下 Redis 的替代方案,提供與 Redis 一樣的數(shù)據(jù)最終一致性保證,采用了類似 Redis 的主從異步復(fù)制模型。考慮到需要應(yīng)對(duì)更多的業(yè)務(wù)場(chǎng)景,后續(xù)會(huì)支持半同步復(fù)制模型。在實(shí)現(xiàn)上,全量復(fù)制利用 RocksDB 的 CheckPoint 特性,增量復(fù)制采用直接發(fā)送 WAL 的方式,從庫接收到WAL直接操作后端引擎,相比于 Binlog 復(fù)制方式(回放從客戶端接收到的命令),省去了命令解析和處理的開銷,復(fù)制速度大幅提升,這樣也就解決了其它采用 Binlog 復(fù)制方式的存儲(chǔ)服務(wù)所存在的復(fù)制延遲問題。
Kvrocks 是如何實(shí)現(xiàn)分布式集群?
業(yè)界常用Redis集群方案主要有兩類:類似 Codis 中心化的集群架構(gòu)和社區(qū) Redis Cluster 去中心化的集群架構(gòu)。Kvrocks 集群方案選擇了類似 Codis 中心化的架構(gòu),集群元數(shù)據(jù)存儲(chǔ)在配置中心,但不依賴代理層,配置中心為存儲(chǔ)節(jié)點(diǎn)推送元數(shù)據(jù),對(duì)外提供 Redis Cluster 集群協(xié)議支持,對(duì)于使用 Redis Cluster SDK 或者 Proxy 的用戶不需要做任何修改。同時(shí)也可以避免類似Redis Cluster 受限于 Gossip 通信的開銷而導(dǎo)致集群規(guī)模不能太大的問題。另外,單機(jī)版的 Kvrocks 和 Redis 一樣可以直接支持 Twmeproxy,通過Sentinel實(shí)現(xiàn)高可用,對(duì)于 Codis 通過簡單的適配也能夠比較快的支持。目前集群方案處在測(cè)試階段,預(yù)計(jì)7月份發(fā)布,待正式發(fā)布后會(huì)給大家詳細(xì)介紹,這里不過多展開。
對(duì)于分布式集群來說,彈性伸縮的能力是必不可少的,Kvrocks 是如何實(shí)現(xiàn)彈性伸縮的?
整個(gè)擴(kuò)縮容拆分為遷移全量數(shù)據(jù)、遷移增量數(shù)據(jù)、變更拓?fù)淙齻€(gè)階段。遷移全量數(shù)據(jù)利用 RocksDB的 Snapshot 特性,生成 Snapshot 迭代數(shù)據(jù)發(fā)送到目標(biāo)節(jié)點(diǎn)。同時(shí),為了加快迭代效率數(shù)據(jù)編碼上Key 增加 SlotID 前綴。遷移增量數(shù)據(jù)階段直接發(fā)送 WAL。當(dāng)待遷移的增量 WAL 小于設(shè)定的閾值則開始阻寫,等發(fā)送完剩余的 WAL 切換拓?fù)渲蠼獬鑼?,這個(gè)過程通常是毫秒級(jí)的。
優(yōu)化點(diǎn)
相比內(nèi)存型服務(wù)來說,最常見的問題是磁盤的吞吐和延時(shí)帶來的毛刺點(diǎn)問題。除了通過慢日志命令來確認(rèn)是否有慢請(qǐng)求產(chǎn)生之外,還提供了 perflog 命令用來定位 RocksDB 訪問慢的問題,使用方式如下:
# 第一條命令設(shè)定只對(duì) SET 命令收集 profiling 日志
# 第二條命令設(shè)定隨機(jī)采樣的比例
# 第三條命令設(shè)定超過多長時(shí)間的命令才寫到 perf 日志里面(如果是為了驗(yàn)證功能可以設(shè)置為 0)
127.0.0.1:6666> config set profiling-sample-commands set
OK
127.0.0.1:6666> config set profiling-sample-ratio 100
OK
127.0.0.1:6666> config set profiling-sample-record-threshold-ms 1
OK
# 執(zhí)行 Set 命令,在去看 perflog 命令就可以看到對(duì)應(yīng)的耗時(shí)點(diǎn)
127.0.0.1:6666> set a 1
OK
127.0.0.1:6666> perflog get 2
1) 1) (integer) 1
2) (integer) 1623123739
3) "set"
4) (integer) 411
5) "user_key_comparison_count = 7, write_wal_time = 122300, write_pre_and_post_process_time = 91867, write_memtable_time = 47349, write_scheduling_flushes_compactions_time = 13028"
6) "thread_pool_id = 4, bytes_written = 45, write_nanos = 46030, prepare_write_nanos = 21605"
之前通過這種方式發(fā)現(xiàn)了一些 RocksDB 參數(shù)配置不合理的問題,比如之前 SST 文件大小默認(rèn)是 256MiB,當(dāng)業(yè)務(wù)的 KV 比較小的時(shí)候可能會(huì)導(dǎo)致一個(gè) SST 文件里面可能有百萬級(jí)別的 KV,從而導(dǎo)致 index 數(shù)據(jù)塊過大(幾十 MiB),每次從磁盤讀取數(shù)據(jù)需要耗費(fèi)幾十 ms。但線上不同業(yè)務(wù)的 KV 大小可能會(huì)差異比較大,通過 DBA 手動(dòng)調(diào)整的方式肯定不合理,所以有了根據(jù)寫入 KV 大小在線自動(dòng)調(diào)整 SST 和 Block Size 的功能,具體描述見:https://github.com/kvrockslabs/kvrocks/issues/118
另外一個(gè)就是 RocksDB 的全量 Compact 導(dǎo)致磁盤 IO 從而造成業(yè)務(wù)訪問的毛刺點(diǎn)問題,之前策略是每天凌晨低峰時(shí)段進(jìn)行一次,過于頻繁會(huì)導(dǎo)致訪問毛刺點(diǎn),頻率過低會(huì)導(dǎo)致磁盤空間回收不及時(shí)。所以增加另外一種部分 Compact 策略,優(yōu)先對(duì)那些比較老以及無效 KV 比較多的 SST進(jìn)行 Compact。開啟只需要在配置文件里面增加一行,那么則會(huì)在凌晨 0 到 7 點(diǎn)之間去檢查這些 SST 文件并做 Compact
compaction-checker-range 0-7
總結(jié)
在設(shè)計(jì)和實(shí)現(xiàn)上,Kvrocks 更注重簡潔高效、穩(wěn)定可靠、易于使用和問題定位。目前 Kvrocks 已經(jīng)在線上大規(guī)模運(yùn)行兩年之久,基本功能已充分驗(yàn)證,大家可以放心使用。如遇到問題,大家可以在微信群,Slack(見 GitHub README),Issue 上反饋和交流,我們也歡迎提 PR 來一起完善 Kvrocks。
在社區(qū)維護(hù)上,希望可以有更加開放的交流氛圍,而不只是把代碼放到 GitHub 的開源。不管是功能設(shè)計(jì)還是代碼開發(fā),都會(huì)盡量把相關(guān)細(xì)節(jié)都在 GitHub 里面公開去討論。
另外,2.0 版本預(yù)計(jì)在 7-8 月份會(huì)完成全部功能的開發(fā),大家可以期待一下,具體進(jìn)展見: https://github.com/kvrockslabs/kvrocks/projects