目录
2.3.1 Reader读取next_row_with_aggregation
2.3.4 Segmentlterator读取next_batch
一、概述
1.1 存储结构的整体介绍
Doris是基于MPP架构的交互式SQL数据仓库,主要用于解决近实时的报表统计和多维分析。Doris高效导入、查询离不开其存储结构的设计。通过阅读Doris BE模块代码,分析Doris BE模块存储层的实现原理,主要包括Doris列存的设计、索引设计、数据读写流程、Compaction流程、Tablet数据分片和Rowset版本管理等功能。通过三篇文章来逐步进行阐述,分别为:
本文章介绍了Doris读取流程及Compaction流程
二、读取流程
2.1 整体读取流程
读取流程为写入的逆过程,读取流程相对复杂些,主要因为进行大量的读取优化。整个读取流程分为两个阶段,一个是init流程,一个是获取next_block数据块的过程。具体过程如下图所示:
层级关系如下:
- OlapScanner对—个tablet数据读取操作整体的封装;
- Reader对读取的参数进行处理,并提供了按三种不同模型读取的差异化处理;
- CollectIterator包含了tablet中多个RowsetReader,这些RowsetReader有版本顺序,CollectIterator将这些RowsetReader归并Merge成统一的lterator功能,提供了归并的比较器;
- RowsetReader则负责了对一个Rowset的读取;
- RowwiseIterator提供了一个Rowset中所有Segment的统一访问的lterator功能。这里的归并策略可以根据数据排序的情况采用Merge或Union;
- Segmentlterator对应了一个Segment的数据读取,Segment的读取会根据查询条件与索引进行计算,找到读取的对应行号信息,定位到对应的数据页page,对数据进行读取。其中,经过过滤条件后,会对可访问的行信息生成bitmap来记录,BitmapRangelterator代表单独实现的,可以按照范围访问这个bitmap的迭代器;
- Columnlterator提供了对列的相关数据和索引统一访问的迭代器。ColumnReader以及各个IndexReader等分别对应具体的数据和索引信息。
2.2 读取init阶段的主要流程
init阶段的执行流程如下:
2.2.1 OlapScanner 查询参数构造
- 根据查询指定的version版本查找出需要读取的RowsetReader(依赖于版本管理的rowset_graph版本路径图,取得查询version范围的最短路径);
- 设置查询信息,包括_tablet、读取类型reader_type=READER_QUERY、是否进行聚合、_version (从o到指定版本);
- 设置查询条件信息,包括filter过滤字段、is_nulls字段;
- 设置返回列信息;
- 设置查询的key_ranges范围(key的范围数组,可以通过short key index前缀索引进行过滤);
- 初始化Reader对象;
2.2.2 Reader的Init流程
- 初始化conditions查询条件对象;
- 初始化bloomFilter列集合(eq、in条件,添加了bloomFilter的列);
- 初始化delete_handler,包括了tablet中存在的所有删除信息,其中包括了版本和对应的删除条件数组;
- 初始化传递给下层要读取返回的列,包括了返回值和条件对象中的列;
- 初始化key_ranges的start key、end key对应的RowCusor行游标对象等;
- 构建的信息设置RowsetReader、Collectlterator。Rowset对象进行初始化,将RowsetReader加入到Collectterator中;
- 调用Collectlterator获取当前行(这里其实为第一行),这里开启读取流程;
2.2.3 RowsetReader的Init流程
- 构建Segmentlterator并过滤掉delete_handler中比当前Rowset版本小的删除条件;
- 构建Rowwiselterator (是对SegmentIterator的聚合iterator),将要读取的Segmentlterator加入到Rowwiselterator。当所有的Segment为整体有序时,采用union iterator顺序读取的方式,否则采用merge iterator归并读取的方式。
2.2.4 Segmentlterator的Init流程
- 初始化ReadableBlock,用来读取当前的Segment文件的对象,实际读取文件;
- 初始化_row_bitmap,用来存储通过索引过滤后的行号,使用bitmap结构;
- 构建Columnlterator,这里仅是需要读取列;
- 如果Column有BitmapIndex索引,初始化每个Column的Bitmaplndexlterator;
- 通过SortkeyIndex前缀索引过滤数据;
- 借助各种索引,按条件过滤数据,条件包括查询条件和删除条件过滤信息。
2.3 读取next阶段的主要流程
next阶段的执行流程如下:
2.3.1 Reader读取next_row_with_aggregation
在reader读取时预先读取一行,记录为当前行。在被调用next返回结果时会返回当前行,然后再预先读取下一行作为新的当前行,reader的读取会根据模型的类型分为三种情况:
- _dup_key_next_row读取(明细数据模型)下,返回当前行,再直接读取Collectorlterator读取next作为当前行;
- agg_key_next_row读取(聚合模型)下,取Collectorlterator读取next之后,判断下一行是否与当前行的key相同,相同时则进行聚合计算,循环读取下一行;不相同则返回当前累计的聚合结果,更新当前行;
- _unique_key_next_row读取(unique key模型)下,与_agg_key_next_row模型方式逻辑相同,但存在一些差异。由于支持了删除操作,会查看聚合后的当前行是否标记为删除行。如果为删除行舍弃数据,直到找到一个不为删除行的数据才进行返回。
2.3.2 Collectlterator读取next
Collectlterator中使用heap(堆)数据结构维护了要读取RowsetReader集合,比较规则为:key相同时比较Rowset的版本。
2.3.3 RowsetReader读取next
- RowsetReader直接读取了Rowwiselterator的next_batch;
- Rowwiseterator整合了Segmenterator。当Rowset中所有Segment整体有序时直接按Union方式迭代返回,无序时按Merge归并方式返回。Rowwiselterator返回当前最大的SegmentIterator的行数据,每次会调用SegmentIterator的next_batch获取数据。
2.3.4 Segmentlterator读取next_batch
- 根据init阶段构造的BitmapRangerInterator,使用next_range每次取出要读取的行号的一个范围range_from、 range_to;
- 先读取条件列从range_from到range_to行的数据。过程如下:
- 调用有的条件列的各个columnlterator的seek_to_ordinal,各个列的读取位置current_rowid定位到Segmentlterator的cur_rowid。这里是通过二分查ordinal_index(一级索引)对齐到对应的data page(数据页)。
- 读出条件列的数据,按条件再进行一次过滤(这次是精确的过滤)。
- 再读取无条件列的数据,放入到Rowblock中,返回Rowblock。
三、Compaction流程
3.1 Compaction整体介绍
3.1.1 Compaction概述
Doris 通过类似 LSM-Tree 的结构写入数据,后台通过 Compaction机制不断将小文件合并成有序的大文件。对于单一的数据分片(tablet),数据会按照顺序写入内存(写缓存memstore),达到阈值后刷写到磁盘,这些文件保存在一个rowset中。在Doris中,Compaction机制根据一定的策略对这些rowset合并成有序的大文件,极大地提升查询性能。
3.1.2 Rowset数据版本
Doris 通过类似 LSM-Tree 的结构写入数据,后台通过 Compaction机制不断将小文件合并成有序的大文件。对于单一的数据分片(tablet),数据会按照顺序写入内存(写缓存memstore),达到阈值后刷写到磁盘,这些文件保存在一个rowset中。在Doris中,Compaction机制根据一定的策略对这些rowset合并成有序的大文件,极大地提升查询性能。
一个tablet中包含若干连续的rowset(rowset是逻辑概念),rowset代表tablet中一次数据变更的数据集合(数据变更包括了数据新增,更新或删除等)。rowset按版本信息进行记录,版本信息中包含了两个字段first和second,first表示当前rowset的起始版本(start version),end表示当前rowset的结束版本(endversion)。
Doris的数据写入是以微批的方式进行的,每一个批次的数据针对每个tablet都会形成一个rowset(一个tablet是由多个rowset组成的)。每个rowset都有一个相应的起始版本(start version)和终止版本(end version)。对于新增的rowset,起始版本和终止版本是一样的,表示为[ 6-6]、[ 7-7]等。多个 rowset经过compaction会形成一个大的rowset。合并后的起始版本和终止版本是多个版本的并集,如[ 6-6]、[ 7-7]、[8-8]合并后变成 [6-8],如下图:
3.1.3 Compaction执行方式
Compaction执行方式可以分为两种类型:base compaction和cumulative compaction,其中cumulative compaction(简称CC)负责将多个最新导入的增量数据进行合并,当增量数据合并后的大小达到一定阈值后,base compaction(简称BC)将基线版本(起始版本start version为0的数据)和与该增量数据版本合并。BC操作因为涉及到基线数据,而基线数据通常比较大,所以操作耗时会比CC长。
这两种compaction的边界通过cumulative point (简称CP)来确定。CP是一个动态变化的版本号,比CP小的数据版本只能触发BC,而比CP大的数据版本,只会触发CC。
3.2 Compaction详细流程
Compaction合并整体流程如下图所示:
(1)计算cumulative_point;
(2)选择compaction需要合并的InputRowsets集合:
- base compaction选取条件:(1)如果存在大于5个非cumulative的rowset,将所有非cumulative的rowset进行合并;(2)版本first为0的base rowset与其他非cumulative的磁盘比例小于10:3时,合并所有非cumulative的rowset进行合并;
- cumulative compaction选取条件:(1)当Rowset集合中segment数量>=5且<=1000时,进行合并;(2)当输出Rowset数量小于5,但存在删除条件版本大于Rowset second版本时,进行合并(让删除的Rowset快速合并进来);(3)当累计的base compaction和cumulative compaction都时间大于1天时,进行合并;
(3)执行compaction;
(4)更新cumulative_point:
- 更新cumulative_point,将cumulative compaction产出的OutputRowset交给后续的base compaction流程。
ps:对于aggregation key聚合模型、unique key主键模型而言,compaction功能可以使得分散在不同Rowset,而key值相同的数据进行合并,达到了预计算的效果。此外,开启compaction后减少了Rowset文件数量,从而提升了查询效率。
ps:compaction合并机制的详细内容见文章:
四、总结
本文详细介绍了Doris系统的底层存储层的读取相关流程。
读取流程依赖于完全的列存实现,对于OLAP的宽表场景(读取大量行,少量列)能够快速扫描, 基于多种索引功能进行过滤(包括Short Key Index前缀索引、Ordinal Index一级索引、ZoneMap索引、 Bitmap位图索引、BloomFilter 索引等),能够跳过大量的数据扫描。此外,Compaction机制根据一定的策略对多个小rowset合并成有序的大文件,极大地提升查询性能。
参考文章:
【Doris】存储层设计介绍3——读取流程、Compaction流程分析_doris 存储层设计介绍 3—— 读取流程、compaction 分析-CSDN博客