Doris 实战与架构

分析型数据库 / 源码阅读 / LESSON 07

存储引擎——Compaction 与版本管理

理解版本链、Publish、Compaction 与数据可见性如何共同维持写入后的读一致性。

阅读时间
40 分钟
学习路径
Doris 实战与架构
内容来源
Doris 深度笔记

预计阅读时间: 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_versionTablet 的最新版本号Tablet::_max_version
visible_version已 Publish 的最新版本 (查询可见)Tablet::_visible_version
next_version下一个写入的版本号 (=max_version + 1)Tablet::_next_version
PublishVersionFE → BE Publish RPC 指定的版本TPublishVersionRequest Thrift struct

源码导航

文件关键类/方法职责
be/src/storage/tablet.hpublish_version(), max_version()版本管理
be/src/storage/compaction/compaction.hCompactionMixin::execute_compact()Compaction 实现
be/src/storage/compaction/cumulative_compaction.hCumulativeCompaction增量 Compaction
be/src/storage/compaction/base_compaction.hBaseCompaction全量 Compaction
be/src/storage/compaction/cumulative_compaction_policy.cppCumulativeCompactionPolicyCompaction 策略
be/src/storage/compaction/compaction_permit_limiter.hCompactionPermitLimiter并发控制
be/src/agent/task_worker_pool.cppPublishVersionWorkerPoolPublish 任务执行(BE Agent)
fe/fe-core/.../transaction/PublishVersionDaemon.javaPublishVersionDaemonPublish 守护线程(FE)

存储引擎——Compaction 与版本管理 图 01

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)所有 Rowsetcompaction.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 在以下时机清理:

  1. 后台 GC 线程定期执行 (默认每 60s)
  2. 删除时检查: 是否还有其他查询引用旧 Rowset (Reference Counting)
  3. 如果有引用 → 延迟 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 CompactionFlush 时直接在内存中合并多个小 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 在数据写入完成后异步执行, 不阻塞写入路径。


Compaction 流程 & Version 演进

下一步