Doris architecture

ANALYTICAL DATABASE / SOURCE READING / LESSON 06

Tablet, Rowset, and Segment storage

Understand Doris columnar storage through tablets, rowsets, segments, pages, and scan boundaries.

Reading
50 min
Track
Doris architecture
Source
Chinese source notes

The source notes for this track are currently maintained in Chinese.

预计阅读时间: 50 分钟 前置阅读: doc-01 §8 (Scan操作), doc-02 §2b (BE写入) 下一次阅读: doc-07 (Compaction), doc-08 (索引)


1. Tablet 分片模型

Tablet 是什么

Tablet 是 Doris 数据管理的最小单位——表的水平分片(Hash 分桶)的物理存储单元。一个 Tablet 大小 ~1-10GB, 默认每个 BE 可管理数万个 Tablet。

元数据定义

OlapTable (FE Catalog 中)
├── Partition 0    [dt='2026-07-01' .. '2026-07-07']
│   ├── Tablet 0   → Replica 0 (BE-1) + Replica 1 (BE-2) + Replica 2 (BE-3)
│   ├── Tablet 1   → Replica 0 (BE-2) + Replica 1 (BE-3) + Replica 2 (BE-1)
│   └── Tablet 2   → ...
├── Partition 1    [dt='2026-07-08' .. '2026-07-14']
│   └── ...
└── Partition N

BE 侧: Tablet (be/src/storage/tablet.h)
├── _tablet_meta   (TabletMeta)
│   ├── tablet_id, schema_hash
│   ├── _rowsets[] → Rowset 列表 (按 Version 排序)
│   └── _version    → 当前版本号
├── _data_dir       → 本地磁盘路径 (如 /data/doris/data/10000/20000/)
└── _rs_graph       → Rowset 版本图 (用于查询时决定读哪些 Rowset)

源码定位

文件关键信息
be/src/storage/tablet.hclass Tablet — Tablet 主类
be/src/storage/tablet_meta.hTabletMeta — Tablet 的持久化元数据

源码导航

文件关键类/方法职责
be/src/storage/tablet.hTabletTablet 主类(元数据+版本管理)
be/src/storage/tablet_meta.hTabletMetaTablet 持久化元数据
be/src/storage/rowset/rowset.hRowsetRowset 接口
be/src/storage/rowset/beta_rowset.hBetaRowset默认 Rowset 实现
be/src/storage/rowset/rowset_meta.hRowsetMetaRowset 元数据(protobuf)
be/src/storage/rowset/rowset_builder.hBetaRowsetBuilderRowset 构建器
be/src/storage/rowset/segment_v2/SegmentWriter, SegmentReaderSegment 读写
gensrc/proto/olap_file.protoSegmentFooterSegment 格式定义(Protobuf)

2. Rowset 体系

Rowset 类型

Rowset (接口, be/src/storage/rowset/rowset.h)
├── BetaRowset (默认, Doris 1.2+)
│   └── 实现: be/src/storage/rowset/beta_rowset.h
│   └── 格式: Segment V2 列存
│   └── 包含: RowsetMeta + N 个 Segment 文件
│
└── AlphaRowset (旧格式, 已废弃)
    └── 仅在升级/兼容场景出现, 不再产生新数据

Rowset 生命周期

Create:   用户写入 → DeltaWriter → RowsetBuilder::build_rowset() → Rowset 文件 (磁盘)
Committed: Rowset 写入完成, 但尚未 Publish → 状态: COMMITTED (不可见)
Visible:  Publish → Tablet::publish_version() → Rowset 变为 VISIBLE
Compact:  多个 Rowset → Compaction::merge() → 新的大 Rowset
Deleted:  Compaction 完成后, 旧的 Rowset 标记为删除 (等待 GC)

源码定位

文件用途
be/src/storage/rowset/rowset.hRowset 基类
be/src/storage/rowset/beta_rowset.hBetaRowset 类
be/src/storage/rowset/beta_rowset_reader.hRowsetReader
be/src/storage/rowset/beta_rowset_writer.hRowsetWriter
be/src/storage/rowset/rowset_builder.hRowsetBuilder
be/src/storage/rowset/rowset_meta.hRowsetMeta

