doris

一、 Apache Doris

  1. 简介
    Apache Doris 是一款基于 MPP 架构(大规模并行处理)的高性能、实时分析型数据库。它以高效、简单和统一的特性著称,能够在亚秒级的时间内返回海量数据的查询结果。Doris 既能支持高并发的点查询场景,也能支持高吞吐的复杂分析场景。
    基于这些优势,Apache Doris 非常适合用于报表分析、即席查询、统一数仓构建、数据湖联邦查询加速等场景。用户可以基于 Doris 构建大屏看板、用户行为分析、AB 实验平台、日志检索分析、用户画像分析、订单分析等应用。
  2. 发展历程
    Apache Doris 最初是百度广告报表业务的 Palo 项目。2017 年正式对外开源,2018 年 7 月由百度捐赠给 Apache 基金会进行孵化。在 Apache 导师的指导下,由孵化器项目管理委员会成员进行孵化和运营。2022 年 6 月,Apache Doris 成功从 Apache 孵化器毕业,正式成为 Apache 顶级项目(Top-Level Project,TLP)。
    目前,Apache Doris 社区已经聚集了来自不同行业数百家企业的 600 余位贡献者,并且每月活跃贡献者人数超过 120 位。
  3. 使用场景
    数据源经过各种数据集成和加工处理后,通常会进入实时数据仓库 Doris 和离线湖仓(如 Hive、Iceberg 和 Hudi),广泛应用于 OLAP 分析场景,如下图所示:

Apache Doris 主要应用于以下场景:

  • 实时数据分析:

    • 实时报表与实时决策:为企业内外部提供实时更新的报表和仪表盘,支持自动化流程中的实时决策需求。
    • 交互式探索分析:提供多维数据分析能力,支持对数据进行快速的商业智能分析和即席查询(Ad Hoc),帮助用户在复杂数据中快速发现洞察。
    • 用户行为与画像分析:分析用户参与、留存、转化等行为,支持人群洞察和人群圈选等画像分析场景。
  • 湖仓融合分析:

    • 湖仓查询加速:通过高效的查询引擎加速湖仓数据的查询。
    • 多源联邦分析:支持跨多个数据源的联邦查询,简化架构并消除数据孤岛。
    • 实时数据处理:结合实时数据流和批量数据的处理能力,满足高并发和低延迟的复杂业务需求。
  • 半结构化数据分析:

    • 日志与事件分析:对分布式系统中的日志和事件数据进行实时或批量分析,帮助定位问题和优化性能。

二、 整体架构
Apache Doris 采用 MySQL 协议,高度兼容 MySQL 语法,支持标准 SQL。用户可以通过各类客户端工具访问 Apache Doris,并支持与 BI 工具无缝集成。

Apache Doris 存算一体架构精简且易于维护。它包含以下两种类型的进程:

  • Frontend (FE): 主要负责接收用户请求、查询解析和规划、元数据管理以及节点管理。
  • Backend (BE): 主要负责数据存储和查询计划的执行。数据会被切分成数据分片(Shard),在 BE 中以多副本方式存储。

在生产环境中,可以部署多个 FE 节点以实现容灾备份。每个 FE 节点都会维护完整的元数据副本。FE 节点分为以下三种角色:
角色
功能
Master
FE Master 节点负责元数据的读写。当 Master 节点的元数据发生变更后,会通过 BDB JE 协议同步给 Follower 或 Observer 节点。
Follower
Follower 节点负责读取元数据。当 Master 节点发生故障时,可以选取一个 Follower 节点作为新的 Master 节点。
Observer
Observer 节点负责读取元数据,主要目的是增加集群的查询并发能力。Observer 节点不参与集群的选主过程。
FE 和 BE 进程都可以横向扩展。单个集群可以支持数百台机器和数十 PB 的存储容量。FE 和 BE 进程通过一致性协议来保证服务的高可用性和数据的高可靠性。存算一体架构高度集成,大幅降低了分布式系统的运维成本。

