什么是 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. 版本是如何递增的?
- 初始状态:一个新创建的 Tablet,其初始版本通常是
[0, 1](Schema 占用的基础版本)。 - 事务提交:每一次导入任务(Stream Load, Broker Load 等)都是一个事务。
- 版本递增:当事务成功提交(Commit)时,Frontend (FE) 会为该导入分发一个全局递增的事务 ID(Txn ID),并在对应的 Tablet 上生成一个新的 Version。
- 例如,前一个版本是 10,新导入成功后,就会生成一个 Version 为
[11, 11]的新 Rowset。
- 例如,前一个版本是 10,新导入成功后,就会生成一个 Version 为
- 读取可见性:查询请求到来时,FE 会获取当前最新的有效版本号(例如 11),并在 BE 端只读取版本号 的数据,大于该版本的数据对当前查询不可见。这保证了读写不冲突。
第二部分:当数据不断导入时,底层 Segment 是如何产生和递增的?
在物理存储上,Doris 的数据最终是以 .dat 后缀的 Segment(段文件) 格式存在于 BE 节点的磁盘上的。
我们要理清三者的包含关系:Tablet Rowset Segment。
一个 Tablet 包含多个 Rowset(版本),一个 Rowset 包含多个 Segment 文件。
下面是持续导入数据时,Segment 的产生和递增过程:
1. 内存缓冲与 Flush 机制(Segment 的诞生)
当发起一次导入任务时:
- 写入 MemTable:数据首先写入 BE 节点的内存缓冲区(MemTable)。
- 触发 Flush:当 MemTable 达到限制(默认限制如 100MB 或者是特定的行数),或者单次导入任务结束时,内存中的数据会被排序、压缩并刷写(Flush)到磁盘上。
- 生成 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.dat10001_4_1.dat (数据稍大,分了两个) |
[4, 4] |
随着导入不断进行,Rowset ID 和 Segment ID 持续递增,磁盘上的小文件(Segment)越来越多。
第三部分:应对 Segment 爆炸的机制 —— Compaction(数据合并)
如果任由 Segment 文件随着导入无限递增,会导致两个严重问题:
- 查询性能急剧下降:每次查询需要打开成百上千个文件,引发大量的随机 IO。
- 句柄耗尽:系统会报 "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。 - 目的:最大程度减少版本链长度,使数据物理上最紧凑,查询效率最高。
总结
- Version(版本) 是 Doris 实现 MVCC 的逻辑标识,每一次数据导入成功就会产生一个新 Version(对应一个 Rowset)。
- Segment(段) 是底层的物理存储文件。
- 生成逻辑:导入数据 写入 MemTable 刷盘(Flush) 生成一个或多个 Segment 文件 关联到当前导入的 Version (Rowset)。
- Doris 通过后台的 Compaction 机制,不断将多个小 Version 的 Segment 合并成大 Version 的 Segment,控制文件数量,确保查询的高性能。