目录
一、Compaction概述
1.1 LSM-Tree概述
1.2 Compaction概述
1.3 Rowset数据版本
1.4 Compaction优点
1.5 Compaction问题
1.5.1 Compaction速度低
1.5.2 写放大问题
1.6 Compaction调优
1.6.1 业务侧
1.6.2 运维侧
二、Compaction执行方式
2.1 Vertical Compaction
2.1.1 概述
2.1.2 原理
2.2 Segment Compaction
2.2.1 概述
2.2.2 原理
注:本篇文章阐述的是Doris2.0版本的compaction机制
LSM-Tree( Log Structured-Merge Tree)是数据库中最为常见的存储结构之一,核心思想是充分发挥磁盘连续读写的性能优势,以短时间的内存和IO磁盘开销换取最大的写入性能,数据以Append-only的方式写入Memtable ,达到阈值后冻结Memtable并Flush为磁盘文件,再结合compaction机制将多个小文件进行多路归并排序形成新的文件,最终实现数据的高效写入。为了降低读取时需要合并的数据量,基于LSM-Tree的系统会引入后台数据合并的逻辑,以一定策略定期的对数据进行合并,Doris中这种机制被称为Compaction。 Doris中每批次的数据写入会生成一个数据版本,因此Compaction机制就是异步将底层小的rowset数据版本合并成一个更大的版本。
Doris 通过类似 LSM-Tree 的结构写入数据,后台通过 Compaction机制不断将小文件合并成有序的大文件。对于单一的数据分片(tablet),数据会按照顺序写入内存(写缓存memstore),达到阈值后刷写到磁盘,这些文件保存在一个rowset中。在Doris中,Compaction机制根据一定的策略对这些rowset合并成有序的大文件,极大地提升查询性能。
ps: Doris中数据组织如下图:
将数据表按照分区分桶规则,切分成若干个数据分片(tablet)存储在不同的be节点上。每个tablet都有多个副本(默认是3副本)。compaction是在每个be上独立进行的,compaction逻辑处理的就是一个be节点上所有的数据分片tablet。
一个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],如下图:
有个疑问:单个tablet中的rowset版本个数过多会什么影响?
主要影响两个方面,一个是be存储节点的内存占用,当rowset的版本过多时,be节点的table_meta部分(主要是其中的rowset元数据部分)占用的内存可能非常多。同时compaction任务就会消耗大量内存与磁盘IO,资源开销较大容易引起oom,影响集群稳定性;二是查询会变慢,查询过程需要对tablet中的数据进行解压处理,当rowset版本很多时,数据解压会变慢,导致查询scan的耗时增加。
每个rowset内部的数据是按主键有序的,但是rowset与rowset之间的数据是无序的,compaction会将多个rowset的数据从无序变成有序,提升数据再读取时的效率。
消除数据变更
数据以Append-only的方式写入,因此Delete,Update等操作都是标记写入,compaction会将标记的数据进行真正的删除或更新,避免数据再读取时进行额外的扫描及过滤。
在aggregate模型上,compaction还可以将不同的rowset中相同key的数据进行预聚合,减少数据读取时的局和计算,进一步提升读取效率
虽然compaction在写入和查询性能方面发挥着关键作用,但是compaction任务执行期间的写放大问题以及随之而来的磁盘I/O和CPU资源开销,也会影响系统稳定性和性能。
不用应用场景,数据写入需求,写入任务并行度,单次提交数据量的大小,提交频次的高低等因素影响compaction策略,不合理的compaction策略则会导致:
在高频写入场景下,短时间内生成的rowset版本太快,如果compaction不及时,就会造成大量版本堆积,最终导致写入失败(-238:OLAP_ERR_TOO_MANY_SEGMENTS);
理论上每次导入操作,不论是只导入一条还是十万、百万条,对于Doris来说,都是只生成一个新的roswet版本。那么在compaction效率有限的情况下,完全可以通过“攒微批+降频率”来规避roswet版本过多的问题。
Compaction本质上是将已经写入的数据读取后,重新写回的过程(读取多个小文件,合并成有序的大文件后再写回),这种重复的数据写入被称为写放大。一个好的compaction策略应该在保证效率的前提下,尽量降低写放大系数,因为过多的compaction会占用大量的内存及磁盘io资源,影响Doris集群的稳定性及查询性能,可能会导致BE OOM。
针对上述的compaction问题,可以从业务侧及运维侧进行调优。
Doris中的Compaction分为 Base Compaction 与 Cumulative Compaction。Cumulative Compaction(简称CC)会将新导入的小版本进行快速合并,但是CC无法处理 Delete版本,所以CC在合并过程中若遇到 Delete 操作就会终止,并将当前Delete操作版本之前的所有版本进行一次合并,之后Base Compaction(简称BC)将基线版本与CC处理的版本合并。当 Delete 版本特别多时, CC的步长也会相应变短,只能合并少量的文件,导致CC不能很好的发挥小文件合并效果。
有些业务是实时写入数据,查询该数据的需求较多,此时可以将Compaction开的大一点以达到快速合并目的,避免影响查询性能。
而有些业务数据写入当天的分区,查询需求针对之前的分区,在这种情况下,可以适当的将Compaction 放的小一点,避免 Compaction 占用过大内存或 CPU 资源。在晚上的低峰阶段对新导入的小版本进行合并,这样对第二天查询效率也不会有很大影响。
适当降低 Base Compaction任务优先级并增加Cumulative Compaction优先级
上文已经介绍了Cumulative Compaction能够快速合并大量生成的小文件,而 Base Compaction 由于合并的文件较大,执行的时间也会相应变长,读写放大也会比较严重。所以调高Cumulative Compaction的优先级。
当收到版本积压报警时,可以动态调大Compaction参数,尽快消耗积压版本。
在Doris-2.0版本中,存在两种Compaction执行方式:
Vertical Compaction:用以彻底解决Compaction的内存问题以及大宽表场景下的数据合并;
Segment Compaction:用以彻底解决数据导入过程中的Segment文件过多问题;
Doris 1.2.2版本之前的compaction执行方式见:
第3.2章:Doris数据导入——Compaction机制-CSDN博客
在之前的版本中,Compaction 合并的基本单元为整行数据。由于Doris存储引擎采用列式存储,行Compaction 的方式对数据读取极其不友好,每次 Compaction 都需要加载所有列的数据,内存消耗极大,而这样的方式在宽表场景下也将带来内存的极大消耗。
Vertical Compaction天然与列式存储更加贴合,使用列组的方式进行数据合并,单次合并只需要加载部分列的数据,因此能够极大减少合并过程中的内存占用。Vertical Compaction算法解决了大宽表场景下的 Compaction 执行效率和资源开销问题。可以有效降低Compaction的内存开销,并提升 Compaction 的执行速度。在实际测试中,Vertical Compaction 使用内存仅为原有 Compaction 算法的 1/10,同时 Compaction 速率提升 15%。
Vertical Compaction 在Doris-2.0版本中默认关闭状态,开启和配置方法(BE 配置)
#可以开启Vertical Compaction合并功能
set enable_vertical_compaction = true
# 每个列组包含的列个数,经测试,默认5列一组compaction的效率及内存使用较友好
set vertical_compaction_num_columns_per_group = 5
# 用于配置vertical compaction之后落盘文件的大小,默认值256M,即:
set vertical_compaction_max_segment_size = 256*1024*1024
Vertical Compaction的执行流程如下图:
整体分为如下几个步骤:
切分列组:将输入 Rowset 按照列进行切分,所有的Key列一组、Value列按 N 个一组,切分成多个 Column Group;
N的个数可以通过参数调整:vertical_compaction_num_columns_per_group
上述参数代表:每个列组包含的列个数,经测试,默认5列一组compaction的效率及内存使用较友好。set vertical_compaction_num_columns_per_group = 5
Key 列合并:Key列的顺序就是最终数据的顺序,多个 Rowset的 Key列采用堆排序进行合并,产生最终有序的 Key 列数据。在产生 Key 列数据的同时,会同时产生用于标记全局序 RowSources。
Value 列的合并:逐一合并 Column Group 中的 Value 列,以 Key列合并时产生的 RowSources为依据对数据进行排序。
数据写入:数据按列写入,形成最终的 Rowset 文件。
Segment Compaction主要应对单批次大数据量的导入场景。和Vertical Compaction的触发机制不同,Segment Compaction允许我们在导入数据的同时,针对一批次数据内的多个Segment进行的合并操作,以有效控制 Segment 文件的数量。
Segment Compaction 在Doris-2.0版本中默认关闭状态,开启和配置方法(BE 配置)
#可以开启Segment Compaction合并功能
set enable_segcompaction = true;
#用于配置合并的间隔。默认每生成10个segment文件将会进行一次
set segcompaction_batch_size =10;
该参数一般设置为10-30,过大的值会增加segment compaction 的内存占用量。
在数据导入阶段,Doris 会先在内存中积攒数据,到达一定批次大小,Flush到磁盘形成一个个的Segment 文件。
大批量数据导入时会形成大量的 Segment 文件进而影响后续查询性能,基于此原因,Doris 对一批次导入的 Segment 文件数量做了限制,如果触发阈值,会报错 -238 (olap_err_too_many_segments) ,同时终止对应的导入任务。
此外,Doris引入Segment Compaction合并算法,允许我们在导入数据的同时,针对一批次数据内的多个Segment进行的合并操作,以有效控制 Segment 文件的数量。具体流程如下所示:
例如:如果单批次新增的Segment 数量超过一定阈值(例如 10个),会触发合并线程去异步执行合并任务。通过将每10 个Segment合并成一个新的Segment 并删除旧 Segment,导入完成后的实际 Segment 文件数量将下降 10 倍。
Segment Compaction会在数据导入的同时并行执行,在单批次大数据量导入的场景下,能够在不显著增加导入时间的前提下大幅降低文件个数,提升查询效率。
ps:如果导入操作本身已经耗尽了内存资源时,不建议使用 segment compaction 以免进一步增加内存压力使导入失败。
参考文章:
最佳实践|Apache Doris 在小米数据场景的应用实践与优化
资源消耗降低 90%,速度提升 50%,解读 Apache Doris Compaction 最新优化与实现