暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

PikiwiDB 离线大键分析工具深度原理解析

Pika开源社区 2026-03-27
43

概述

     在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
    2️⃣ 操作案例
    1. 查看大于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
        1. 查看所有键

          ./bigkey_analyzer data/pikiwidb/db
          1. 输出到文件

            ./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 的真实体量。

              源码分析

              1. 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;
                }


                1. 复杂数据结构

                对于 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 -> AnalyzeHashes
                  std::unordered_map<std::string, std::pair<int64_tint64_t>> hash_sizes;
                  auto meta_iter = db->NewIterator(read_options, handles[0]); // handles[0] 是 default CF
                  for (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_cf
                    for (data_iter->SeekToFirst(); data_iter->Valid(); data_iter->Next()) {
                        // 反序列化 DataKey,提取其归属的主 Key
                        storage::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
                     命令在底层原理上的本质区别。

                    社区

                    欢迎加入我们的社区:




                    文章转载自Pika开源社区,如果涉嫌侵权,请发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

                    评论