预计阅读时间: 40 分钟 前置阅读: doc-06 (Rowset/Segment) 下一次阅读: doc-08 (索引)
1. Tablet 版本模型
版本的核心概念
Tablet 使用单调递增的 int64 版本号来跟踪数据的修改。每次写入产生一个新 Rowset, 其版本号 = 写入前 Tablet.max_version + 1。
Tablet 初始状态:
max_version = 0, visible_version = 0, rowsets = []
第一次写入 (INSERT 100 行):
DeltaWriter → Rowset [version=1, rows=[1..100]]
Tablet.rowsets = [Rowset(v1)]
max_version = 1, visible_version = 0 (还未 Publish)
Publish:
→ Tablet::publish_version(1)
visible_version = 1, Rowset(v1) 变为 VISIBLE
第二次写入 (INSERT 50 行):
Rowset [version=2, rows=[101..150]]
Publish → visible_version = 2
查询时:
所有 version ≤ visible_version 的 Rowset 都可以被读取
需要读: Rowset(v1) + Rowset(v2)
关键概念
| 术语 | 含义 | 源码位置 |
|---|---|---|
max_version | Tablet 的最新版本号 | Tablet::_max_version |
visible_version | 已 Publish 的最新版本 (查询可见) | Tablet::_visible_version |
next_version | 下一个写入的版本号 (=max_version + 1) | Tablet::_next_version |
PublishVersion | FE → BE Publish RPC 指定的版本 | TPublishVersionRequest Thrift struct |
源码导航
| 文件 | 关键类/方法 | 职责 |
|---|---|---|
be/src/storage/tablet.h | publish_version(), max_version() | 版本管理 |
be/src/storage/compaction/compaction.h | CompactionMixin::execute_compact() | Compaction 实现 |
be/src/storage/compaction/cumulative_compaction.h | CumulativeCompaction | 增量 Compaction |
be/src/storage/compaction/base_compaction.h | BaseCompaction | 全量 Compaction |
be/src/storage/compaction/cumulative_compaction_policy.cpp | CumulativeCompactionPolicy | Compaction 策略 |
be/src/storage/compaction/compaction_permit_limiter.h | CompactionPermitLimiter | 并发控制 |
be/src/agent/task_worker_pool.cpp | PublishVersionWorkerPool | Publish 任务执行(BE Agent) |
fe/fe-core/.../transaction/PublishVersionDaemon.java | PublishVersionDaemon | Publish 守护线程(FE) |