三、 存储引擎

  1. 存储引擎架构
    整体层级:表 → Partition(分区)→ Bucket(分桶)→ Tablet(多副本)→ RowSet → Segment → Block
    其中:Bucket 是逻辑分桶,Tablet 是其物理实现

    • 表(Table)

      • 定义:最上层的逻辑概念,是用户操作的基本单位,包含完整的 schema(列定义、数据类型、主键 / 排序键等)、分区策略、分桶策略等元信息。
      • 作用:抽象数据集合,屏蔽底层存储细节,提供统一的 SQL 操作接口(如SELECT/INSERT)。
      • 示例:用户创建的user_behavior表,包含user_id、action、dt等列,定义按dt分区、按user_id分桶。
    • Partition(分区)

      • 定义:对表按业务规则(如时间、范围、列表)进行的逻辑拆分,每个分区是表的子集,拥有独立的元数据(但共享表的 schema)。

      • 作用:

        • 减少查询扫描范围(如查询dt='2023-10-01'的数据时,仅需访问对应分区);
        • 支持分区级操作(如删除过期分区、对特定分区设置存储策略)。
      • 示例:user_behavior表按dt(日期)分区,每个分区对应一天的数据(如p20231001、p20231002)。

    • Bucket(分桶)与 Tablet(最小存储单元,多副本)

      • Bucket(分桶):

        • 定义:在每个分区内部,按哈希或范围对数据进一步拆分(通常基于主键 / 分桶键),是逻辑上的细分单位。
        • 作用:将大分区拆分为更小的子集,实现并行读写(每个分桶可由不同节点处理)。
      • Tablet:

        • 定义:Bucket 的物理实现,是 Doris 中最小的存储和调度单位。每个 Bucket 对应一个 Tablet,且每个 Tablet 有多个副本(默认 3 个),分布在不同的 BE 节点,保证高可用和负载均衡。
        • 作用:
          • 存储实际数据,是数据分片的最终载体;
          • 集群调度的基本单位(如副本迁移、负载均衡均以 Tablet 为单位)。
      • 联系:1 个 Partition 包含 N 个 Bucket(数量由buckets参数指定),1 个 Bucket 对应 1 个 Tablet(物理实体),1 个 Tablet 有 M 个副本(由replication_num指定)。

    • RowSet(行集)

      • 定义:每个 Tablet 内部的版本化数据集,是逻辑上的 “数据版本单位”。每次写入(如INSERT)或合并(Compaction)会生成新的 RowSet。

      • 作用:

        • 管理数据版本(通过版本号区分新旧数据);
        • 作为 Compaction 的基本单位(合并多个小 RowSet 为大 RowSet,优化查询)
      • 示例:一个 Tablet 中可能包含多个 RowSet(如版本 1、版本 2...),分别对应不同时间写入的数据

    • Segment(段)

      • 定义:RowSet 的物理存储文件,是磁盘上的二进制文件。一个 RowSet 会拆分为多个 Segment(按数据量拆分,如单个 Segment 不超过 1GB)。
      • 作用:
        • 存储实际的行数据,包含数据本身、索引(如稀疏索引、布隆过滤器)、统计信息(如列的最大 / 最小值);
        • 支持查询时的 “文件级过滤”(通过统计信息跳过无关 Segment)。
    • Block(块)

      • 定义:Segment 内部按列拆分的最小存储单位(Doris 是列存引擎)。每个 Segment 中的每一列数据会被拆分为多个 Block,每个 Block 包含连续的若干行数据。
      • 作用:
        • 支持按列压缩(减少存储空间);
        • 支持高效的列存查询(仅读取查询涉及的列的 Block);
        • 作为 IO 操作的最小单位(读取数据时按 Block 加载,减少 IO 次数)。
    • 层级包含:上层概念包含下层概念,形成 “表→分区→分桶 / Tablet→RowSet→Segment→Block” 的完整链条。

      • 例:一个表有 10 个分区,每个分区有 8 个分桶(对应 8 个 Tablet),每个 Tablet 有 5 个 RowSet,每个 RowSet 拆分为 3 个 Segment,每个 Segment 的每列包含若干 Block。
  2. 数据写入和更新流程
    Doris 的写入流程采用 “内存缓冲 + 磁盘持久化” 的分层设计,确保高吞吐写入的同时避免数据丢失,核心步骤如下:

  3. 写入 MemTable(内存缓冲区)数据写入时,首先进入 Tablet(最小存储单元)对应的 MemTable(内存表)。MemTable 基于 SkipList 数据结构实现,特性包括:

    • 保持数据按排序键(Sort Key)有序,便于后续快速合并;
    • 支持 O (log n) 级别的插入和查询,适配高频写入场景;
    • 采用 “写时复制”(Copy-On-Write)机制,新写入数据进入可变 MemTable(Mutable MemTable),避免并发冲突。
  4. MemTable 刷盘(Flush)生成 RowSet当 MemTable 达到内存阈值(如单 Tablet 64MB)或满足时间条件(如定时 1 分钟)时,会触发 Flush 操作:

    • 将当前 MemTable 标记为不可变(Immutable MemTable),新写入数据进入新的 Mutable MemTable;
    • 不可变 MemTable 中的有序数据被序列化,生成磁盘上的 RowSet(行集,数据版本单位),完成从内存到磁盘的持久化。
  5. RowSet 的版本管理每次写入或合并操作会生成新的 RowSet,每个 RowSet 分配唯一递增的版本号,确保数据更新的原子性和可追溯性。多个 RowSet 共同构成 Tablet 的完整数据(类似多版本并发控制)。

