Floyd Small Object Compact Encoding:小型 Hash/Set/ZSet/List 紧凑存储预研 #345
AlexStocks
started this conversation in
Ideas
Replies: 0 comments
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Uh oh!
There was an error while loading. Please reload this page.
背景
Floyd 当前对 Hash、Set、ZSet、List 采用 MetaCF + DataCF 的展开存储:一个 Redis Key 除了 metadata 外,每个 field/member/list element 还会形成独立 RocksDB entry;ZSet 同时维护 member→score 和 (score, member)→empty 两套索引。
这种格式适合大对象和局部更新,但对于只有少量元素的小对象,会重复存储 Redis Key、version、reserve、ctime,并增加 MemTable/SST entry、WAL、Bloom Filter、Block Index、Block Cache 和 Compaction 开销。
RocksDB 从 8.0 开始提供 Wide Columns:一个 key 可以对应多个 named columns。需要评估能否借此将小 Hash/Set/ZSet/List 聚合存储,类似 Redis 的 listpack/intset/quicklist 思路。
相关资料:
当前结论
“小对象聚合为一个 RocksDB KV”方向有价值,但当前不建议直接以 RocksDB WideColumnEntity 作为 Floyd 的生产存储格式。
更合适的候选方案是:
本 Discussion 记录设计方向和后续验证项,当前优先级为 Low / Backlog,暂不进入实现。
为什么不直接采用 RocksDB Wide Columns
1. Wide Columns 当前仍是整体 Entity
RocksDB 会把 columns 序列化为一个 entity value,格式大致为:
PutEntity会覆盖完整 entity。当前公开 API 没有通用的单 column 更新、删除和只读取部分 columns 的能力。因此修改一个 Hash field,仍然需要读取、修改、重新序列化并写回整个 entity。截至 RocksDB 11.1.2,公开 DB API 仍以
PutEntity、GetEntity、MultiGetEntity为主,没有稳定的UpdateColumn/DeleteColumn/ProjectionAPI。2. 只有 Hash/Set 映射较自然
可能的映射:
问题是:
(score, member)排序;3. 当前 rust-rocksdb binding 没有 Entity API
Kiwi 使用定制的
arana-db/rust-rocksdb,当前固定版本内部携带 RocksDB 10.9.1。C++ 层存在 Wide Columns,但 Rust 层没有完整暴露:RocksDB 原生 C API 也没有直接提供完整 Wide Columns bridge,因此需要扩展 librocksdb-sys 的 C/C++ FFI、对象生命周期和错误处理。
4. Engine、Batch 与 Raft Binlog 不支持 Entity 类型
Floyd 当前 Engine/Batch 抽象只有普通 Put/Delete。Raft Binlog 的操作类型也只有:
Follower apply 会将 value 作为普通 KV 写入。WideColumnEntity 是 RocksDB internal value type;若只复制序列化后的 bytes 并使用普通 Put,Follower 得到的是普通 value,而不是 WideColumnEntity。
直接采用 Wide Columns,需要同步扩展 Engine、Batch、Binlog、Follower apply、snapshot/restore、混合版本兼容和存储格式门禁。
5. 当前 TTL Compaction Filter 无法处理 WideColumnEntity
Floyd 的 MetaCF TTL 清理依赖普通 CompactionFilter 解析 value 中的 type/etime。RocksDB 对 WideColumnEntity 需要使用
FilterV3;如果没有覆盖 FilterV3,默认行为是保留 WideColumnEntity。当前 rust-rocksdb binding 没有暴露 FilterV3。如果直接把对象写成 entity,过期对象可能逻辑不可见,但无法由现有 TTL Compaction Filter 物理清理。
候选方案:Floyd Compact Encoding
建议为复合类型增加两种持久化编码:
读取流程:
写入后若超过阈值:
自动状态转换只做:
Expanded 对象即使删除成员后重新变小,也不在在线请求路径自动转回 Packed,避免阈值附近反复转换、扫描 DataCF、制造 tombstone 和增加 Compaction 压力。未来如有需要,可由离线工具或低优先级后台任务完成 repack。
各数据类型的候选 Packed 编码
Hash
Set
建议支持两个子编码:
01与1错误合并。ZSet
(score, member)排序,直接支持范围和 rank 类命令;List
阈值策略
可以借鉴 Redis 的维度,但不能只照搬 entry 数量:
Floyd 还必须增加总编码大小上限:
只有三个条件都满足时保持 Packed。
第一轮 benchmark 候选范围:
Redis 的 list 8 KiB 是每个 quicklist node 的限制,不是整个 List 的限制。Floyd 第一版若把完整小 List 放入一个 Value,应把该限制理解为整个 Packed List 的大小上限。
存储格式与兼容性
该设计必须依赖 #342 的 StorageManifest 和 Value Format 版本门禁,例如:
新版本可以识别旧 Expanded 和新 Packed/Expanded;旧二进制必须拒绝打开包含新格式的数据目录。
不能只在 reserve 中写一个 encoding bit 后继续允许旧二进制打开,否则旧代码可能把 Packed Meta 当成普通 metadata,再去 DataCF 查成员,最终把实际存在的数据错误表现为空。
后续 Benchmark 指标
在重新启动该方向前,至少需要比较 Packed 与 Expanded 的:
测试数据应覆盖不同 entry 数、element 大小、总 payload 大小,以及 Hash/Set/ZSet/List 的 point/range/update/delete 命令。
优先级与启动条件
当前优先级:Low / Backlog。
在以下前置条件满足前不进入实现:
若将来 RocksDB 提供稳定的 column-level update/projection,且 rust-rocksdb 完整暴露 PutEntity/GetEntity/FilterV3,可以重新评估 Wide Columns。届时建议先从映射最自然的小 Hash 做实验,不一次覆盖四种数据类型。
非目标
All reactions