在海量 KV 存储场景中,RocksDB 的写放大(Write Amplification)一直是架构师的心头大患。为此,PingCAP 开发了 Titan 作为 RocksDB 的 KV 分离插件,通过将大 Value 写入独立的 Blob 文件来减缓 LSM-Tree 的重写压力。
然而,在实际生产环境中,业务负载是动态变化的。某个 Column Family(以下简称 CF)在初期可能有大量大 Value 写入,但随着业务重构,其 Value 尺寸可能变小。此时,继续保留 Titan 不仅无法降低写放大,反而会引入额外的 Blob GC 读写开销和双重空间放大。
如何针对特定 CF 优雅地启用或禁用 Titan?能否做到不停机动态切换?直接关闭配置会发生什么灾难?本文将结合 Titan 底层设计,为你提供一套完整的生产级落地指南。
一、 核心误区:为什么不能直接在配置中关闭 Titan?
在探讨优雅方案之前,必须先纠正一个致命的误区:“既然我不想用 Titan 了,直接把配置文件的 Titan 相关参数删掉,或者把 enable_titan 改为 false 重启不就行了?”
千万不要这么做。这会导致数据库直接报 Corruption 错误或读取到脏数据。
底层原理分析
当 Titan 启用时,RocksDB 的 MemTable 和 SST 中存储的不再是原始的 Value,而是一个指向 Blob 文件的指针(即 BlobIndex,包含 Blob File ID、Offset 和 Size)。
如果你在依然存在 BlobIndex 的情况下强行关闭 Titan 引擎:
- RocksDB 依然能正常启动(因为它把
BlobIndex当作普通的 Value 读入)。 - 当客户端发起
Get请求时,RocksDB 会直接把这个BlobIndex结构体(一串二进制数据)作为用户数据返回。应用层会拿到一堆乱码。 - 在 Compaction 过程中,由于失去了 Titan 引擎的解析,RocksDB 无法正确回收底层的 Blob 文件,导致磁盘空间永久泄漏。
因此,禁用 Titan 的核心逻辑是:必须将所有已写入 Blob 文件的 Value,安全地写回(Migration)到普通的 RocksDB SST 中,直到 Blob 文件数量归零,方可物理关闭。
二、 动态切换的核心武器:blob_run_mode
Titan 官方设计了一个非常优雅的枚举参数 blob_run_mode,它是实现动态切换和安全过渡的关键。该参数有三个可选值:
Normal:正常运行模式。大 Value 写入 Blob,小 Value 写入 SST;Compaction 时继续分离。ReadOnly:只读模式。停止写入新的 Blob。所有新写入的 KV(无论多大)都会直接存入 RocksDB SST。对于旧的BlobIndex,依然支持正常读取。Fallback:回水/回流模式。同样停止写入新 Blob。更重要的是,在 RocksDB 进行 Compaction 时,一旦碰到旧的BlobIndex,会主动把 Blob 里的原始 Value 读出来,重新写回新生成的 SST 文件中。
通过调控 blob_run_mode,我们就可以在不停机的情况下,优雅地实现 Titan 的启用与禁用。
三、 实操:优雅禁用特定 CF 的 Titan(回水方案)
假设我们需要对名为 write_heavy_cf 的 CF 禁用 Titan。以下是标准的操作流程:
第一步:在线修改 blob_run_mode 为 Fallback
如果使用的是 TiKV,可以通过 tikv-ctl 或 SQL(如果是 TiDB)在线动态调整参数,无需重启服务:
# 使用 tikv-ctl 动态修改指定 CF 的 blob_run_mode
tikv-ctl --host 127.0.0.1:20160 modify-tikv-config -n "rocksdb.write_heavy_cf.titan.blob-run-mode" -v "fallback"
注意: 此时新写入的数据已经不会再进入 Titan 空间,全部流入 RocksDB SST。
第二步:加速数据回水(Trigger Compaction)
将模式改为 Fallback 后,旧数据只有在发生 Compaction 时才会写回 SST。如果该 CF 的写入变慢,自然 Compaction 可能会耗时极长。
为了加速这一过程,建议对该 CF 发起一次 Manual Compaction(手动重写):
# 手动触发指定 CF 的全量 Compaction
tikv-ctl --host 127.0.0.1:20160 compact -c write_heavy_cf
第三步:监控 Blob 文件数量
在回水期间,需要严密监控该 CF 关联的 Blob 文件数量。可以通过 Prometheus 监控指标 tikv_engine_num_blob_files,或者直接通过 CLI 工具查看。
只有当该 CF 的 Blob File Count 降为 0 时,才说明数据已经完全迁回 RocksDB。
第四步:物理关闭(可选)
当 Blob 文件清零后,你可以安全地在配置文件(如 tikv.toml)中把该 CF 的 Titan 彻底关闭,以便释放 Titan 占用的内存 Buffer:
[rocksdb.write_heavy_cf.titan]
enabled = false # 此时可以安全设置为 false
保存并伴随下一次业务低峰期滚动重启集群,即完成了该 CF 的优雅禁用。
四、 实操:优雅启用特定 CF 的 Titan
相反地,如果一个现有的普通 CF 随着业务发展,大 Value 变多,需要开启 Titan,步骤则简单得多。
1. 配置文件修改
直接在配置文件中为该 CF 开启 Titan,并配置触发分离的阈值(如 min-blob-size = "1KB"):
[rocksdb.write_heavy_cf.titan]
enabled = true
min-blob-size = "1KB"
blob-run-mode = "normal"
2. 滚动重启与数据渐进分离
重启后,新写入的、大于 1KB 的 Value 会直接进入 Titan Blob 文件。
那么存量的旧大 Value 呢?
你不需要做任何特殊处理。随着 RocksDB 后台自动进行 Compaction,当高层 SST 的数据向底层合并时,Titan 会自动检测并把大于阈值的 Value 剥离出来,写入全新的 Blob 文件。这个过程对业务是完全无感的。
五、 生产环境动态切换的“避坑指南”
在执行上述动态切换(尤其是禁用/回水)时,必须注意以下两个潜在的性能陷阱:
1. 临时空间暴涨(Space Amplification Spike)
在 Fallback 阶段,旧的 Blob 文件在没有被完全清理前,其数据已经被复制回了 RocksDB SST 中。这意味着在 Compaction 完成前,该部分数据在磁盘中同时存在了两份(一份在 SST,一份在 Blob)。
- 对策:确保在执行回水操作前,目标磁盘至少留有 30% 以上的可用空间。避免因空间写满触发 RocksDB 的 Read-Only 保护。
2. I/O 吞吐与 CPU 抖动
Fallback 模式下的 Compaction 属于“重度 I/O 密集型”操作。因为系统不仅要写 SST,还要高频并发读取旧的 Blob 文件来还原 Value。
- 对策:
- 不要同时对多个大 CF 执行
Fallback手动压缩。 - 通过限制
max-background-jobs或使用rate_limiter限制 Compaction 的最大写带宽,防止挤占前台业务的 Read/Write 延迟。
- 不要同时对多个大 CF 执行
3. 严格校验 min-blob-size
在准备启用 Titan 时,千万不要把 min-blob-size 设得太小(例如小于 128 字节)。过小的阈值会导致大量本属于 LSM 优势区间的小 Key 强行分离,不仅无法减缓写放大,反而会因为频繁维护 BlobIndex 的元数据和点查带来的双重 I/O,导致读性能暴跌。
总结
在 RocksDB/Titan 体系中,blob_run_mode 是保证数据平滑迁移的唯一安全通道。切记“先软后硬”的原则:禁用时先用 Fallback 回水,待监控指标清零后再物理关闭;启用时合理评估阈值,依靠 LSM 自然收敛。掌握这套机制,才能在多变的业务负载下,让底层存储架构游刃有余。