基于本文回答
0
评论

讲讲 Doris 中主键模型的读时合并(Merge-on-Read, MoR)

在 Apache Doris 中,Unique Key(主键)模型是为了满足用户对于数据的更新(Upsert)删除(Delete)需求而设计的。

在 Doris 的演进过程中,Unique Key 模型经历了两种底层的实现方式:传统的 读时合并(Merge-on-Read, 简称 MoR) 和后来引入的 写时合并(Merge-on-Write, 简称 MoW)

下面为你详细解析 Doris 主键模型中的 读时合并(MoR) 机制。


1. 什么是读时合并(Merge-on-Read, MoR)?

顾名思义,“读时合并”的核心思想是:把数据合并(去重、更新)的压力放在“读取(查询)”阶段,而让“写入”阶段尽可能轻量和快速。

在 MoR 模式下,当有新数据写入或者旧数据更新时,Doris 不会在写入时去底层数据文件中寻找旧数据并覆盖它,而是将新数据作为一个新的版本(Version)直接追加写入(Append-only)。只有当用户发起查询(Read)时,Doris 才会把相同主键的多个版本数据读取出来,在内存中进行动态合并,最终只返回最新版本的数据。

2. MoR 的工作原理详解

我们可以从 写入读取后台清理 三个阶段来理解 MoR 是如何工作的。

A. 写入阶段 (Write)

  • 追加写入(Append-only): 所有的 INSERTUPDATEDELETE 操作,在存储层都被视为“追加写入”。
  • 版本号(Version): Doris 为每一次导入批次分配一个递增的版本号。例如,第一次写入版本号是 V1,第二次更新是 V2。
  • 删除标志(Delete Sign): 如果是 DELETE 操作,Doris 并不是物理删除那行数据,而是写入一条主键相同,但隐藏列 __DORIS_DELETE_SIGN__ 被标记为 1 的数据(相当于墓碑机制 Tombstone)。

例子:
假设有一张用户表,主键是 user_id

  1. 写入: (user_id=1, name='Alice', city='Beijing') -> 存入磁盘,版本号 V1。
  2. 更新: Alice 搬到了上海,写入 (user_id=1, name='Alice', city='Shanghai') -> 存入磁盘,版本号 V2。
    此时磁盘上同时存在 V1 和 V2 两条 user_id=1 的记录。

B. 读取阶段 (Read) - 【核心性能瓶颈所在】

当用户执行 SELECT * FROM table WHERE user_id = 1 时:

  1. Doris 的扫描节点(Scanner)会把磁盘上 V1 和 V2 两个版本的数据都读入内存。
  2. 在内存中,按照主键 user_id 进行多路归并排序(Merge Sort)。
  3. 发现两条主键相同的记录,比较它们的版本号。
  4. 保留版本号更大的 V2(上海),丢弃 V1(北京)。
  5. 如果最新版本的数据带有删除标志(Delete Sign = 1),则将其过滤掉,不返回给用户。

C. 后台清理阶段 (Compaction)

如果数据不断更新,磁盘上的版本会越来越多,导致查询时需要合并的数据量呈指数级上升,查询极其缓慢。
为了解决这个问题,Doris 后台会不断运行 Compaction(数据压缩/合并) 任务:

  • Cumulative Compaction(增量合并): 将最近产生的小版本合并成一个较大的版本。
  • Base Compaction(全量合并): 将增量合并产生的大版本与基线数据合并。
  • 在 Compaction 过程中,Doris 也会在后台执行主键去重,物理删除掉旧版本和被标记为删除的数据,从而降低后续查询的合并压力。

3. MoR 的优缺点分析

优点:

  1. 写入性能极高: 因为是纯追加写入(Append-only),无需在写入时读取和比对历史数据,不需要加锁等待,写入吞吐量非常大。
  2. 写资源消耗低: 写入过程非常轻量,CPU 和内存消耗少,适合极高并发的高频实时写入场景。

缺点:

  1. 查询性能较差(特别是数据刚写入时): 因为查询时需要在内存中进行多路归并排序和去重,这非常消耗 CPU,导致查询延迟较高。
  2. 谓词下推受限: 对于非主键列的过滤条件(WHERE 条件),很多时候无法在扫描磁盘数据时就过滤掉,必须等数据合并出最新版本后才能进行过滤(否则可能会用旧版本的数据做了错误的过滤)。这导致数据读取量大增。
  3. 性能极度依赖 Compaction: 如果写入过于频繁,导致后台 Compaction 跟不上,积压的版本过多(Version Too Many),查询性能会呈断崖式下跌,甚至报出 -235 错误导致写入失败。

4. 为什么 Doris 后来引入了写时合并(Merge-on-Write, MoW)?

正是因为 MoR 在高频更新 + 极速分析的场景下,查询性能无法满足用户的极致要求,Doris 从 1.2 版本开始引入了 Merge-on-Write (MoW),并在之后的版本中将其作为 Unique Key 模型的默认选项

MoW 与 MoR 的区别简述:

  • MoW(写时合并): 写入数据时,系统会去寻找该主键旧版本所在的位置,并通过 Bitmap(Delete Bitmap)将旧数据标记为删除。
  • 查询时: 查询引擎只需要根据 Delete Bitmap 过滤掉被标记为删除的行即可,完全不需要在内存中做任何主键的比对和合并
  • 结果: 稍微牺牲了一点写入速度(因为写入时要维护 Bitmap),但换来了和明细模型(Duplicate)几乎一样快的极速查询性能,并且完美支持各种谓词下推。

5. 总结建议

  • 如果你使用的 Doris 版本较老(1.2 以前),主键模型默认就是 MoR。你需要密切关注后台的 Compaction 状态。
  • 如果你使用的是 Doris 1.2 及以上版本(尤其是 2.x 版本),强烈建议使用默认的 MoW(写时合并)机制,它在 95% 以上的场景中都能提供比 MoR 好得多的综合体验。
  • 什么情况下还在用 MoR? 只有在极少数“写请求极其巨大、更新频率极高,但查询频率很低、对查询延迟要求不高”的极端场景下,MoR 的高吞吐写入优势才有发挥空间。
右滑查看面试常问