讲讲 Doris 中主键模型的写时合并(Merge-on-Write, MoW)
在 Apache Doris 中,主键模型(Unique Key Model) 主要用于满足需要更新和去重的业务场景(例如 CDC 数据同步、订单状态更新等)。
在 Doris 1.2 版本之前,主键模型默认采用的是 读时合并(Merge-on-Read, MoR) 机制;而从 1.2 版本开始引入了 写时合并(Merge-on-Write, MoW),并在 Doris 2.0 版本中将其作为主键模型的默认实现。
MoW 的引入是 Doris 在实时更新场景下的一次重大架构升级。下面我将为你详细剖析 Doris 中主键模型的写时合并(MoW)机制。
1. 为什么需要写时合并(MoW)?
要理解 MoW,首先要了解之前的 读时合并(MoR) 的痛点。
- 读时合并(MoR)的原理:写入数据时,Doris 只是简单地将新数据(无论是插入还是更新)追加写入到底层。当用户发起查询(Read)时,系统需要把所有历史版本的数据读取出来,在内存中根据主键进行比较,保留版本号最大的那一行,然后再进行聚合、过滤等操作。
- MoR 的痛点:
- 查询性能差:读取时需要做大量的比较和合并操作,极其消耗 CPU。
- 谓词下推失效:因为必须先合并才能知道哪行是最终数据,很多索引(如 ZoneMap)和条件过滤(WHERE)无法下推到存储层,导致扫描了大量无用数据。
为了解决查询慢的问题,Doris 引入了 写时合并(MoW)。核心思想是:把合并的代价从“查询端”转移到“写入端”,从而换取极致的查询性能。
2. 写时合并(MoW)的核心机制
MoW 的底层实现主要依赖两个关键技术:Delete Bitmap(删除位图) 和 Primary Key Index(主键索引)。
A. 核心数据结构:Delete Bitmap
在 MoW 模式下,对于每一个数据分片(Tablet)中的每一个数据文件(Segment),Doris 都会维护一个与之对应的 Delete Bitmap。
- 作用:这个 Bitmap 记录了当前数据文件中,哪些行(Row ID)已经被标记为失效/删除(即被后来的新数据覆盖了)。
- Bitmap 中的位如果为
1,表示该行已失效;为0表示该行有效。
B. 写入流程(Write)
当新的一批数据(Rowset)写入 Doris 时,主要发生以下步骤:
- 追加写入:新数据首先依然是顺序追加写入到新的 Segment 文件中。
- 主键冲突检测:系统利用主键索引(Primary Key Index) 去历史的数据文件中查找,看这些新写入的主键是否在以前出现过。
- 更新 Delete Bitmap:如果在历史文件(旧 Segment)中找到了相同的主键,系统就会定位到那条旧数据的 Row ID,并将对应的历史 Delete Bitmap 中的该位设置为
1(标记为删除)。 - 可见性:当写入事务提交时,新的数据文件和修改后的 Delete Bitmap 会同时生效。
C. 查询流程(Read)
因为写入时已经做好了标记,查询时的流程变得极其简单和高效:
- 读取数据文件(Segment)。
- 读取对应的 Delete Bitmap。
- 按位过滤:直接把 Delete Bitmap 中为
1的行过滤掉。 - 剩下的数据就是最终的有效数据,直接返回。
查询优势:完全不需要在内存中做主键比较和合并!这使得 MoW 模式下的主键表,查询性能几乎逼近明细模型(Duplicate Key),并且完美支持所有的谓词下推和二级索引。
D. Compaction(后台合并)
随着不断写入,被标记为删除的历史数据会越来越多,占用磁盘空间。Doris 的后台会定期触发 Compaction 任务:
- 读取多个小文件。
- 根据 Delete Bitmap 物理剔除掉那些被标记为删除的行。
- 生成一个干净的大文件,并释放磁盘空间。
3. MoW 的优缺点分析
优点(极大的提升):
- 查询性能极速提升:相比于 MoR 模式,MoW 模式下的查询性能通常有 3倍到 10倍 的提升(甚至更高)。
- 全面支持谓词下推:WHERE 条件可以推到存储层,利用 ZoneMap、BloomFilter 等索引直接过滤数据,大幅减少 IO。
- 更稳定的查询延迟:MoR 模式下,如果后台 Compaction 不及时,查询会非常慢(因为积累的版本多)。MoW 模式下,查询性能对 Compaction 的依赖大大降低,延迟非常稳定。
缺点(不可避免的代价):
- 写入性能略有损耗:因为写入时需要查询主键索引并更新 Delete Bitmap,所以写入速度会比 MoR 略慢,CPU 和内存消耗在写入端会略高。
- 内存开销增加:为了保证写入速度,Doris 会将主键索引缓存在内存中(通常是 Block 级别的索引或者持久化的 B+ 树),这会占用一定的节点内存。
(注:Doris 社区经过多次优化,目前 MoW 的写入性能损耗已经控制在非常小的范围内,完全能满足绝大多数高并发实时写入的要求。)
4. 如何在 Doris 中使用 MoW?
在 Doris 2.0 及更高版本中,创建 Unique Key 表时,默认就已经开启了 MoW。
如果你使用的是 Doris 1.2 版本,或者需要显式声明,可以通过 PROPERTIES 中的 enable_unique_key_merge_on_write 参数来控制:
CREATE TABLE users (
user_id INT,
user_name VARCHAR(50),
age INT,
update_time DATETIME
)
UNIQUE KEY(user_id) -- 声明为主键模型
DISTRIBUTED BY HASH(user_id) BUCKETS 10
PROPERTIES (
"enable_unique_key_merge_on_write" = "true" -- 开启写时合并 (2.0+ 默认开启)
);
总结
Doris 的写时合并(Merge-on-Write)是典型的“空间和写入时间换取读取时间”的设计。通过引入主键索引和 Delete Bitmap,它成功地将复杂的合并操作从用户查询的关键路径上剥离,放到了数据摄入(Ingestion)阶段。这使得 Doris 在面对 MySQL/Oracle CDC 实时同步、实时大屏更新等场景时,依然能够提供毫秒级的极速分析体验。