3. Segment 文件格式——列存物理布局

Segment 文件布局

┌───────────────────────────────────────────────┐
│  Segment File (一个物理文件, 多列)              │
│                                                │
│  ┌──────────────────────────────────────┐     │
│  │  Data Pages                          │     │
│  │  ┌ Column 0: dt (Date) ────────────┐ │     │
│  │  │ Page 0: rows 0..4095             │ │     │
│  │  │ Page 1: rows 4096..8191          │ │     │
│  │  │ Page 2: rows 8192..12287         │ │     │
│  │  └──────────────────────────────────┘ │     │
│  │  ┌ Column 1: status (String) ───────┐ │     │
│  │  │ Page 0: rows 0..4095             │ │     │
│  │  │ Page 1: rows 4096..8191          │ │     │
│  │  │ ...                              │ │     │
│  │  └──────────────────────────────────┘ │     │
│  │  ┌ Column 2: amount (Int64) ────────┐ │     │
│  │  │ ...                              │ │     │
│  │  └──────────────────────────────────┘ │     │
│  └──────────────────────────────────────┘     │
│                                                │
│  ┌──────────────────────────────────────┐     │
│  │  Index Pages (元数据)                 │     │
│  │  ├ Ordinal Index (每列): Page→Offset  │     │
│  │  └ ZoneMap Index (每列): Min/Max/Null │     │
│  └──────────────────────────────────────┘     │
│                                                │
│  ┌──────────────────────────────────────┐     │
│  │  Footer (文件尾)                      │     │
│  │  ├ SegmentFooter (protobuf)           │     │
│  │  ├ Num rows                           │     │
│  │  ├ ColumnFooters[] (每列信息)          │     │
│  │  │ ├ Column type, encoding, compress  │     │
│  │  │ ├ IndexPage offset                 │     │
│  │  │ └ DataPage offsets[]               │     │
│  │  └ Footer offset (末尾 8 bytes)       │     │
│  └──────────────────────────────────────┘     │
└───────────────────────────────────────────────┘

与 Parquet 的类比:

  • Segment = Parquet File (一整个文件)
  • Page = Row Group (4096 行)
  • Column Data Pages = Column Chunk (一列的所有 Page)
  • Ordinal Index = Page Index (Row Group 到 offset 的映射)
  • ZoneMap = Column Statistics (Min/Max/Null count)

Page 分页设计

默认每页 4096 行 (由 write_batch_size 控制, 通常等于 OLAP_TABLE_MAX_RAW_ROWS_PER_BLOCK)。这个粒度的选择:

页大小优点缺点
太大 (16384+)索引条目少, 索引查找快随机访问开销大
太小 (1024-)精确定位到需要的数据索引文件大, 页切换频繁
4096 (当前)在索引大小和数据粒度间取得平衡-

4. Encoding / Compression 链

列编码 (Encoding)

编码适用列说明
PLAIN_ENCODING所有类型无压缩存储, 直接读取
RLE (Run Length)低基数列连续重复值压缩 (如 status='OK' 重复 1000 次 → 1 个值+1000)
BIT_SHUFFLE数值列按 bit 重排后压缩, 适合浮点数
DICT_ENCODING字符串列字典编码 (值→int 映射), 对低基数字符串非常有效
PREFIX_ENCODING排序后的字符串列前缀编码 (共享长前缀)

压缩算法 (Compression)

压缩速度压缩率适用
LZ4极快中等 (~2:1)热数据 (查询频率高)
ZSTD好 (~4:1)温数据
LZ4F较快较好 (~3:1)默认压缩 (平衡速度快 + 压缩率好)

存储引擎——Tablet / Rowset / Segment 文件格式 图 01

端到端链路

写入: Raw Data → [DictEncoding 字典化] → [LZ4/ZSTD 压缩] → Page → Segment File
读取: Segment File → Page → [解压缩] → [解码] → ColumnVector (内存列)

5. SegmentWriter → SegmentReader 读写路径

写入 (SegmentWriter)