Compaction(数据合并)是存储引擎的核心后台任务,用于解决 “小文件 / 小版本堆积” 问题,通过合并分散的小数据块来优化存储结构、提升查询性能。其设计围绕 “平衡写入性能与查询效率” 展开,具体机制如下:
1、Compaction 的核心目的

  • 随着数据不断写入,Doris 会生成大量小的 Rowset(每个 Rowset 对应一次 MemTable 刷盘或小批量写入),导致:
  • 查询效率下降:查询需扫描多个 Rowset,增加 IO 和合并开销;
  • 存储冗余:每个 Rowset 包含独立的元数据和索引,浪费存储空间;
  • 管理复杂度上升:过多版本的 Rowset 增加元数据管理成本。

Compaction 的作用就是通过合并这些小 Rowset,生成更少、更大的 Rowset,最终实现:

  • 减少查询时需扫描的 Rowset 数量;
  • 优化索引和统计信息,加速查询过滤;
  • 释放旧版本 Rowset 占用的存储空间。

2、Compaction 的两种类型
Doris 针对不同场景设计了两种 Compaction 策略,互补协作

  • Minor Compaction(轻度合并)

    • 触发条件:当一个 Tablet 内的小 Rowset 数量达到阈值(默认 10 个)时自动触发。

    • 合并对象:选取多个非重叠的小 Rowset(通常是最近生成的),合并为一个较大的 Rowset。

      • 非重叠指 Rowset 的数据范围(按排序键)无交叉,合并时无需处理数据冲突,仅需按顺序拼接。
    • 特点:

      • 轻量操作,IO 和 CPU 开销小,可高频执行;
      • 不淘汰旧版本 Rowset,仅减少数量(例如 10 个小 Rowset 合并为 1 个);
      • 主要目的是临时缓解小 Rowset 堆积问题,为 Major Compaction 减负。
  • Major Compaction(重度合并)

    • 触发条件:

      • 定时触发(默认每天凌晨执行);
      • 手动触发(通过 ALTER TABLE ... COMPACT 命令);
      • 当 Rowset 总大小或版本数量达到阈值时自动触发。
    • 合并对象:一个 Tablet 内所有重叠或相邻的 Rowset,无论新旧。

      • 重叠指 Rowset 的数据范围有交叉(如包含相同主键的更新数据),合并时会按版本号保留最新数据,淘汰旧版本。
    • 特点:

      • 重量级操作,IO 和 CPU 开销大,执行频率低;
      • 彻底淘汰旧版本 Rowset,仅保留合并后的新版本;
      • 合并后的数据按排序键重新组织,优化存储结构,显著提升查询性能。
  1. 列存储引擎的底层实现
  • Segment 的物理结构(以 v2 版本为例):
    • 文件开始是8个字节的magic code,用于识别文件格式和版本
    • Data Region:用于存储各个列的数据信息,这里的数据是按需分page加载的
    • Index Region: Segment 物理结构的核心组成部分,负责存储各类索引信息,用于加速查询时的数据定位与过滤,减少无效 IO。
    • Footer信息
      • FileFooterPB:定义文件的元数据信息
      • 4个字节的footer pb内容的checksum
      • 4个字节的FileFooterPB消息长度,用于读取FileFooterPB
      • 8个字节的MAGIC CODE,之所以在末位存储,是方便不同的场景进行文件类型的识别
  1. 编码与压缩策略(存储效率的核心)
  • 编码策略(Encoding):优化数据表示形式

     编码的核心是根据数据类型的特性,将原始数据转换为更紧凑、更易压缩或更利于查询的格式。Doris 针对不同数据类型(数值型、字符串型、时间型等)设计了专属编码方式:
    
    • 1、数值型(int、float、double 等)

数值型数据(尤其是整数)在分析场景中占比高,且具有连续或集中分布的特点,Doris 主要采用以下编码:
* 定长编码(Fixed-Length Encoding)

适用于 TINYINT、SMALLINT、INT、BIGINT 等固定长度类型,直接按其原生字节长度存储(如 INT 占 4 字节,BIGINT 占 8 字节)。优势是读写速度快,无需额外解析。
* 差值编码(Delta Encoding)

适用于有序递增 / 递减的数值列(如自增 ID、连续时间戳)。原理是存储 “当前值与前一个值的差值”,而非原始值。例如,序列 [100, 101, 102, 103] 编码为 [100, 1, 1, 1],差值通常较小,可进一步用变长编码压缩。
* RLE 编码(Run-Length Encoding,游程编码)

