第
12 篇 把 compaction 并发与 RateLimiter 钉在
CompactionJob 线程池上。LevelDB 时代「整库一棵
LSM + 一份 MANIFEST」在 Flink state、TiKV Region
本地引擎等场景不够用:同一 DB 实例内需要多组
comparator、独立 compaction 策略、可单独 drop
的逻辑分区,又不能牺牲跨分区原子写。
RocksDB 3.0 引入 Column
Family(CF):每个 key-value 恰属一个
CF;WAL 整库共享,MemTable 与 SST
文件按 CF 独立。本文是系列 第 13
篇,沿 Wiki Column Families 与
db/column_family.*
源码路径说明机制、ColumnFamilyHandle
语义、选项分层,以及 Flink 如何把多种 state 变量映射到不同
CF(Flink 侧细节见 stream/12,本系列第
16 篇补内核侧对照)。
本文是「RocksDB 内核机制」系列第 13 篇(共 18 篇)。→ 系列目录
篇目 核心内容 第 12 篇 · 并发 Compaction 与 Rate Limiter CompactionJob线程池第 13 篇 · Column Family 共享 WAL、独立 LSM、Flink CF 映射 第 14 篇 · 事务与 OCC OptimisticTransactionDB第 15 篇 · Checkpoint 与 Ingest 硬链接快照
版本锚定:RocksDB 9.x(
facebook/rocksdb,Wiki 与include/rocksdb/*.h);Flink 1.20+(CF 映射引用官方 State Backends 文档,B 级)。本篇不粘贴未实测的 WAL 回收延迟数字。
一、问题:单 LSM 树不够用时拆什么
LevelDB 只有 default 一棵 LSM:comparator、memtable 大小、compression、compaction 策略全库统一。第 3 篇 已把 CF 标为相对 LevelDB 的核心 diff 之一。生产嵌入的典型诉求:
| 诉求 | 单 CF 的困难 | CF 提供的抽象 |
|---|---|---|
| 不同 state 变量隔离 scan | 同一 Iterator 扫全库 | 每 CF 独立 LSM,点查/范围扫限定在 CF 内 |
| 按模块调 compaction | 全局 compaction_style |
ColumnFamilyOptions per-CF |
| 逻辑删除一组数据 | 逐 key Delete | DropColumnFamily 快速丢弃整棵子树 |
| 跨模块原子更新 | 只能靠 WriteBatch |
仍用 WriteBatch,但可一次写多个 CF |
CF 不是 多进程隔离,也
不是 独立 WAL 目录;它是 同一
DB 指针下的逻辑分区,磁盘上仍共用一个
DB 目录(MANIFEST 全局一份,见 第 6
篇)。
二、共享 WAL、独立 LSM:磁盘与内存布局
Wiki Column Families 的实现要点(A 级):
Column Families share the write-ahead log and don’t share memtables and table files.
flowchart TB
subgraph DB["单个 RocksDB DB 实例"]
WAL["共享 WAL (*.log)"]
subgraph CF1["Column Family: default"]
M1["MemTable / imm"]
V1["Version L0..Ln"]
S1["*.sst"]
end
subgraph CF2["Column Family: count"]
M2["MemTable / imm"]
V2["Version L0..Ln"]
S2["*.sst"]
end
MAN["全局 MANIFEST"]
end
Write["WriteBatch 多 CF 写"] --> WAL
WAL --> M1
WAL --> M2
M1 --> S1
M2 --> S2
V1 --> MAN
V2 --> MAN
一次 DB::Write 把整批
WriteBatch 追加到 当前活跃
WAL;recovery 时按 batch 里的 CF
id 把记录 replay 到 各自 CF 的
MemTable(第 4
篇)。读路径上,每个 CF 持有独立
SuperVersion(mem + imm +
Version),第 7 篇
的 GetImpl 需传入
ColumnFamilyHandle* 才能定位正确 LSM。
原子写跨 CF:WriteBatch
可含 {cf1,k1,v1} 与
{cf2,k2,v2};要么整批
commit,要么都不生效——依赖 单 WAL 顺序 + 单 sequence
空间,而非 per-CF WAL。
三、WAL 回收与
max_total_wal_size
共享 WAL 带来 回收耦合:某个 CF flush 后,WAL 里仍可能有 其他 CF 未落盘 的数据,旧 WAL 不能删。
Wiki 说明(A 级):
- 任一 CF flush 时可能切换新 WAL;新写入进新文件。
- 旧 WAL 仅当 所有 CF 都已 flush 且其中数据全部进入 SST 后才可删除。
- 应定期 flush 各
CF;
DBOptions::max_total_wal_size可在 WAL 过大时 自动 flush 滞后的 CF。
工程含义:Flink 多 CF 作业若某一 state 写入很少、长期不 flush,会 拖住 WAL 体积,间接影响 checkpoint 与磁盘占用——这与 stream/13 状态调优 里「多 CF 共享 WAL」的调参点一致。本篇只钉机制,不展开 Flink 指标。
四、ColumnFamilyHandle
与 Open 语义
ColumnFamilyHandle 类似 已打开的 CF
文件描述符(Wiki 用语)。要点:
| API / 行为 | 说明 |
|---|---|
DB::Open(..., vector<ColumnFamilyDescriptor>, handles) |
读写打开须 列出 DB 中现有全部 CF,否则
InvalidArgument |
DB::OpenForReadOnly |
可只打开 CF 子集 |
DB::ListColumnFamilies |
静态方法,枚举磁盘上 CF 名 |
CreateColumnFamily /
DropColumnFamily |
在线增删;Drop 后若仍有 Handle 引用,数据延迟删除 |
delete handle |
关闭前须释放 所有 Handle,再
delete db |
WriteBatch::Put(ColumnFamilyHandle*, ...)、Get/Iterator/NewIterator
等均需显式 CF 参数。默认 CF 名为
"default";仅用旧 API(无 CF 参数)的代码等价于
只写 default(Wiki Backward
compatibility)。
Drop 语义:DropColumnFamily
后 Handle 仍可对 已 drop CF
读写,直到最后一个 Handle
释放——便于滚动升级时排空引用,但运维上容易误判「已删仍在用」。
五、选项分层:DBOptions
与 ColumnFamilyOptions
RocksDB 3.0 起把 Options
拆成两层(include/rocksdb/options.h,A
级):
| 结构 | 作用域 | 典型字段 |
|---|---|---|
DBOptions |
整库 | max_background_jobs、wal_dir、rate_limiter、statistics |
ColumnFamilyOptions |
单 CF | write_buffer_size、compaction_style、comparator、table_factory |
Options |
继承二者 | 单 CF 或 default CF 的便捷写法 |
ColumnFamilyDescriptor = CF 名 +
ColumnFamilyOptions,在
Open 时成批传入。不同 CF 可配置 不同
leveled/universal 策略、不同 bloom、不同 TTL(第
10–11 篇 的 picker 按 CF 独立运行)。
Comparator 约束:同一 DB 内各 CF
可以 使用不同 comparator(按 CF
配置);SstFileWriter / external ingest 时
comparator 必须与目标 CF 完全一致(第
15 篇)。
六、MANIFEST 与多 CF 版本链
第 6
篇 已述:MANIFEST
全局一份,VersionEdit 带
kColumnFamily 区分
CF;VersionSet::ColumnFamilySet 维护
ColumnFamilyData → Version* 链表。
Flush / compaction 提交时,LogAndApply
可一次写入 多 CF 的 Edit
组(kInAtomicGroup),保证 跨
CF 的元数据视图一致切换。读侧每个 CF 的
SuperVersion 独立切换;不存在「一个 CF
看到新 SST、另一个仍见旧 Version」的半套状态(在同一
GetAndRefSuperVersion(cf) 调用内)。
七、Flink 多 state 变量 → 多 Column Family
Flink EmbeddedRocksDBStateBackend
的映射规则(Apache Flink 文档 State Backends →
Predefined Per-ColumnFamily Options,B 级):
当前实现中,算子里的每一种 registered state 对应一个 RocksDB Column Family。
示例:同一窗口算子注册
ValueStateDescriptor("count") 与
MapStateDescriptor("user-last-seen") → 该
subtask 的 RocksDB 至少 两个业务 CF(另加
RocksDB 内置 default,通常不参与 Flink
状态)。
flowchart LR
OP["WindowOperator"]
subgraph RDB["subtask RocksDB"]
CFc["CF: count"]
CFm["CF: user-last-seen"]
WAL["共享 WAL"]
BC["共享 Block Cache / WBM"]
end
OP --> CFc
OP --> CFm
WAL --> CFc
WAL --> CFm
与内核机制的对应关系:
| Flink 概念 | RocksDB 机制 |
|---|---|
| 多种 state 变量 | 多 CF,独立 LSM |
| KeyGroup 前缀 | user key 编码,不是 CF 边界(stream/12) |
| 增量 checkpoint | 上传 不可变 SST;依赖 CF 内 SST 与全局 MANIFEST 语义(第 15 篇 Checkpoint) |
RocksDBOptionsFactory |
按 CF 名返回不同 ColumnFamilyOptions |
分工:stream/12 讲作业布局与 KeyGroup;本篇讲 为何 Flink 选 CF 而非 key 前缀分 state——便于 per-state TTL compact filter、savepoint 元数据对齐、以及缩小单次 Iterator 扫描范围。第 16 篇将把 TiKV / Kafka Streams 一并对照。
八、API 示例:Open 与跨 CF 原子写
#include "rocksdb/db.h"
#include "rocksdb/options.h"
DBOptions db_opts;
std::vector<ColumnFamilyDescriptor> cfs{
{"default", ColumnFamilyOptions{}},
{"metrics", ColumnFamilyOptions{}},
};
std::vector<ColumnFamilyHandle*> handles;
rocksdb::DB* db;
auto s = DB::Open(db_opts, "/tmp/cf_demo", cfs, &handles, &db);
WriteBatch batch;
batch.Put(handles[0], "k1", "v1");
batch.Put(handles[1], "m1", "100");
WriteOptions wopts;
s = db->Write(wopts, &batch); // 单 WAL 记录,原子跨 CF
db->DropColumnFamily(handles[1]);
for (auto* h : handles) delete h;
delete db;(示例仅说明 API 形状;路径与错误处理从简。)
九、与 LevelDB / 单 CF 心智的对照
| 维度 | LevelDB | RocksDB 多 CF |
|---|---|---|
| WAL | 1 份 | 仍 1 份,全 CF 共享 |
| MemTable / SST | 1 套 | 每 CF 一套 |
| MANIFEST | 1 份 | 1 份,Edit 带 CF id |
| Open | 无 CF 列表 | 读写须枚举已有 CF |
| 原子批写 | WriteBatch |
WriteBatch + 多 CF handle |
十、小结
- Column Family = 共享 WAL + 独立 MemTable/SST/Version/SuperVersion 的逻辑分区。
ColumnFamilyHandle管理 CF 生命周期;Drop 后延迟删数据直至 Handle 归零。DBOptions/ColumnFamilyOptions拆分库级与 CF 级调参;Flink 为 每种 registered state 建一个 CF。- WAL 回收 受最慢 flush 的 CF
牵制;
max_total_wal_size用于自动 flush 落后 CF。 - 读路径、compaction、checkpoint 均须在 正确 CF 上下文 下理解——后续事务(第 14 篇)与 ingest(第 15 篇)同样依赖 CF 边界。
上一篇:并发 Compaction 与 Rate Limiter
下一篇:事务与 OptimisticTransactionDB
返回 系列目录
参考资料
- RocksDB Wiki, Column Families(共享 WAL、flush
与 WAL 删除、
max_total_wal_size;A 级)。 - RocksDB 9.x
源码:
db/column_family.h、db/column_family.cc(ColumnFamilyData、SuperVersion);db/version_set.cc(ColumnFamilySet、LogAndApply);include/rocksdb/options.h(DBOptions、ColumnFamilyOptions)(A 级)。 - RocksDB 9.x
源码:
include/rocksdb/db.h(Open、CreateColumnFamily、DropColumnFamily、ListColumnFamilies)(A 级)。 - Apache Flink 文档, State Backends → Predefined Per-ColumnFamily Options(每种 registered state 对应一个 CF;B 级)。
- stream-processing 第 12 篇:RocksDB State Backend;第 6 篇 MANIFEST;第 4 篇 WAL。
同主题继续阅读
把当前热点继续串成多页阅读,而不是停在单篇消费。
【RocksDB 内核机制】LevelDB · WAL · Compaction · Column Family · 生产嵌入
补全存储引擎三角最后一角:从 LevelDB 基线与 RocksDB 架构演进,到 WAL/MemTable/SST 写路径、Get/Iterator 读路径、Leveled/Universal compaction 与 write stall,再到 Column Family、事务、Checkpoint 与 Flink/TiKV 嵌入对照。
【RocksDB 内核机制】生产嵌入对照:Flink · TiKV · Kafka Streams
从 RocksDB 内核侧补全 Flink KeyGroup 前缀与增量 checkpoint SST 上传机制,对照 TiKV Region 引擎与 Kafka Streams changelog 容错;不重复 stream/12 作业侧全文,给出 Docker 复现步骤而不伪造 checkpoint 指标。
【RocksDB 内核机制】LSM 生态全景:存储引擎三角与 RocksDB 生态位
闭合存储引擎三角的最后一角:对照 PG B-Tree 与列存 scan,定位 LSM 写优化引擎在 TiKV、Flink、Kafka Streams、ClickHouse Embedded 中的嵌入方式,并划分本系列与 lsm-tree DIY、storage/31–32、stream/12 的分工边界。
【RocksDB 内核机制】WAL 与 WriteBatch:持久化与原子批写
从 log::Writer 的 32KB Block 分片 record、WriteBatch 二进制布局,到 DBImpl::WriteImpl 中 Group Commit 与 sync/fsync 语义,钉住写路径第一步:先 WAL 后 MemTable 的提交顺序与原子性边界。