// SegmentWriter — 写入一个 Segment
SegmentWriter::init(SegmentFooter* footer_schema)
  → 确定列编码 (RLE/DICT/PLAIN) 和压缩 (LZ4/ZSTD)

SegmentWriter::append_block(Block* block)
  → 将 Block 的 4096 行写入各列的当前 DataPage
  → 当 Page 行数 ≥ 4096 → finalize_page() → flush DataPage

SegmentWriter::finalize()
  → finalize 最后一个 incomplete Page
  → 写各列的 Ordinal Index Page
  → 写 ZoneMap index
  → 写 Footer (SegmentFooter protobuf)
  → posix_fadvise (建议 OS 缓存策略)

读取 (SegmentReader)

// SegmentReader — 读取 Segment 的特定列和行范围
SegmentReader::init(SegmentReadOptions* opts)
  → opts.cols[] = {"status", "amount"}      // 只读这两列
  → opts.key_ranges[] = [BJ, SH]             // key 范围过滤
  → opts.conditions[] = (amount > 100)       // 谓词 (索引过滤用)
  → opts.delete_bitmap                      // 删除标记

SegmentIterator::next_batch(Block* block)
  → 1. ZoneMap 快速检查: 每列 Min/Max 是否与 key_range/condition 有交集
  →    如果没有 → 跳过整个 Segment (不读任何 Page)
  → 2. Ordinal Index 定位: 找到包含目标 key_range 的 Page
  → 3. BloomFilter 检查 (如有)
  → 4. 读取 DataPage: io.read(page_offset, page_size)
  → 5. 解压缩 → 解码 → 构造 ColumnVector
  → 6. 应用 DeleteBitmap 过滤删除的行
  → 7. 返回 Block (4096 行)

6. 列存优势体现

优势原理典型场景
列投影查询 3/100 列 → 只需读 3 列的 ColumnData, 其他 97 列完全跳过宽表分析 (100+ 列)
谓词下推WHERE a > 10 → 读 a 列的 ZoneMap → 过滤后只读剩余的 Page时序分析, 多维过滤
编码压缩同列数据相似 → RLE/DICT 编码有效; CPU 缓存友好低基数列 (status, city)
SIMD 友好ColumnVector<T> → 连续内存布局 → SIMD 操作 (AVX2/AVX512)Agg, Filter
延迟物化先过滤行的位置, 再按需读其他列 (不会先读所有列再过滤)复杂 WHERE 条件

7. 常见问题 / 面试题

Q1: 一个 Tablet 最多有多少个 Rowset? A: 理论上无限。实际上 Cumulative Compaction 控制在 ~10-20 个。如果 Compaction 跟不上写入速度, Rowset 数量会持续增长, 导致查询性能下降 ("Rowset Runaway" 问题)。运维需监控 tablet_max_rosversion 指标。

Q2: Segment 格式为什么不直接用 Parquet? A: (1) Doris 需要更细粒度的索引 (Ordinal Index 按 Page 定位, Parquet 的 Page Index 是后来加的); (2) Segment 支持 Doris 特有的 Merge-on-Write/DeleteBitmap 语义; (3) 性能: Segment 是 Doris 的内存格式的磁盘投影, 读写转换最小。

Q3: 延迟物化在 SegmentReader 中是如何实现的? A: 查询 SELECT status, amount FROM t WHERE city='BJ' 的执行顺序: (1) 先读 city 列的 ZoneMap + DataPages → 过滤产生 row_ids[1,3,7,...]; (2) 然后只读 row_ids 对应位置的 status 和 amount 列 (不读 city 列的全部数据)。这避免了先读 status 和 amount 列的全部行再过滤。

Q4: Segment 文件的 Footer 为什么放在文件末尾而不是开头? A: 文件尾部的位置固定(最后 8 bytes 存 footer_offset), 可以直接 seek 到。如果放在开头, 写入时需要预留空间 (Segment 写完才知道各列的 Page offset)。这是大多数列存格式的常见设计 (Parquet, ORC 均如此)。


Segment 文件物理布局 (列存格式)

下一步