基于本文回答

播面 播面

文图音视,全方位拆解八股文
0
评论

什么是 Doris 的数据版本控制(Version)?当数据被不断导入时,Doris 的底层物理数据片(Segment)是如何产生和递增的?

Apache Doris 的数据版本控制(Version)和底层物理数据片(Segment)的设计,是其能够支持高频高并发导入、秒级即时查询(Real-time OLAP)以及高可用性的核心基石。

下面我们将分两部分,深入解析 Doris 的数据版本控制机制,以及在持续导入数据时,底层的 Segment 是如何产生和递增的。


第一部分:什么是 Doris 的数据版本控制(Version)?

Doris 的数据版本控制是基于 MVCC(Multi-Version Concurrency Control,多版本并发控制) 机制实现的。

在 Doris 中,数据并不是直接修改覆盖的,而是通过“追加写(Append-only)+ 多版本标记”的方式来管理的。

1. 核心概念

  • Tablet(数据分片):Doris 表的物理分片(按 Bucket 划分)。Version 实际上是绑定在每一个 Tablet 上的。
  • Rowset(数据批次):每一次导入(Transaction)成功后,在 Tablet 内部生成的一个逻辑数据集合。一个 Rowset 代表了一个特定的 Version
  • Version(版本号):一个由两个数字组成的区间 [Start, End](例如 [10, 10])。
    • 当版本未合并时,Start 等于 End(如 [10, 10])。
    • 当发生数据合并(Compaction)后,多个版本会融合成一个大区间(如 [0, 10])。

2. 版本是如何递增的?

  1. 初始状态:一个新创建的 Tablet,其初始版本通常是 [0, 1](Schema 占用的基础版本)。
  2. 事务提交:每一次导入任务(Stream Load, Broker Load 等)都是一个事务。
  3. 版本递增:当事务成功提交(Commit)时,Frontend (FE) 会为该导入分发一个全局递增的事务 ID(Txn ID),并在对应的 Tablet 上生成一个新的 Version。
    • 例如,前一个版本是 10,新导入成功后,就会生成一个 Version 为 [11, 11] 的新 Rowset。
  4. 读取可见性:查询请求到来时,FE 会获取当前最新的有效版本号(例如 11),并在 BE 端只读取版本号 11\le 11 的数据,大于该版本的数据对当前查询不可见。这保证了读写不冲突

第二部分:当数据不断导入时,底层 Segment 是如何产生和递增的?

在物理存储上,Doris 的数据最终是以 .dat 后缀的 Segment(段文件) 格式存在于 BE 节点的磁盘上的。

我们要理清三者的包含关系:Tablet \rightarrow Rowset \rightarrow Segment
一个 Tablet 包含多个 Rowset(版本),一个 Rowset 包含多个 Segment 文件。

下面是持续导入数据时,Segment 的产生和递增过程:

1. 内存缓冲与 Flush 机制(Segment 的诞生)

当发起一次导入任务时:

  1. 写入 MemTable:数据首先写入 BE 节点的内存缓冲区(MemTable)。
  2. 触发 Flush:当 MemTable 达到限制(默认限制如 100MB 或者是特定的行数),或者单次导入任务结束时,内存中的数据会被排序、压缩并刷写(Flush)到磁盘上。
  3. 生成 Segment:每次 Flush 都会在磁盘上生成一个物理文件,命名格式通常为:${tablet_id}_${rowset_id}_${segment_index}.dat
    • 例如:10001_2_0.dat(Tablet ID 是 10001,Rowset ID 是 2,第 0 个 Segment)。

2. 单次导入产生多个 Segment 的场景

如果一次导入的数据量非常大(例如 1GB):

  • 内存的 MemTable 会被多次写满并刷盘。
  • 这单次导入(同一版本)就会生成多个 Segment 文件:
    • 10001_2_0.dat (Segment 0)
    • 10001_2_1.dat (Segment 1)
    • 10001_2_2.dat (Segment 2)
  • 这些 Segment 共同组成了 Rowset 2(对应的版本可能是 [2, 2])。

3. 连续导入时的递增全景图

假设我们每隔 10 秒向同一个 Tablet 导入一批小数据,物理文件的变化如下:

导入轮次 事务状态 产生的 Rowset 物理 Segment 文件 版本区间 (Version)
第 1 次 提交 Rowset 0 10001_0_0.dat [0, 1] (初始版本)
第 2 次 提交 Rowset 2 10001_2_0.dat [2, 2]
第 3 次 提交 Rowset 3 10001_3_0.dat [3, 3]
第 4 次 提交 Rowset 4 10001_4_0.dat
10001_4_1.dat (数据稍大,分了两个)
[4, 4]

随着导入不断进行,Rowset ID 和 Segment ID 持续递增,磁盘上的小文件(Segment)越来越多。


第三部分:应对 Segment 爆炸的机制 —— Compaction(数据合并)

如果任由 Segment 文件随着导入无限递增,会导致两个严重问题:

  1. 查询性能急剧下降:每次查询需要打开成百上千个文件,引发大量的随机 IO。
  2. 句柄耗尽:系统会报 "Too many open files" 错误。

为了解决这个问题,Doris 内部有自动的 Compaction(后台合并) 机制,它是控制版本和 Segment 数量的平衡器。

1. Cumulative Compaction (CC) - 增量合并

  • 过程:Doris 后台线程会监控 Tablet。当发现有多个相邻的、较小的 Rowset 时,会将它们读取出来,在内存中进行归并排序,然后写成一个全新的、更大的 Rowset
  • 版本变化:假设合并了 [2, 2][3, 3][4, 4],合并后会生成一个新的 Rowset,版本号为 [2, 4]
  • 物理文件变化:原先的多个小 Segment 文件(如 3 个)被合并成了 1 个较大的 Segment 文件。旧的 Segment 文件会在没有活跃查询读取它们后,被垃圾回收(GC)机制物理删除。

2. Base Compaction (BC) - 基线合并

  • 过程:将 Cumulative Compaction 产生的已合并 Rowset(如 [2, 4])与历史最大的基线 Rowset(通常是 [0, 1])再次进行合并。
  • 版本变化:合并后形成版本为 [0, 4] 的超大 Rowset。
  • 目的:最大程度减少版本链长度,使数据物理上最紧凑,查询效率最高。

总结

  1. Version(版本) 是 Doris 实现 MVCC 的逻辑标识,每一次数据导入成功就会产生一个新 Version(对应一个 Rowset)。
  2. Segment(段) 是底层的物理存储文件。
  3. 生成逻辑:导入数据 \rightarrow 写入 MemTable \rightarrow 刷盘(Flush) \rightarrow 生成一个或多个 Segment 文件 \rightarrow 关联到当前导入的 Version (Rowset)。
  4. Doris 通过后台的 Compaction 机制,不断将多个小 Version 的 Segment 合并成大 Version 的 Segment,控制文件数量,确保查询的高性能。
00:00
00:00