2. Compaction 类型
全部类型
| 类型 | 触发条件 | 合并哪些 Rowset | 源码 |
|---|---|---|---|
| Cumulative Compaction | 持续后台运行 | 最后 N 个 Delta Rowset (N = cumulative_compaction_num_singleton_deltas, 默认5) | cumulative_compaction.cpp |
| Base Compaction | 低峰时段执行 | 所有 Rowset (全部合并为1个) | base_compaction.cpp |
| Full Compaction | 用户手动触发 (Schema Change) | 所有 Rowset | compaction.cpp |
| Cold Compaction | 冷数据 (cooldown 迁移后) | 冷 Rowset → 合并后写入对象存储 | cold_data_compaction.cpp |
Cumulative vs Base 对比
Cumulative Compaction (频繁):
Before: [Base: 0-9] + [D1: 10] + [D2: 11] + [D3: 12] + [D4: 13] + [D5: 14]
合并最后 5 个 Delta:
After: [Base: 0-9] + [Merged: 10-14]
Base Compaction (低峰):
Before: [Base: 0-9] + [Merged: 10-14]
合并所有:
After: [New Base: 0-14]
3. Compaction 调度
调度流程
1. Pick Candidates
CumulativeCompactionPolicy::pick_candidate(rowsets)
→ 选择最后 N 个连续的 Delta Rowset
2. Priority Calculation
→ Priority = f(num_rows, num_rowsets, compaction_size)
→ 行数越多, Rowset 越多 → Priority 越高
3. Permit Limiter (并发控制)
→ CompactionPermitLimiter::request()
→ 每个 BE 最多 N_be_compaction 个 Compaction 并发 (默认 5)
→ 如果 permit 不足 → 入队列等待
4. Execute Merge
→ CompactionMerger::merge(rowsets, output_rowset)
→ 逐列读取每个 Rowset 的 Segment → Merge → 写新 Rowset
5. Commit (原子提交)
→ Tablet::commit_compaction(output_rowset, input_rowsets)
→ 原子操作: 删除旧 Rowset + 添加新 Rowset
4. Merge 过程详解
内部执行
CompactionMerger::merge()
→ for each Column (dt, status, amount, city, ...):
→ for each Input Rowset:
→ read Column Page →
→ MergeSort (按主键排序)
→ 遇到同 key: 选择最新版本的行 (version大的)
→ 遇到 DeleteBitmap 标记的行 → 丢弃
→ write Merged Column Page → SegmentWriter
→ SegmentWriter::finalize()
→ 生成新的 Rowset
GC 时机
Compaction 生成新 Rowset 后, 旧的 Rowset 被标记为 "删除待 GC"。GC 在以下时机清理:
- 后台 GC 线程定期执行 (默认每 60s)
- 删除时检查: 是否还有其他查询引用旧 Rowset (Reference Counting)
- 如果有引用 → 延迟 GC (直到引用释放)
5. 版本可见性与 DeleteBitmap
删除的实现方式
Doris 不真正删除磁盘数据 (直到 Compaction), 而是通过 DeleteBitmap 标记 "已删除的行":
写入: Rowset v1: rows {1: alive, 2: alive, 3: alive}
DELETE: 生成 DeleteBitmap v2: {2 → deleted, 3 → deleted}
查询: SegmentReader 读取 v1 的 rows{1,2,3} → DeleteBitmap 过滤 {2,3} → 返回 {1}
Compaction: v1 和 v2 合并 → 新 Rowset v3: rows{1} (物理删除 2,3)
DeleteBitmap 的来源
| 来源 | 场景 |
|---|---|
| DELETE FROM 语句 | 用户显式删除 |
| Unique Key 模型 (MoW) | 新 key 替换旧 key → 旧行 marked deleted |
| Load 去重 | 同名 Label 的重试 → 第一次写入的行 marked deleted |
6. Vertical Compaction / Segment Compaction 优化
| 优化 | 原理 | 效果 |
|---|---|---|
| Vertical Compaction | 按列组 (Column Group) 分离执行合并: 热点列组先合并, 减少内存峰值 | 内存降低 30-50% |
| Segment Compaction | Flush 时直接在内存中合并多个小 Segment 为 1 个 (避免小 Rowset 进入 Compaction 队列) | 减少 Compaction 调度次数 |
| Ordered Compaction | 对于 key 有序的数据, 跳过 MergeSort (因为 Rowset 之间天然有序) | CPU 降低 20-30% |
7. 常见问题 / 面试题
Q1: Cumulative Compaction 的 "最后 N 个" 为什么是 5 (默认)? A: 合并更多 Delta → 减少 Rowset 数量更快, 但每次合并的成本 (IO+CPU) 也更高。N=5 是在性能基准测试中平衡成本和收益得出的经验值。写入吞吐非常高的场景可调大到 7-10。
Q2: 为什么需要两种 Compaction (Cumulative + Base)? A: Cumulative 是"增量合并", 只合并最近的 Delta, 低成本但不够彻底; Base 是"全量合并", 将所有 Rowset 合并为 1 个, 彻底消除碎片但成本高。两者互补, Cumulative 保证 Rowset 数量不爆炸, Base 在低峰时彻底优化。
Q3: 如果一个查询正在读一个 Rowset, 同时 Compaction 要删除它, 会怎么处理?
A: Compaction 提交时会检查 Rowset 的引用计数 (RefCount)。如果查询正在读 → RefCount > 0 → Compaction 将旧 Rowset 加入 _stale_rs_meta 列表但不释放文件 → 查询结束后 RefCount 为 0 → GC 线程最终清理文件。
Q4: Compaction 会影响写入性能吗? A: 通过以下机制控制: (1) CompactionPermitLimiter 限制并发; (2) Compaction 使用低优先级的 IO 调度 (避免与用户查询的 IO 竞争); (3) Compaction 在数据写入完成后异步执行, 不阻塞写入路径。

下一步
- 索引详解: doc-08-storage-index.md——5种存储索引的原理和代码