基于本文回答
0
评论

讲讲Doris 的数据版本控制(Version)?

Apache Doris 的数据版本控制(Data Versioning)是其底层存储架构的核心机制之一。它不仅决定了数据如何写入和读取,还支撑了 Doris 的 MVCC(多版本并发控制)事务的 ACID 特性以及高效的数据更新与合并(Compaction)

下面为你详细拆解 Doris 的数据版本控制机制:


1. 什么是版本(Version)?

在 Doris 中,版本是对数据变更(导入、更新、删除)的逻辑抽象

  • 版本号生成:版本号是一个单调递增的整数。每次导入任务(Load/Insert)在提交时,Doris 的 FE(Frontend)节点都会为受影响的 Tablet(数据分片)分配一个递增的版本号。
  • 版本与 Rowset 的对应关系:在 BE(Backend)存储节点上,每次成功的写入都会生成一个新的数据文件集合,称为 Rowset。每一个 Rowset 都会绑定一个版本范围,用 [start_version, end_version] 表示。

2. 版本的生命周期(从写入到读取)

A. 写入过程(单行版本 [V, V]

  1. 用户发起一次导入任务(如 Flink 写入或 Stream Load)。
  2. FE 开启一个事务(Transaction)。
  3. BE 将数据写入本地磁盘,生成一个 Rowset。此时这个 Rowset 的版本是不确定的。
  4. 导入完成,FE 提交事务,并为该批次数据分配一个具体的可见版本号(假设为 V10)。
  5. BE 将该 Rowset 的版本标记为 [10, 10]。这代表这个文件只包含了版本 10 的数据。

B. 读取过程(MVCC 与可见性)

Doris 支持 MVCC(多版本并发控制),这意味着读写不互斥。

  1. 当用户发起查询时,FE 会确定当前该表/分区的最大连续可见版本号(假设为 V15)。
  2. FE 将查询请求和版本号 V15 发送给 BE。
  3. BE 在读取数据时,只会读取版本号 15\le 15 的 Rowset。
  4. 如果此时后台有新的事务提交产生了 [16, 16],或者有未提交的事务,查询都会直接忽略它们。这保证了查询的快照隔离性(Snapshot Isolation)

C. 合并过程(Compaction 与版本范围)

如果每次写入都产生一个 [V, V] 的小文件,查询时就需要扫描大量文件,性能会急剧下降。因此 Doris 会在后台进行 Compaction(数据压缩合并)

  • 假设当前有三个连续的 Rowset:[10, 10][11, 11][12, 12]
  • 后台 Compaction 线程会将这三个 Rowset 读取出来,进行排序、合并(如果是聚合模型则进行预聚合),生成一个新的大的 Rowset。
  • 这个新的 Rowset 的版本会被标记为 [10, 12]
  • 合并完成后,旧的三个单版本 Rowset 会被标记为废弃并在稍后被物理删除。
  • 查询时的匹配逻辑:查询 V15 时,BE 发现 [10, 12] 这个区间被完全包含在 15 以内,就会直接读取这个大文件。

3. 版本控制在不同数据模型中的表现

Doris 有三种主要的数据模型,版本机制在其中的作用略有不同:

① 明细模型 (Duplicate) 和 聚合模型 (Aggregate)

  • 新版本的数据直接追加。
  • 对于 Aggregate 模型,Compaction 过程中会将不同版本中具有相同 Key 的数据进行聚合计算(如 SUM、MAX),从而减少数据量。

② 唯一键模型 (Unique Key) —— 版本控制的核心难点

在 Unique Key 模型中,Doris 需要保证相同 Key 的数据只有最新的一条有效。Doris 提供了两种基于版本的实现方式:

  • Merge-on-Read (MoR - 读时合并)
    • 新老版本数据同时存在磁盘上(比如版本 10 有 Key_A,版本 15 也写入了 Key_A)。
    • 查询时,BE 会把版本 10 和版本 15 的数据都读出来,在内存中比较版本号,只保留版本号最大的数据返回给用户。
    • 特点:写入极快,但查询由于要合并大量历史版本,性能会有损耗。
  • Merge-on-Write (MoW - 写时合并,Doris 1.2+ 默认)
    • 为了加速查询,Doris 引入了 Delete Bitmap(删除位图)
    • 当写入新版本(如 V15)的 Key_A 时,系统会去查找 Key_A 以前在哪个版本(假设在 V10)。
    • 系统会在 V15 对应的数据结构中,在 V10 的 Delete Bitmap 里把 Key_A 对应的行标记为“已删除”。
    • 查询时,不需要在内存中动态比较版本了,直接根据 Delete Bitmap 过滤掉旧版本数据即可。
    • 特点:牺牲了一点点写入性能(因为要查历史版本),换取了极致的查询性能。

4. 常见的版本相关问题与调优

在实际使用 Doris 时,开发者最常遇到的问题就是版本堆积(Too Many Versions)

  • 报错现象:写入失败,报错 errCode = 2, detailMessage = too many versions. tablet_id: xxx...(通常也被称为 -235 错误)。
  • 根本原因:用户的写入频率过高(例如每秒用 Flink 写几十次,或者代码里用 INSERT INTO ... VALUES 一条一条插),导致产生了大量 [V, V] 的小文件。后台的 Compaction 线程合并速度赶不上新版本的生成速度,为了保护系统不崩溃(打开过多文件句柄导致 OOM 或 IO 阻塞),Doris 强行拒绝了新的写入。
  • 解决/调优方案
    1. 降低写入频率,增大批次:这是最根本的解决办法。将 Flink 的 Sink 攒批时间(sink.buffer-flush.interval)调整到 10 秒或更大。
    2. 调整 Compaction 参数:如果硬件资源充足,可以在 BE 配置(be.conf)中增加 Compaction 的线程数,加快合并速度(如增加 compaction_task_num_per_disk)。
    3. 合理设计表结构:避免单表的分区和分桶数量过多。因为版本是基于 Tablet(分片)维度的,分片越多,产生的小文件就呈现乘数级增长。

总结

Doris 的数据版本控制(Version)是一个将 写入事务、底层存储文件(Rowset)、后台合并(Compaction)和多版本并发控制(MVCC) 串联起来的核心枢纽。理解了 [start_version, end_version] 的概念,就能深刻理解 Doris 是如何实现高并发读取和高效数据更新的。

右滑查看面试常问