适用于存在大量重复值的数值列(如 status 列,值多为 0 或 1)。原理是记录 “值 + 连续出现次数”,例如 [5,5,5,3,3] 编码为 [(5,3), (3,2)],大幅减少重复存储。
* 2、字符串型(varchar、string)

字符串型数据长度可变、多样性高,Doris 重点优化重复率和存储效率:
* 字典编码(Dictionary Encoding)

适用于低基数字符串列(如性别 ["male", "female"]、省份 ["Beijing", "Shanghai"])。原理是:
* 收集列中所有唯一字符串,分配一个整数 ID(如 male→0,female→1);
* 用 ID 替代原始字符串存储(如 ["male", "female", "male"] 编码为 [0, 1, 0]);
* 字典表(字符串→ID 映射)单独存储在元数据中。优势:将变长字符串转为定长整数,压缩率极高,且支持数值型的快速比较(如 =、<)。

    * 前缀编码(Prefix Encoding)

适用于有公共前缀的字符串(如 URL ["http://a.com", "http://b.com"]、文件路径)。原理是存储 “首行完整字符串 + 后续行与首行的前缀差值”,例如:
* 首行:"http://a.com"(完整存储);
* 第二行:"http://b.com" 与首行共享前缀 "http://",仅存储 6,b.com(6 表示前缀长度,后续为差异部分)。

    * 变长编码(Variable-Length Encoding)

适用于高基数、无规律字符串(如 UUID、随机生成的字符串)。原理是存储 “长度 + 内容”(如字符串 abc 存储为 0x03 616263,0x03 表示长度为 3),兼顾灵活性和基本存储效率。
* 3、时间型(date、datetime、timestamp)

时间型数据本质是 “有语义的数值”(如 Unix 时间戳),Doris 采用:
* 整数编码(Integer Encoding)
将时间转换为整数(如 date 转为距离 epoch 的天数,datetime 转为秒级 / 毫秒级时间戳),再按数值型编码(如定长编码)存储。例如,2023-10-01 转为天数 19593(假设 epoch 为 1970-01-01),用 4 字节整数存储,既节省空间又支持快速范围查询(如 dt > '2023-10-01' 转为整数比较)

*  编码的自动选择与配置

Doris 会根据列的数据类型和基数自动选择编码方式(默认策略):

低基数字符串列 → 字典编码;
有序数值列 → 差值编码;
高重复数值列 → RLE 编码;
其他类型 → 默认编码(如定长、变长)。
用户也可通过 PROPERTIES 在表创建时指定编码(覆盖默认策略)。

  • 压缩策略(Compression):进一步减少数据体积

编码后的数据会通过压缩算法进一步减少体积,Doris 压缩策略的核心是按 Block 级压缩(每个 Block 独立压缩),兼顾压缩率和读写速度。

  1. 主流压缩算法及适用场景

Doris 支持多种压缩算法,默认根据数据类型和编码方式自动选择,也可手动配置:

  • LZ4:默认算法,速度优先(压缩 / 解压速度极快,CPU 开销低),压缩率中等(通常比原始数据小 2~3 倍)。适用场景:高频查询的热数据、对延迟敏感的场景(如实时分析)。
  • ZSTD:压缩率优先(压缩率比 LZ4 高 10%~30%),速度略慢于 LZ4 但仍优于 GZIP。适用场景:冷数据(低频访问)、对存储成本敏感的场景(如历史归档数据)。
  • SNAPPY:平衡速度和压缩率,性能介于 LZ4 和 ZSTD 之间(目前逐步被 LZ4 替代)。
  • NONE:不压缩,适用于已高度编码(如字典编码的低基数列)或需极致读写速度的场景。
  1. 压缩的层级与流程
    Doris 的压缩在Block 级别进行(每个 Block 是列的连续多行数据,大小通常 64KB~1MB),流程如下:
  2. 数据按列拆分为 Block(如 age 列的 1024 行数据组成一个 Block);
  3. 对 Block 进行编码(如差值编码、字典编码);
  4. 对编码后的 Block 用指定算法压缩(如 LZ4);
  5. 压缩后的 Block 存储在 Segment 文件的 Data Region 中,元数据记录压缩算法(用于解压)。

Block 级压缩的优势:

  • 解压时仅需加载并解压查询涉及的 Block,无需解压整个 Segment,减少 IO 和 CPU 开销;
  • 小 Block 适合内存缓存,加速高频访问数据的读取。

Doris-BE-存储结构设计解析
https://wingsgo.github.io/2020/02/24/doris-03-be_refactor_2019.html
https://doris.apache.org/zh-CN/community/design/doris_storage_optimization
https://parquet.apache.org/docs/file-format/

posted @ 2025-08-21 12:49  小海哥哥de  阅读(63)  评论(0)    收藏  举报