版本范围:RocksDB 7.x ~ 9.x
什么是 ingest?
RocksDB 的 ingest,完整名字通常叫:
cpp
DB::IngestExternalFile()
它做的事情是:
先在 RocksDB 外面生成一个 SST 文件,然后把这个 SST 文件直接接入 RocksDB 的 LSM-tree。
普通写入是这样:
text
Put / Write
-> WAL
-> MemTable
-> Flush 到 L0
-> Compaction 往下推
ingest 是这样:
text
离线生成 SST
-> IngestExternalFile
-> 直接接入 LSM-tree 的某一层
所以 ingest 的核心价值是:
对于大批量有序数据,不再逐条写 MemTable,也不一定要先进 L0,可以减少写入路径上的开销和后续 compaction 压力。
但要注意一句话:
ingest 不是"无成本导入",也不是"一定直接进最底层"。
它会根据 key range、sequence number、memtable、已有 SST 文件、正在运行的 compaction,选择一个安全的 level。
为什么需要 ingest?
先看普通写入。如果你有一大批数据:
text
k000001 -> v1
k000002 -> v2
k000003 -> v3
...
k999999 -> v999999
如果逐条 Put(),路径是:
text
每条写入
-> 写 WAL
-> 插入 MemTable
-> MemTable 满了 flush 到 L0
-> L0 文件多了 compact 到 L1
-> 再从 L1 compact 到 L2
-> ...
这当然能工作,但在批量导入、数据迁移、快照恢复这类场景里,代价偏大:
- 每条数据都要走 MemTable;
- MemTable flush 后一般先生成 L0 文件;
- L0 文件多了会触发 compaction;
- 大批量导入可能把 L0 打爆;
- 后台 compaction 会和前台业务抢 IO;
- 最终可能造成 write stall。
但如果这批数据本来已经是有序的,或者可以在线下排序,那就没必要一条条写进 MemTable,我们其实可以先生成一个标准 SST 文件:
text
file1.sst:
k000001 -> v1
k000002 -> v2
k000003 -> v3
...
然后直接把这个文件接入 RocksDB。这就是 ingest。
ingest 适合什么场景?
常见场景有几类。
1. 批量导入
例如:
text
外部系统导出一批 key-value
-> 离线排序
-> 生成 SST
-> ingest 到 RocksDB
这比逐条 Put() 更接近批处理路径。
2. 数据迁移
比如一个分片、一个 range、一个 Region 要从 A 节点迁移到 B 节点,可以这么做:
text
A 节点导出某段 key range 的 SST
-> 传输到 B 节点
-> B 节点 ingest
3 快照恢复
如果一个副本落后很多,重新追日志成本太高,可以直接应用一个快照。快照里的数据可以组织成 SST,然后 ingest 到本地 RocksDB。这类场景特别依赖 ingest,因为它比逐条回放更适合大块数据恢复。
4. 历史数据回填
例如你已经有一份在线数据,现在要补一批历史数据。
这个时候要区分两种语义:
text
普通 ingest:
新 ingest 的数据应该像一次新写入,可能覆盖旧数据。
ingest_behind:
新 ingest 的数据是历史数据,只能填空,不能覆盖已经存在的新数据。
ingest_behind 后面单独讲。
怎么生成一个可以 ingest 的 SST?
通常用:
cpp
rocksdb::SstFileWriter
简化代码类似这样:
cpp
Options options;
SstFileWriter writer(EnvOptions(), options);
Status s = writer.Open("/tmp/file1.sst");
assert(s.ok());
s = writer.Put("k000001", "v1");
assert(s.ok());
s = writer.Put("k000002", "v2");
assert(s.ok());
s = writer.Put("k000003", "v3");
assert(s.ok());
s = writer.Finish();
assert(s.ok());
IngestExternalFileOptions ifo;
s = db->IngestExternalFile({"/tmp/file1.sst"}, ifo);
assert(s.ok());
注意几个硬约束。
a. key 必须按 comparator 严格递增
SST 文件内部本来就是有序的。所以写入 SST 时,key 必须按目标 RocksDB 的 comparator 顺序递增:
text
正确:
a
b
c
d
错误:
a
c
b
d
如果顺序不对,这个 SST 就不是合法的 ingest 输入。这里的"有序"不是字符串肉眼看起来有序,而是:
text
按照目标 Column Family 的 comparator 有序
如果用了自定义 comparator,就必须按自定义 comparator 的顺序写。
b. comparator 必须一致
生成 SST 用的 comparator 必须和目标 DB / Column Family 的 comparator 一致。否则 RocksDB 无法保证:
text
文件内部顺序正确
文件 range 判断正确
查找语义正确
compaction 语义正确
这类错误非常危险,因为它不是简单的性能问题,而是可能破坏 LSM 的基本有序性。
c. SST 的 table format / compression 要兼容
SstFileWriter 生成文件时,会带上 table format、compression、checksum 等信息,目标 DB 必须能识别这些格式。所以跨版本、跨编译选项、跨 RocksDB 配置 ingest 时,要特别注意:
text
table factory
compression
checksum
comparator
column family 配置
尤其不要随便拿一个 RocksDB 实例内部已经生成的 .sst 文件,直接丢到另一个 RocksDB 里 ingest。正常 ingest 的主线假设是:
这个文件是用
SstFileWriter生成的外部 SST。
RocksDB 9.5 之后开始提供 DB-generated SST ingest 的实验能力,但 7.x、8.x、早期 9.x 不应该把它当作基础能力来设计。
IngestExternalFile 内部大概做什么?
一次普通 ingest,大概分几步。
第一步:把外部文件接入 DB 目录
RocksDB 会把外部 SST 文件放进 DB 的文件管理体系里。这里可能是:
text
copy
hard link
move
取决于选项、文件系统、路径关系和权限。常见选项:
cpp
IngestExternalFileOptions ifo;
ifo.move_files = true;
move_files = true 的意思是尽量移动文件,而不是复制文件。这可以减少大文件复制成本。但要注意:
- move 之后原路径文件就不应该再被上层使用;
- 跨文件系统可能 move 失败;
- 失败后是否 fallback 到 copy,取决于相关选项;
- 权限、只读文件、文件系统行为都可能影响 ingest。
所以生产环境里,move_files 要配合文件生命周期管理一起设计。
第二步:短暂阻塞写入
普通 ingest 需要给这个 SST 文件里的所有 key 分配 sequence number。RocksDB 的读写一致性依赖 sequence number。简单讲:
text
sequence number 越大,版本越新
snapshot 通过 sequence number 决定能看到哪些版本
外部 SST 文件刚生成时,里面的数据并不是通过当前 DB 的写路径产生的。所以 ingest 时,RocksDB 需要给它分配一个合适的全局 sequence number。为了保证这个 sequence number 和同时发生的前台写入之间顺序一致,RocksDB 会在关键阶段阻塞写入。所以 ingest 不是完全透明的后台操作。它可能造成前台写入短暂抖动。
第三步:必要时 flush memtable
如果外部 SST 的 key range 和当前 memtable 有重叠,RocksDB 可能需要先 flush memtable。
为什么?
因为 memtable 里有还没落盘的数据,而且这些数据也有自己的 sequence number。如果不处理好,就可能出现这种问题:
text
memtable 里有 user:1 = new
ingest 文件里有 user:1 = imported
这两个版本谁新、谁旧,必须严格由 sequence number 决定。为了让 ingest 后的 LSM 结构和版本顺序是可解释的,RocksDB 可能会先把相关 memtable flush 成 SST,再继续 ingest,这也意味着:
ingest 可能间接触发 flush,从而放大写入抖动。
如果你设置:
cpp
allow_blocking_flush = false;
那么当 ingest 需要 blocking flush 时,它可能直接失败,而不是阻塞等待。这个选项适合对延迟非常敏感、宁愿失败重试也不愿阻塞的场景。
第四步:选择一个安全的 level
这是 ingest 最容易被误解的地方。很多人会问:
ingest 的 SST 是不是先到 L0?
答案是:
普通 ingest 不是一定先进 L0。
RocksDB 会尝试把它放到尽可能低、同时又安全的 level。
这和普通写入不同。普通写入 flush 出来的 SST 一般先进 L0:
text
Put
-> MemTable
-> Flush
-> L0
而 ingest 是:
text
External SST
-> 选择一个合适 level
-> 直接挂到 LSM
它的目标是尽量减少后续 compaction。
什么叫"尽可能低且安全的 level"?
假设有一个待 ingest 的文件:
text
fileX: [d, k]
RocksDB 会尝试找一个 level,把它放进去。但它要满足一些条件。
1. 不能破坏同层不重叠
对于 leveled compaction,L1 及以下通常要求同一层文件 key range 不重叠。比如 L2 已经有:
text
L2:
fileA: [a, f]
fileB: [g, m]
待 ingest 文件是:
text
fileX: [d, k]
那 fileX 同时和 fileA、fileB 重叠。如果直接放到 L2,就会破坏 L2 的不重叠结构。所以不能简单放进去。
2. 不能和上层产生不安全的版本关系
假设待 ingest 文件想放到 L4,但 L1、L2 或 L3 中有和它 key range 重叠的文件。这时候要非常小心,因为 RocksDB 读路径依赖这样的基本直觉:
text
上层数据通常更新
下层数据通常更旧
普通 ingest 的外部 SST 会被分配一个新的 global sequence number。它在语义上更像"一次新写入"。如果把一个"新版本"的文件放到很底层,而上层还有重叠的旧文件,就可能让读路径和版本关系变复杂,甚至产生 stale read 风险。所以 RocksDB 在选择 level 时,要保证这个文件放进去之后,不会和上层已有文件形成不安全的重叠关系。
3. 不能和正在运行的 compaction 输出冲突
还有一个容易忽略的点:
ingest 不只要看当前已有文件,还要看正在运行的 compaction。
如果某个 compaction 正在往 L2 输出文件,而待 ingest 文件也想放 L2,二者 key range 有冲突,那也不能直接放。否则两个后台动作可能同时破坏 level 的不重叠约束。
4. 放不下去怎么办?
如果低层放不进去,RocksDB 会退而求其次,选择更高的 level。在一些情况下,文件可能最终落到 L0。所以结论是:
text
普通 ingest 会尽量不进 L0
但如果 key range 不干净、和上层/同层重叠严重,它仍然可能进 L0 或较高 level
这就是为什么同样是 ingest,有时候很轻,有时候会把 L0 和 compaction 打爆。
一个直观例子:干净 range 的 ingest
假设 DB 里已有数据:
text
L1:
[a, c]
L2:
[m, z]
现在 ingest 一个文件:
text
fileX: [d, l]
它和 L1、L2 都不重叠,这时 RocksDB 很可能可以把它放到比较低的 level。结果类似:
text
L2:
[d, l]
[m, z]
这类 ingest 很便宜,它接近于:
text
改元数据
-> 文件接入 LSM
-> 后续 compaction 压力较小
一个反例:脏 range 的 ingest
现在 DB 里有:
text
L0:
[a, z]
[b, y]
L1:
[a, f]
[g, m]
[n, z]
L2:
[a, z]
你要 ingest:
text
fileX: [c, x]
这个文件范围很宽,几乎和所有层都有重叠。这时 RocksDB 很难把它直接放到低层。它可能只能放到很高的 level,甚至 L0。后续结果就是:
text
L0 文件数增加
读放大增加
pending compaction bytes 增加
后续 compaction 需要慢慢消化
所以 ingest 的性能不只取决于文件大小,还取决于:
text
key range 是否干净
是否和已有数据重叠
是否和 memtable 重叠
是否和正在 compaction 的输出重叠
global sequence number 是什么?
理解 ingest,一定要理解 global sequence number。RocksDB 每次写入都会分配 sequence number:
text
seq=100: user:1 = Alice
seq=200: user:1 = Bob
读的时候,在满足 snapshot 约束的前提下,seq 更大的版本更新。但外部 SST 文件不是通过当前 DB 的 Write() 生成的。它里面的 key-value 没有当前 DB 分配的正常 sequence number。所以 RocksDB 引入 global sequence number:
text
fileX.sst:
k1 -> v1
k2 -> v2
k3 -> v3
ingest 后:
整个文件里的 key 都像 seq=500 的写入
也就是说,这个 SST 文件里的所有 entry 可以共享一个 global seqno,这有两个意义:
- 让 ingested 数据进入 RocksDB 的多版本读写体系;
- 让 snapshot、读路径、compaction 可以正确理解这个文件里的版本新旧。
write_global_seqno 是什么?
常见选项:
cpp
ifo.write_global_seqno = true / false;
它控制的是:
ingest 时是否把 global sequence number 写回 SST 文件的 metablock。
如果写回 SST,就需要对 SST 文件做一次小的随机写。如果不写回,RocksDB 可以通过 MANIFEST 记录来推导这个文件的 global sequence number。大概理解:
text
write_global_seqno = true:
global seqno 写进 SST 文件本身
兼容老版本更好
但需要修改 SST 文件
write_global_seqno = false:
不改 SST 文件
依赖 MANIFEST 记录 global seqno
对不支持随机写的文件系统更友好
在 7.x~9.x 中,这个选项已经是常见 ingest 语义的一部分。如果是跨版本兼容,尤其要关注这个选项。
ingest 会不会覆盖已有 key?
普通 ingest 的语义更像:
text
把外部 SST 当作一次新的批量写入
所以如果外部 SST 里有:
text
user:1 = imported
而 DB 里之前有:
text
user:1 = old
普通 ingest 后,从最新视图看,通常应该读到:
text
user:1 = imported
也就是说,普通 ingest 可以表达"新数据覆盖旧数据"。但这并不意味着它会立刻把旧数据物理删除。旧版本可能还在底层 SST 中,后续由 compaction 清理。所以逻辑结果和物理清理要分开看:
text
逻辑上:
新值可见
物理上:
旧值可能还在,等 compaction 回收
ingest_behind 是什么?
ingest_behind 是一个特殊模式。
选项类似:
cpp
ifo.ingest_behind = true;
它的语义和普通 ingest 不一样。普通 ingest 像是:
text
我导入的是新数据,应该覆盖旧数据
ingest_behind 像是:
text
我导入的是历史数据,只能填补空白,不能覆盖已经存在的新数据
例如 DB 里已有:
text
user:1 = Bob
你 ingest_behind 的文件里有:
text
user:1 = Alice
user:2 = Carol
那么最终语义应该接近:
text
user:1 仍然是 Bob
user:2 可以补进 Carol
也就是说,已有数据优先,ingest_behind 的数据在"后面"。
ingest_behind 为什么总是放 bottommost level?
ingest_behind 的核心语义是历史数据回填。历史数据应该比当前 DB 里的所有数据都旧。所以它会使用非常老的 sequence number,通常可以理解成:
text
seqno = 0
既然它是最老的数据,就应该放在最底层:
text
Lmax / bottommost level
这样符合 LSM 的直觉:
text
越上层越新
越下层越旧
所以 ingest_behind 的典型路径是:
text
外部历史 SST
-> bottommost level
-> 作为最老版本存在
ingest_behind 的限制
这个功能很容易被误用。几个重要限制要记住。
1. 必须从 DB 创建之初就允许
RocksDB 要求 DB 从一开始就以允许 ingest behind 的方式运行。也就是说,不能一个已经跑了很久的 DB,突然打开 allow_ingest_behind,然后开始做历史回填。原因是:RocksDB 需要从全局语义上保证"seqno=0 的历史数据"不会破坏已有版本顺序。
2. 主要用于历史回填,不是覆盖导入
如果你希望导入数据覆盖已有数据,用普通 ingest。如果你希望导入数据只补缺失,不覆盖已有数据,才考虑 ingest_behind。不要把这两个场景混在一起。
3. 只适合特定 compaction style
在 7.x~9.x 的主线文档口径里,ingest_behind 主要是和 universal compaction 一起使用的能力。如果你的 DB 使用 level compaction,不应该默认认为 ingest_behind 可用。
这点非常关键。
很多线上 RocksDB,尤其是在线 KV 场景,通常使用的是 level compaction。这种场景下讨论 ingest_behind,一定要先确认实际 RocksDB 版本、compaction style 和选项是否支持。
ingest 和 L0 的关系
这个问题单独拎出来讲,因为它非常常见。
a. 普通写入 flush 基本先进 L0
普通写入路径:
text
Put
-> MemTable
-> Flush
-> L0
所以普通写入的 flush 文件一般先进 L0。
b. 普通 ingest 不一定进 L0
普通 ingest 路径:
text
External SST
-> RocksDB 选择安全 level
如果这个文件 range 很干净,它可能直接进入 L2、L3 或更底层。所以:
text
ingest 不是一定先到 L0
c. 但 ingest 也可能进 L0
如果这个文件和已有文件重叠严重,或者无法安全放到底层,它就可能落到 L0 或较高层。所以更准确的说法是:
text
普通 ingest 尝试放到最低的安全 level;
如果放不下去,才退到更高 level,极端情况下进 L0。
d. 判断是不是"干净 ingest"
一个 ingest 是否健康,可以看这几个点:
text
ingest 文件最后落到了哪个 level
是否增加了 L0 file count
是否触发了 memtable flush
是否造成 write stall
ingest 后 pending compaction bytes 是否上涨
如果每次 ingest 都进 L0,那通常说明:
text
key range 不够干净
与已有数据重叠太多
与 memtable 重叠
bottom level 没有合适空间或正在 compaction 冲突
fail_if_not_bottommost_level 有什么用?
有一个选项很适合排查 ingest 是否"真的干净":
cpp
ifo.fail_if_not_bottommost_level = true;
它的意思是:
如果这个文件不能被放到 bottommost level,就直接失败。
这在一些场景很有用,比如你做 bulk load,希望文件直接落到底层,避免后续 compaction。如果设置这个选项后 ingest 失败,说明:
text
这个文件 range 和现有 LSM 不够干净
不能安全地直接放到底层
这时候应该检查:
text
是否有重叠 key range
是否有 memtable overlap
是否有 L0/L1 重叠
是否有正在运行的 compaction 输出冲突
它不一定适合所有线上路径,但很适合验证导入策略是否合理。
ingest 为什么会导致写延迟抖动?
很多人以为 ingest 只是"挂一个文件",应该非常快,这个理解只对了一半。如果是非常干净的 SST,确实可能接近元数据操作。但 ingest 也可能带来几个明显开销。
1. 阻塞写入分配 sequence number
前面说过,ingest 需要分配 global sequence number。这个过程需要和前台写入建立严格顺序。所以它可能短暂阻塞写入。在单机 RocksDB 里,这可能只是一个小抖动。 但在在线系统里,一个 RocksDB 实例里可能承载很多数据分片,一个数据分片做 ingest,如果阻塞了整个 DB 的写入路径,影响就可能扩散到其他数据分片。
2. 触发 memtable flush
如果 ingest 文件和 memtable range 重叠,可能触发 flush。flush 本身会写 SST,也会消耗 IO。如果当时系统写入压力已经很高,这个 flush 会进一步放大延迟。
3. 增加 L0 或 compaction 压力
如果 ingest 文件没法放到低层,最后落到 L0 或高层,就会产生后续 compaction。表现可能是:
text
ingest 当下不慢
但之后 pending compaction bytes 上升
L0 file count 上升
write stall 增多
读延迟变差
所以 ingest 的影响不能只看 IngestExternalFile() 调用本身耗时,还要看后续 LSM 是否被它打乱。
ingest 和 compaction 的关系
ingest 的理想状态是:
text
文件 range 干净
直接放到低层
不产生太多后续 compaction
但实际经常是:
text
文件 range 不干净
只能放到 L0 / 较高 level
后续 compaction 慢慢消化
所以 ingest 和 compaction 关系很密切。可以这么理解:
text
ingest 是把一批已经排好序的数据接入 LSM
compaction 是后续维护 LSM 结构的清理工
- 如果 ingest 接入的位置好,compaction 压力小。
- 如果 ingest 接入的位置差,compaction 压力大。
为什么 key range 切分很重要?
假设你要导入 100GB 数据。一种做法是生成很少几个超大 SST:
text
file1: [a, z] 50GB
file2: [a, z] 50GB
这很可能很糟糕,因为每个文件的 range 都太宽。它们几乎和所有已有 level 都重叠。另一种做法是按 key range 切成多个更干净的文件:
text
file1: [a, c]
file2: [d, f]
file3: [g, i]
file4: [j, l]
...
这样每个文件只和局部范围相关。好处是:
text
更容易直接放入低层
单次 ingest 冲突更少
后续 compaction 范围更小
读写抖动更可控
所以 ingest 不是简单地"文件越大越好"。更合理的目标是:
text
文件大小适中
key range 清晰
和目标 DB 现有数据重叠尽量少
ingest 的文件大小怎么考虑?
没有一个固定答案。但一般要避免两个极端。
1. 文件太小
文件太小会导致:
text
SST 文件数量多
元数据压力大
open file / cache / index/filter 成本增加
compaction 文件选择更碎
2. 文件太大
文件太大会导致:
text
单次 ingest 时间长
单个文件 range 可能太宽
后续 compaction 粒度太大
失败重试成本高
3. 更合理的方向
通常应该让 ingest SST 的大小接近 RocksDB 正常 compaction 产出的 target file size。也就是参考:
text
target_file_size_base
target_file_size_multiplier
不要脱离目标 CF 的 level 文件大小规划。
常见选项怎么理解?
下面只讲最常用、最容易影响行为的选项。
move_files
cpp
ifo.move_files = true;
作用:
text
尽量 move 外部 SST,而不是 copy
优点:
text
减少大文件复制成本
ingest 更快
风险:
text
原文件生命周期必须交给 RocksDB
跨文件系统可能失败
权限和文件系统行为要确认
allow_blocking_flush
cpp
ifo.allow_blocking_flush = true / false;
作用:
text
如果 ingest 需要 flush memtable,是否允许阻塞等待 flush。
如果设为 false,遇到必须 flush 的情况可能直接失败。适合:
text
延迟敏感
可以失败重试
不希望 ingest 隐式触发长时间 flush
write_global_seqno
cpp
ifo.write_global_seqno = true / false;
作用:
text
是否把 global seqno 写回 SST 文件本身
影响:
text
true 更偏兼容性,但需要修改 SST 文件
false 更少随机写,依赖 MANIFEST 记录
verify_checksums_before_ingest
cpp
ifo.verify_checksums_before_ingest = true;
作用:
text
ingest 前校验文件内容 checksum
优点:
text
更早发现损坏文件
代价:
text
需要额外读文件
大文件 ingest 变慢
生产环境是否开启,要看数据来源可靠性和延迟要求。
verify_file_checksum
作用类似:
text
校验文件级 checksum
如果你跨机器传输 SST,或者从外部系统生成 SST,建议认真考虑校验。
fail_if_not_bottommost_level
cpp
ifo.fail_if_not_bottommost_level = true;
作用:
text
如果不能放到底层,就失败。
适合:
text
验证 bulk load 是否足够干净
避免导入后产生大量后续 compaction
ingest_behind
cpp
ifo.ingest_behind = true;
作用:
text
历史数据回填,不覆盖已有更新数据。
限制很多,不能当作普通 ingest 的替代。
ingest 和 Delete / Tombstone 的关系
普通 ingest 的 SST 里可以包含:
text
Put
Delete
Merge
Range deletion
但实际能不能用、怎么用,要看:
text
SstFileWriter 支持
目标 DB 配置
merge operator
range tombstone 支持
RocksDB 具体版本
这里重点说两个常见点。
1. SST 里写 Delete
如果外部 SST 里包含:
text
delete user:1
普通 ingest 后,它就像一批新的删除操作进入 DB。逻辑上,它可以删除旧值。但物理空间不会马上释放,还是要等 compaction。
2. SST 里写 Merge
如果 SST 里包含 Merge operand,那么目标 DB 必须有兼容的 merge operator。否则 RocksDB 无法解释这些 merge 记录。所以跨系统 ingest 时,Merge 是一个高风险点。
如何判断 ingest 是否健康?
建议看这些指标。
text
ingest 调用耗时
ingest 文件落入的 level
L0 file count
pending compaction bytes
write stall 次数和时长
flush 次数和耗时
compaction read/write bytes
block cache 命中率变化
磁盘 util / iowait
如果有日志,也要看:
text
IngestExternalFile
picked level
flush memtable
write stall
compaction pending
典型问题一:为什么 ingest 后 L0 爆了?
常见原因:
text
ingest 文件 range 太宽
和已有 L0/L1/L2 文件重叠太多
和 memtable 重叠导致 flush
bottom level 无法安全接入
正在运行的 compaction 阻挡了目标 level
解决思路:
text
按 key range 切更细
减少和现有数据重叠
在低峰期做 ingest
必要时提前 flush / compact 目标范围
用 fail_if_not_bottommost_level 做验证
观察 ingest 后文件实际进入哪个 level
典型问题二:为什么 ingest 会卡住写入?
原因可能是:
text
分配 global seqno 时需要短暂阻塞写入
需要 blocking flush
move/copy 大文件耗时
校验 checksum 需要读完整文件
文件系统 rename/link/copy 慢
DB mutex / manifest 写入竞争
排查方向:
text
看 ingest 耗时分布
看是否触发 flush
看是否开启 checksum 校验
看 move_files 是否实际成功
看 MANIFEST 写入是否慢
看磁盘 IO 是否打满
典型问题三:为什么 ingest 很快,但后面系统变慢?
这通常说明 ingest 本身只是把文件接进来了,但接入位置不好。
比如:
text
文件进了 L0
L0 file count 上升
pending compaction bytes 上升
compaction 开始追债
前台写入被 slowdown / stall
读路径要查更多 L0 文件
所以 ingest 评估不能只看:
text
IngestExternalFile() 返回耗时
还要看之后几分钟甚至更长时间的 LSM 状态。
生产实践建议
1. 先设计 key range,再生成 SST
不要先把数据随便塞进几个大 SST。应该先考虑:
text
目标 DB 当前 key 分布
level 文件 range
导入数据 range
单文件大小
是否会和热写 range 重叠
2. 尽量让 ingest 文件 range 干净
理想情况:
text
待 ingest 文件和已有数据不重叠
或者只在很小范围内重叠
这样更容易放到低层。
3. 控制单次 ingest 批量
不要一次性 ingest 太多大文件,把 manifest、文件系统、compaction 一起打爆。更稳的方式是:
text
分批 ingest
每批后观察 L0 / pending compaction
必要时限速
低峰期执行
4. 明确是否允许覆盖
导入前先明确语义:
text
要覆盖已有数据:
普通 ingest
只补历史缺口:
ingest_behind,但要确认 compaction style 和 DB 选项
不要用错语义。
5. 对跨机器传输的 SST 做校验
如果 SST 是从别的机器传来的,建议至少有一层校验:
text
传输层 checksum
文件级 checksum
RocksDB ingest 前 checksum verify
不要为了省一点 ingest 时间,导入损坏文件。
6. 关注 ingest 后的后效应
每次 ingest 后至少看:
text
L0 file count 是否上升
pending compaction bytes 是否上升
write stall 是否增加
读延迟是否变化
compaction 是否追得上
如果这些指标持续恶化,说明 ingest 只是把问题延后了。
总结
RocksDB ingest 可以这样理解:
- 普通写入走 WAL + MemTable + Flush + L0;
- ingest 是先在线下生成 SST,再把 SST 接入 RocksDB;
- 它适合批量导入、数据迁移、快照恢复、历史数据回填;
- 外部 SST 的 key 必须按 comparator 严格递增;
- comparator、table format、compression、checksum 等必须和目标 DB 兼容;
- 普通 ingest 会给文件分配 global sequence number,所以可能短暂阻塞写入;
- 如果 SST range 和 memtable 重叠,ingest 可能触发 blocking flush;
- 普通 ingest 不是一定进 L0,而是尝试放到安全的最低 level;
- 如果 key range 不干净,ingest 仍然可能进 L0,并制造后续 compaction 压力;
ingest_behind是历史数据回填语义,不覆盖已有新数据,限制很多,不能当普通 ingest 用;- 在 7.x~9.x 范围内,通用方案应以
SstFileWriter生成外部 SST 为主; - 9.5+ 的 DB-generated SST ingest 是额外实验能力,不应倒推到整个 7.x~9.x 范围;
- 判断 ingest 是否健康,要看 ingest 耗时,也要看后续 L0、pending compaction bytes、write stall 和读写延迟。
一句话:
IngestExternalFile 是 RocksDB 给批量有序数据开的"快速入口"。
入口本身很快,但能不能真正快,取决于这个 SST 能不能以干净的 key range 安全地接入 LSM。