概述
在PikiwiDB (Pika) 系统的运维与治理中,大键 (BigKey) 是影响系统稳定性与性能的核心隐患。BigKey 通常指 Value 占用内存空间过大(如String 类型超过数 MB),或集合类数据结构包含元素数量过多(如 Hash/List/Set 结构包含数百万个成员)的键对。
传统的大键发现手段通常依赖于 redis-cli --bigkeys
命令或编写脚本进行全量 SCAN
。但在生产环境执行这些操作,效率太低,且容易对线上业务产生影响。
为了彻底解决“在线分析风险”这一痛点,PikiwiDB 社区推出了全新的离线分析利器 ——BigKey Analyzer。
与传统工具截然不同,bigkey_analyzer
采用了 离线直读 的设计,直接深入底层,精准剖析 RocksDB 的数据文件。
✨ 零侵入:直接读取磁盘上SST 文件,完全不占用线上服务的 CPU 和网络资源,对生产环境零干扰。
✨ 全景透视:不仅支持 Strings,更完美支持 Hashes, Sets, Zsets, Lists 等所有复杂数据结构。
✨ 真实存储视角:它计算的不是简单的 Value 长度,而是键值对在存储引擎中的真实大小。这对于磁盘容量规划具有极高的参考价值。
1️⃣工具支持的功能
(1) 大键检测和筛选
--min-size=SIZE
:只显示大于指定字节数的键--top=N
:只显示最大的N个键
(2) 键类型分析
--type=TYPE
:按类型筛选键
(3) 前缀统计分析
--prefix-stat
:按键前缀进行统计--prefix-delimiter=C
:自定义前缀分隔符
(4) 输出控制
--output=FILE
:将结果输出到文件
./bigkey_analyzer --help

查看大于1MB的最大的10个键.
./bigkey_analyzer --min-size=1048576 --top=10 data/pikiwidb/db在海量数据面前,全量输出往往会刷屏。
bigkey_analyzer
提供了过滤参数,能够在茫茫数据中精准定位出大key所在。图中列出了大小超过1MB的键,并给出了key的类型、大小、名称以及TTL的相关数据。
前缀聚合统计
大 Key 问题不是单个 Key 造成的,而是某个业务模块(具有相同前缀)生成了海量数据。使用 --prefix-stat
可以按前缀聚合统计。
./bigkey_analyzer --prefix-stat data/pikiwidb/db
查看所有键
./bigkey_analyzer data/pikiwidb/db
输出到文件
./bigkey_analyzer --output=result.txt /data/pikiwidb/db
检测原理:底层直读
分析工具之所以能做到“快而准”,关键在于它通过直接调用 RocksDB 的 API 打开数据库,彻底绕过了网络协议栈的开销。
在源码层面,它利用 rocksdb::DB::OpenForReadOnly
以只读模式打开指定路径的数据文件。这意味着它不需要建立 TCP 连接,不需要进行协议解析,直接遍历底层的迭代器(Iterator),效率获得了数量级的提升。
//以只读模式直接打开底层存储
rocksdb::DB* db;rocksdb::Status status = rocksdb::DB::OpenForReadOnly(options, path, &db);if (!status.ok()) {// Error handling...}
PikiwiDB 利用 RocksDB 的 Column Family 特性来实现复杂数据结构(如 Hash 和 Zset)的分离存储,分析工具必须具备“重组数据”的能力。它需要像拼图一样,将分散在不同 Column Family 中的元数据(Metadata)和数据实体(Data Entity)重新聚合,才能计算出一个大 Key 的真实体量。
源码分析
string 类型
String 类型的分析最为直观,因为它的元数据和数据体都存储在同一个 RocksDB 实例中。 源码中,工具直接遍历 default 列族,计算 Key 与 Value 的字节大小之和。
void AnalyzeStrings(const std::string& path, std::vector<KeyInfo>& key_infos, const Config& config) {......for (iter->SeekToFirst(); iter->Valid(); iter->Next()) {const std::string& key = iter->key().ToString();const std::string& value = iter->value().ToString();// 直接:大小 = 键长 + 值长int64_t size = key.size() + value.size();// ... (省略 TTL 解析逻辑)}delete iter;delete db;}
复杂数据结构
对于 Hash、Set、Zset 和 List,情况则变得相对复杂。PikiwiDB 为了支持高并发读写,将这些集合类型的Metadata 与 Data 拆分到了不同的 Column Families (CF) 中。
以 Hash 为例,工具必须同时打开两个视窗:
Default CF:存储 Hash 的 Key 以及字段个数、过期时间等元数据。
Data CF:存储具体的
field
和value
。
这意味着,一个 Hash Key 的真实大小,等于 元数据开销+ 所有 Field 的大小 + 所有 Value 的大小。为了不遗漏任何一个字节,采用了一种 分阶段聚合 的策略。
第一阶段:元数据扫描 首先遍历默认列族,捕获所有的 Hash Key,并在内存中建立一个Map。
// bigkey_analyzer -> AnalyzeHashesstd::unordered_map<std::string, std::pair<int64_t, int64_t>> hash_sizes;auto meta_iter = db->NewIterator(read_options, handles[0]); // handles[0] 是 default CFfor (meta_iter->SeekToFirst(); meta_iter->Valid(); meta_iter->Next()) {// 解析元数据,初始化基础大小(Key本身大小 + 内部开销)int64_t sum = key.size() + 12;hash_sizes[key] = std::make_pair(sum, ttl);}
第二阶段:数据归并 切换到 data_cf
列族进行全量扫描。每扫描到一个数据项(Field-Value 对),它会解析出其归属的 Hash Key,然后去 map 中累加大小。
auto data_iter = db->NewIterator(read_options, handles[1]); // handles[1] 是 data_cffor (data_iter->SeekToFirst(); data_iter->Valid(); data_iter->Next()) {// 反序列化 DataKey,提取其归属的主 Keystorage::ParsedHashesDataKey parsed_key(encoded_key_slice);std::string hash_key = parsed_key.key().ToString();// 计算当前 Field-Value 对的物理大小int64_t field_size = 4 + hash_key.size() + 4 + field.size() + value_slice.size();// 找到对应条目,累加大小auto it = hash_sizes.find(hash_key);if (it != hash_sizes.end()) {it->second.first += field_size;}}
随机查询是磁盘 I/O 的杀手,而我们的工具通过两次顺序扫描(Sequential Scan)配合内存聚合,极大地提升了分析效率。这正是离线分析工具相比在线 SCAN
命令在底层原理上的本质区别。
社区
欢迎加入我们的社区:





