doris的使用
doris 的使用
初步概念
doris 是一款具有大规模并行处理(mpp)架构,支持 存算一体,存算分离,具有特有的 查询引擎 和 存储引擎 的实时分析型数据库。doris 的衍生产品有 starrocks,同种类似产品有 clickhouse。
FE,BE 的具体作用:
在存算一体的模型中:
FE:响应用户请求,查询解析,表元数据的管理,节点管理等;划分了 leader,follower,observer 三种角色
BE:数据存储,任务的执行,数据会被切分成数据分片(Shard),在 BE 中以多副本方式存储。
doris 的数据模型,存储引擎,查询引擎
存储引擎(借鉴 apache orcfile)
3.0 版本开始支持存算分离,之前为存算一体。
采用 Segment v2 的形式来存储数据,其结构主要分为数据区、索引区和页脚三个部分。数据区存储各列的数据,按需分页加载;索引区存储列的索引数据,按列粒度加载;页脚包含文件的元数据信息,如 SegmentFooterPB、校验和以及 FileFooterPB 的长度
apache doris 文件分布:
plaintext表(Table) ├── 分区(Partition) -- 第一级:按业务维度划分(如时间) │ ├── 分桶(Bucket) -- 第二级:按哈希均匀分布(如用户ID) │ │ ├── RowSet -- 第三级:写入时的逻辑单元(内存/临时) │ │ │ └── Segment -- 第四级:持久化的物理文件(磁盘) │ │ └── ... │ └── ... └── ...
每张表可以创建不同的分区,但是 doris 只能创建一级分区,每个分区下按照不同列取 hash 或者 random 打散创建不同的桶,也称为 tablet;每个 tablet 划分不同的 rowset;依托列式存储的概念,rowset 划分不同 segment,segment 是最小的数据单元;
行列混存
doris 的使用列式存储数据,对于条件过滤,排序,分区性能很好,但是对于点查(select * from table)需要需要读取全部的列,每个列的读取都需要一次 IO,对于宽表(比如上百列的 DWS 表)影响较大;
故使用行列混存(行存的原理是在存储时增加了一个额外的列,这个列将对应行的所有列拼接起来采用特殊的二进制格式存储),应对点查的场景
store_row_column 设置是否开启行列混存,以及一些额外混存的参数设置
部分列更新
对应的是整行更新
数据模型(借鉴 google masa)
参考:doris 数据模型介绍
数据模型中明细模型,主键模型,聚合模型的排序键作用: 明细模型中排序键只具有排序的功能;在主键模型和聚合模型同时具有排序,聚合的功能
-
分区策略 分区类型:
- range 分区:按照分区列的值范围将数据划分到对应的分区,Range 分区支持的列类型 DATE, DATETIME, TINYINT, SMALLINT, INT, BIGINT, LARGEINT
- list 分区:按照分区列的具体值划分到对应分区,类型支持 BOOLEAN, TINYINT, SMALLINT, INT, BIGINT, LARGEINT, DATE, DATETIME, CHAR, VARCHAR ,枚举类型 的数据类型
分区模式:
- 手动分区 用户手动创建分区(如建表时指定或通过
ALTER语句增加 - 自动分区 系统根据时间调度规则自动创建分区,但写入数据时不会按需创建分区
- 动态分区 数据写入时,系统根据需要自动创建相应的分区,使用时注意脏数据生成过多的分区
-
分桶策略
- hash 通过计算分桶列值的
crc32哈希值,并对分桶数取模,将数据行均匀分布到分片中 - random 随机分配数据行到分片中
分桶数量的设置: 主要坚持大小原则:建议一个 tablet 的大小在 1-10G 范围内,即分桶的大小,通常选择 4-5G。合理预估存量数据和未来数据的增长趋势得出一定时间范围总量,结合分桶在分区的存储下,再根据 每个分桶 4-5G 的选择预估分桶数
目前,Doris 仅支持修改新增分区的分桶数量,对于以下操作暂不支持:
-
不支持修改分桶类型
-
不支持修改分桶键
-
不支持修改已创建的分桶的分桶数量
- hash 通过计算分桶列值的
-
支持的数据类型
doris 支持常用的绝大部分类型,其中 BITMAP,HLL 为比较新颖的数据类型
-
数据压缩 doris 默认采用 LZ4 的压缩编码,可以根据各自的场景选择不同的压缩算法,实现存储优化和加速查询。 影响压缩比的主要因素有:
- 数据排序性 数据有序一般比无序更好压缩
- 数据重复性
- 数据的类型
- ......
doris 的元数据管理
关键点:
- doris 的 FE 节点(leader)负责元数据的写操作
- 写操作会先记录为预写日志到文件(图中为 LOGx.x)
- 通过复制预写日志到其他非 leader 的 FE 节点,通过日志回放,修改自身的镜像(图中为 Checkpoint.x),完成元数据同步
- leader 节点会定期 chekpoint,形成新的镜像,而后通知其他非 leader 的节点拉取最新镜像,并定期清理过期镜像
可以看出 doris 的元数据的 HA 和 Hadoop 的 namenode 的元数据管理是有区别的,主要在于 hadoop 的元数据合并是在 secondary namenode 上进行的 hdfs 的元数据合并,而 doris 的元数据是由 leader 来合并维护。相同点在于都使用 WAL 机制,主要原因是内存相对出现故障的概率更高,因此考虑操作流程写入日志文件,而后加载入内存回放得到完整的元数据
查询引擎(借鉴 apache impala)
采用大规模并行处理(MPP)架构,支持节点间和节点内并行执行,以及多个大型表的分布式 Shuffle Join;Doris 查询引擎是向量化引擎,所有内存结构均按列式布局等
索引
doris 默认每张表内部都设计并自动维护了两种索引,前缀索引和 zonemap 索引,只有在这两种内置的索引不满足需求的时候,才考虑添加手动索引。
doris 为查询加速引入了索引的设计。常用索引如下
-
前缀索引
-
倒排索引
-
BloomFilter 索引
-
NGram BloomFliter 索引
-
向量索引
前缀索引
doris 每种数据模型都维护了自己的 sort key,明细模型的 sort key 为 duplicate key,主键模型的 sort key 为 unique key,聚合模型的 sort key 为 aggregate key,doris 的存储数据物理结构中数据按照 sort key 有序存储,此时就已经可以达到按照 sort key 快速查询的效果,前缀索引在 sort key 的基础上,每隔若干行取首行数据按照特定截断规则获取前 36 个字节作为前缀索引的内容,从而实现点查效率提升
由于前缀索引是 doris 的自动维护构建,所以应用广泛,通常建表时需要如下考虑:
- 采用经常出现在 where 条件中的列作为 sort key
- 将查询频率高的列建表时字段靠前,前缀索引是从最左侧取的 36 字节
- 不要将 vachar 列放在表的首列位置
倒排索引
常用于文本搜索的场景,通过对文本进行分词,对每个分词构建对应到存储行号的索引表,将查询时的多个结果多次进行 and,or,not 操作从而加速查询
BloomFilter 索引
对布隆过滤的实现,常用于高基数数字段过滤,不支持 Tinyint、Float、Double
NGram BloomFliter 索引
专门用于加速字符串列的 LIKE '%pattern%' 模糊查询
向量索引
Apache Doris 自 4.0 版本起原生支持 ANN(Approximate Nearest Neighbor,近似最近邻)向量搜索,基于 Faiss 实现 HNSW 与 IVF 索引,可在亿级向量数据上实现毫秒级 TopN 与范围检索。
不同的索引对不同的运算符和触发条件有不同的要求:
物化视图
物化视图是既包含计算逻辑也包含数据的实体。它不同于视图,因为视图仅包含计算逻辑,本身不存储数据。
注意:doris 中的每种物化视图的都有必须满足的条件使用物化视图才能生效,查询官网资料即可
同步物化视图和异步物化视图的同异步区别在于同步能够保证基表和物化视图在基表数据变更时能够同步变化到构建的物化视图,异步就是需要任务定时触发或者手动触发
同步物化视图
当构建好同步物化视图后,原来基于基表的单表查询不用变更,doris 查询时会自动改写 sql 选择效果最好的同步物化视图查询(如果基表有多张物化视图)
异步物化视图
基表数据变更时异步物化视图有一定的同步延迟,适用范围更广
| 物化视图类型 | 全量刷新 | 分区增量刷新 | 实时刷新 | 支持单表 | 支持多表 |
|---|---|---|---|---|---|
| 异步物化视图 | 支持 | 支持 | 不支持 | 支持 | 支持 |
| 同步物化视图 | - | - | 支持 | 支持 | 不支持 |
其中一个使用场景
物化视图的优点:
-
数据同步:拥有自动或手动的刷新机制,可以与基表(如图中的维表 1-4)保持数据同步。刷新可以是全量的,也可以是增量的
-
查询优化:具备智能的查询改写能力。当用户查询基表时,数据库可以自动判断并“智能地”重定向查询到物化视图,从而利用预计算的结果加速查询,这个过程对用户透明
doris 正交 bitmap 使用
参考资料
doris 提供了 bitmap 的这种数据结构来代替 count(distinct) 这种场景的精确去重,在满足 doris 的 bitmap 使用条件下,bitmap 会更高效,spark 中也有相关的 bitmap 函数;对于数据统计允许一定误差存在的场景,可以考虑 HLL。
主键更新和并发控制
参考资料:
https://doris.apache.org/zh-CN/docs/4.x/data-operate/update/unique-update-concurrent-control
doris 中的主键模型提供了完善的并发控制能力,只支持主键模型,通过 mvcc,基于 版本号(doris 的版本号是又 FE 维护的,非只读操作通过事务来实现处理,事务提交成功才会更新版本号,也就是说版本号的增长和事务提交成功的顺序有关系)的机制,保证同一个主键的更新;由于版本号由事务提交成功的顺序决定,由于网络时延,可能会造成在业务上数据的不正确覆盖,因此引入了 sequence 列 的机制,通过取业务数据中的能够代表业务数据先后顺序的列为 sequence 列,实现主键模型的更新,Sequence 列的优先级高于 MVCC 版本号。
主键更新机制的应用:
- 数据防回退,借助版本号的机制,主键模型天然支持晚到的数据覆盖原数据
- 极值获取/用户首单的场景
比如用户首单,可以巧妙借助 doris 的 sequence 列转化,实现 doris 更新后保留较早的数据,从而实现首单,比如
Sequence 值=固定极高时间(如 9999-12-31)−实际订单时间
doris 表的设计要点
主要考虑的点
- 表将要存储的数据量:数据量决定了是否需要分区,分桶
- 表数据的更新频率:sort key(doris 也叫主键)和索引的设计 doris 中默认每张表都维护了前缀索引和 zonemap 索引,其中前缀索引依托主键排序后并按照一定规则前缀选取方式,间隔固定大小数据维护 36 字节的数据前缀,便于数据查询;只有在前缀索引和 zonemap 索引不满足的使用场景才考虑手动创建其他索引,非必要不要创建;频繁更新将导致如果存在手动创建索引的情况下,除数据的更新,还有维护索引的更新,拖慢数据的更新速度
- 表数据的存储:doris 采用列式存储,常见可选的有 orc,parquet 等;结合数据压缩,可以进一步节省存储和加速查询速度,场景的压缩 方式比如 snappy,zlib,gzip 等
- 表的查询方式:如果涉及点查询的场景(即类似 select * from table)需要取表全部的列,可以考虑行列混存;如果表数据有部分列更新写入的需求,可以考虑 doris 的部分列更新
表模型选择
- 明细模型 主键只有排序作用,没有去重功效
- 主键模型 主键具有唯一性
- 聚合模型 按照主键聚合,常见函数有 min,max,replace,sum
ODS,DWD,DIM,DWS 层一般采用明细模型,或者主键模型;聚合模型主要时 DWS,需要固化需求的 ADS,和一些实时场景
表主键
主键字段选择常用,优先将整形,日期类字段排在前面(doris 的表 DDL 中字段的顺序很重要),不要将字符串类型的字段放在字段定义的靠前位置,这会导致 doris 的前缀索引效果失效或者大大减低(和前缀索引的相关规则有关)
分区分桶
doris 的数据组件方式就是一级分区下按照分桶存储的,在表的分区下的分桶的 tablet 压缩后大小在 1--10GB 比较合适,按照这个规则可通过如下命令查看并后续调整分区数量和分桶数量
sparksql1 show partitions from table table_name --看分区分桶以及数据存储大小。
一般使用自动分区和动态分区
索引
doris 内置索引:前缀索引,zonemap 索引;非必要不要手动创建其它索引,其他常用索引有 boomfliter 索引(对高基数的数字段过滤查询),Ngram boomfliter 索引(字符串模糊查询加速),倒排索引(文本检索场景)
存储和压缩
一般时列式存储和选择何时的压缩方式
建表模板
-
自动分区
plaintext1 creat table if not exists table_x( 2 dt DATE comment '', 3 key1 BIGINT comment '', 4 key2 varchar(20) comment '', 5 utc_dt date comment '', 6 ... 7 ,INDEX idx_name1(utc_dt) USING INVERTED COMMENT 'utc_dt时间的倒排索引' 8 ) 9 ENGINE=OLAP 10 UNIQUE KEY(dt,key1,key2...) 11 COMMENT '表的comment信息' 12 AUTO PARTITION BY RANGE(DATE_TRUNC(dt,"DAY"))() --自动分区DAY,WEEK,MONTH,YEAR 13 DISTRIBUTED BY HASH(experiment_no,user_no) BUCKETS 8 --设置分桶数,根据数据大小自己设置 14 PROPERTIES ( 15 "replication_allocation" = "tag.location.default: 3", --默认 16 "bloom_filter_columns"="key2...", --按需 多个字段按逗号隔开 17 "compression" = "zstd" --选用zstd压缩,按需 18 ); -
动态分区
plaintext1 creat table if not exists table_x( 2 dt DATE comment '', 3 key1 BIGINT comment '', 4 key2 varchar(20) comment '', 5 utc_dt date comment '', 6 ... 7 --按需 ,INDEX idx_name1(utc_dt) USING INVERTED COMMENT 'utc_dt时间的倒排索引' 8 ) 9 ENGINE=OLAP 10 UNIQUE KEY(dt,user_no,tenant_no,experiment_no) 11 COMMENT '' 12 PARTITION BY RANGE(dt)() 13 DISTRIBUTED BY HASH(experiment_no,user_no) BUCKETS 32 14 PROPERTIES ( 15 "replication_allocation" = "tag.location.default: 3", --默认 16 "dynamic_partition.enable" = "true", --动态分区表开启,非分区表不需要 17 "dynamic_partition.time_unit" = "DAY", --分区单位:DAY, WEEK, MONTH, YEAR 18 "dynamic_partition.end" = "3", --动态分区往后创建多少周期 19 "dynamic_partition.prefix" = "p", --默认,分区前缀 20 "dynamic_partition.buckets" = "32", -- 分桶数 21 "bloom_filter_columns"="key1,...", --按需 多个字段按逗号隔开 22 "dynamic_partition.create_history_partition" = "TRUE", --动态分区是否要创建历史分区 23 "dynamic_partition.history_partition_num" = "100", --动态分区创建多少历史分区 24 "compression" = "zstd" --选用zstd压缩,按需 25 );
LSM-tree 与 B-Tree 的设计及应用对比
问题背景:写放大 (Write Amplification)
- 定义: 写放大是指存储系统物理层面实际写入的数据量与应用程序逻辑层面请求写入的数据量之间的比值。该比值大于 1 即表示存在写放大。
- 成因: 主要源于存储介质(特别是 NAND Flash SSD)的物理特性。SSD 以页(Page)为单位写入,以块(Block)为单位擦除。更新页内一小部分数据,必须将包含该页的整个块读取到内存,修改数据,然后将整个更新后的块写回一个新的空闲位置。这个“读-改-写”(Read-Modify-Write)的循环是写放大的主要来源。,比如 HDFS
- 负面影响:
- 降低 I/O 吞吐量: 占用了额外的 I/O 带宽,限制了系统的写入性能。
- 缩短硬件寿命: 加速了闪存介质的磨损,因为其擦写次数(P/E Cycles)有限。
解决方案:LSM-tree (Log-Structured Merge-Tree)
- 核心思想: 将离散的、随机的写入请求转化为批量的、顺序的写入操作,从根本上规避了存储介质的“读-改-写”惩罚。
- 架构组件:
- MemTable: 位于内存中的数据结构(通常是平衡树或跳表),用于缓冲新的写入操作(Insert, Update, Delete)。所有写请求首先进入此处,响应速度极快。
- Immutable MemTable: 当 MemTable 达到预设阈值时,会转化为一个不可修改的副本,准备持久化。
- SSTable (Sorted String Table): 持久化在磁盘上的、不可变且内部有序的数据文件。Immutable MemTable 会通过顺序 I/O 被刷写(Flush)成一个新的 SSTable。
- Compaction (合并): 后台进程,周期性地将多个层级的 SSTable 文件进行归并。此过程会清理冗余数据(被覆盖的旧值、已删除的记录),并生成新的、更大的 SSTable,以优化存储空间和读取性能。
LSM-Tree 读写流程举例
写入流程(只追加写,绝不修改磁盘已有文件)
-
写日志与内存:先顺序追加写磁盘 WAL 日志(防断电),再写入内存 MemTable 并自动按 Key 排序。
-
刷盘(Flush):内存写满后整块刷入磁盘,生成不可变的 SSTable 文件。修改与删除(写墓碑标记
DELETE)统统作为新记录追加。 -
【数据写入演进顺序示例】
- T1:写入
Alice = 18 - T2:写入
Bob = 20 - T3:修改
Alice = 19 - T4:删除
Bob(写入墓碑Bob : DELETE)- ➔ 内存写满刷盘,生成文件
SSTable_1:[Alice: 19, Bob: DELETE]
- ➔ 内存写满刷盘,生成文件
- T5:写入
Charlie = 25- ➔ 内存写满刷盘,生成文件
SSTable_2(最新):[Charlie: 25]
- ➔ 内存写满刷盘,生成文件
- T1:写入
读取流程(按时间由新到旧检索,命中即止)
-
检索顺序:内存 MemTable ➔ 最新 SSTable ➔ 较旧 SSTable。靠前层级一旦命中立刻返回,遮罩掉更老的文件数据。
-
【基于上述数据状态的读取示例】
-
查 Charlie:直接在最新的
SSTable_2命中,返回25。 -
查 Alice:先查
SSTable_2(未命中),再查较旧的SSTable_1命中19并返回(自动屏蔽更早的18)。 -
查 Bob:在
SSTable_1命中DELETE墓碑标记,直接判定为“已删除”。
-
后台合并(Compaction)
-
物理清理:后台线程异步读取多个旧 SSTable 进行多路归并排序,物理擦除 被覆盖的旧版本数据 与 墓碑标记,合成更紧凑的大文件。
-
【合并示例】
- 将
SSTable_1与SSTable_2归并,彻底物理擦除Bob: DELETE墓碑和历史旧值Alice: 18。 - 合并后生成新文件
SSTable_Merged:[Alice: 19, Charlie: 25]。
- 将
LSM-tree 的权衡:读放大 (Read Amplification)
- 定义: 为响应单个逻辑读取请求,存储系统需要执行多次物理读取操作的现象。
- 成因: 由于数据的最新版本可能分布在内存的 MemTable 以及磁盘上多个层级的 SSTable 中,一次查询必须从新到旧(从 MemTable 到高层级 SSTable,再到低层级 SSTable)依次查找,直到找到目标数据。
- 优化手段:
- 布隆过滤器 (Bloom Filter): 一种概率型数据结构,能快速判断一个 SSTable 中“绝对不包含”某个键,从而避免不必要的磁盘 I/O。
- 索引块 (Index Block): 在 SSTable 内部存储稀疏索引,帮助快速定位数据大致所在的块。
对比数据结构:B-Tree
- 核心思想: 始终维持数据全局有序的平衡多路搜索树。数据更新采用“就地更新”(In-place Update)策略。
- 写入机制: 插入或更新数据时,需要先通过树的遍历定位到目标叶子节点,然后执行修改。如果节点空间不足,会引发节点分裂(Split)和树的再平衡(Rebalancing),这些操作可能级联向上传播,导致多次随机 I/O。这正是 B-Tree 产生较高写放大的原因。
- 读取机制: 读取路径非常高效。由于全局有序,从根节点开始,仅需几次磁盘寻道(通常与树的高度成对数关系)即可精确定位数据。
两种核心策略的对决:Merge-on-Write vs. Merge-on-Read
| 策略模型 | 核心机制 | 写入路径 | 读取路径 | 写放大 | 读放大 | 适用场景 | 代表 |
|---|---|---|---|---|---|---|---|
| 写时归并 (Merge-on-Write) | 写入数据时立即合并/更新,保证存储数据始终是最新、最规整的单一版本。 | 复杂且慢,涉及随机读写和数据重组。 | 简单且快,直接读取最终版本。 | 高 | 低 | 读密集型 (Read-Intensive) | B-Tree |
| 读时归并 (Merge-on-Read) | 写入时只进行追加(Append-only),将数据合并的复杂性推迟到读取时或后台处理。 | 简单且快,主要是顺序写入。 | 复杂且慢,需要读取多个版本并动态归并。 | 低 | 高 | 写密集型 (Write-Intensive) | LSM-tree |
实例分析:LSM-tree 在 Apache Doris 中的应用
Doris 通过类似 LSM-Tree 的结构写入数据,在后台通过 Compaction 机制不断将小文件合并成有序的大文件,同时也会处理数据的删除、更新等操作,解决写放大问题,Doris 主键 (unique) 模型,从 Doris 2.0 开始,除了原来的 Merge-on-Read(MoR),也引入了 Merge-on-Write(MoW)的存储方式,MoR 是为了写入做优化,而 MoW 是为了更快的分析性能做优化
版权声明