前言
上篇文章Paimon源码解读 -- Compaction-1.MergeTreeCompactTask解析了Paimon-Compaction阶段的大概流程
其中Paimon的compaction操作由如下几个部分组成,
- 用
SingleFileWriter和RollingFileWriter去执行写入和滚动文件操作 -- 详情看文章Paimon源码解读 -- Compaction-2.SingleFileWriter和RollingFileWriter - 用
ReducerMergeFunctionWrapper去执行聚合逻辑 -- 详情看文章Paimon源码解读 -- PartialUpdateMerge - 用
readerForMergeTree()最后由SortMergeReader去执行特定的合并算法,去将文件进行排序合并重写
一.流程
回顾MergeTreeCompactiRewriter中的rewriteCompaction()的部分代码

1.readerForMergeTree()
java
protected <T> RecordReader<T> readerForMergeTree(
List<List<SortedRun>> sections, MergeFunctionWrapper<T> mergeFunctionWrapper)
throws IOException {
// 调MergeTreeReaders.readerForMergeTree()
return MergeTreeReaders.readerForMergeTree(
sections,
readerFactory,
keyComparator,
userDefinedSeqComparator,
mergeFunctionWrapper,
mergeSorter);
}
2.MergeTreeReaders类
(1) readerForMergeTree()
遍历每个 section,为每个 section 创建一个 ReaderSupplier,这个 ReaderSupplier 通过调用 readerForSection() 方法来获取相应的 RecordReader。
最后,使用 ConcatRecordReader.create() 方法将所有创建的 ReaderSupplier 合并成一个 RecordReader 并返回。
java
// 创建RecordReader
public static <T> RecordReader<T> readerForMergeTree(
List<List<SortedRun>> sections,
FileReaderFactory<KeyValue> readerFactory,
Comparator<InternalRow> userKeyComparator,
@Nullable FieldsComparator userDefinedSeqComparator,
MergeFunctionWrapper<T> mergeFunctionWrapper,
MergeSorter mergeSorter)
throws IOException {
// 遍历每个 section,为每个 section 创建一个 ReaderSupplier,这个 ReaderSupplier 通过调用 readerForSection 方法来获取相应的 RecordReader。
// 最后,使用 ConcatRecordReader.create 方法将所有的 ReaderSupplier 合并成一个 RecordReader 并返回。
List<ReaderSupplier<T>> readers = new ArrayList<>();
for (List<SortedRun> section : sections) {
readers.add(
() ->
readerForSection(
section,
readerFactory,
userKeyComparator,
userDefinedSeqComparator,
mergeFunctionWrapper,
mergeSorter));
}
return ConcatRecordReader.create(readers);
}
(2) readerForSection() -- 关键入口
遍历当前section中的每个Sorted Run,为其创建一个 SizedReaderSupplier
这个 SizedReaderSupplier 实现了估计大小和获取记录读取器的方法,其中获取记录读取器通过调用 readerForRun 方法实现。
最后是关键:使用 MergeSorter.mergeSort() 方法,将这些 SizedReaderSupplier 进行合并排序重写文件,并应用用户定义的键比较器、序列比较器和合并函数包装器,返回该 section 最终的 RecordReader。
java
// 创建一个section对应的RecordReader
public static <T> RecordReader<T> readerForSection(
List<SortedRun> section,
FileReaderFactory<KeyValue> readerFactory,
Comparator<InternalRow> userKeyComparator,
@Nullable FieldsComparator userDefinedSeqComparator,
MergeFunctionWrapper<T> mergeFunctionWrapper,
MergeSorter mergeSorter)
throws IOException {
// 遍历 section 中的每个 SortedRun,为每个 SortedRun 创建一个 SizedReaderSupplier。
// 这个 SizedReaderSupplier 实现了估计大小和获取记录读取器的方法,其中获取记录读取器通过调用 readerForRun 方法实现。
// 最后,使用 mergeSorter 的 mergeSort 方法,将这些 SizedReaderSupplier 进行合并排序,并应用用户定义的键比较器、序列比较器和合并函数包装器,返回该 section 最终的 RecordReader。
List<SizedReaderSupplier<KeyValue>> readers = new ArrayList<>();
for (SortedRun run : section) {
readers.add(
new SizedReaderSupplier<KeyValue>() {
@Override
public long estimateSize() {
return run.totalSize();
}
@Override
public RecordReader<KeyValue> get() throws IOException {
return readerForRun(run, readerFactory);
}
});
}
return mergeSorter.mergeSort(
readers, userKeyComparator, userDefinedSeqComparator, mergeFunctionWrapper); // 这是排序合并重写文件的关键入口
}
(3) readerForRun()
遍历当前 Sorted Run 中的每个数据文件(DataFileMeta),为每个文件创建一个 ReaderSupplier,该 ReaderSupplier 通过调用FileReaderFactory实现类.createRecordReader()方法来创建相应的记录读取器。
最后,使用 ConcatRecordReader.create() 方法将这些 ReaderSupplier 合并成一个 RecordReader 并返回。
java
private static RecordReader<KeyValue> readerForRun(
SortedRun run, FileReaderFactory<KeyValue> readerFactory) throws IOException {
List<ReaderSupplier<KeyValue>> readers = new ArrayList<>();
// 遍历 Sorted Run 中的每个数据文件(DataFileMeta),为每个文件创建一个 ReaderSupplier,该 ReaderSupplier 通过调用 readerFactory 的 createRecordReader 方法来创建相应的记录读取器。
//最后,使用 ConcatRecordReader.create 方法将这些 ReaderSupplier 合并成一个 RecordReader 并返回。
for (DataFileMeta file : run.files()) {
readers.add(() -> readerFactory.createRecordReader(file));
}
return ConcatRecordReader.create(readers);
}
3.MergeSorter类
(1) 属性和构造函数 -- 参数解析
java
private final RowType keyType; // key的行类型
private RowType valueType; // value的行类型
private final SortEngine sortEngine; // 排序算法,由配置'sort-engine'绑定,默认是败者树loser-tree,也可设置为最小堆min-heap
private final int spillThreshold; // 溢出阈值,由配置'sort-spill-threshold'绑定,不能小于2,若没配置该属性,则取num-sorted-run.stop-trigger值(该参数最低是8,因为没配置的话,取num-sorted-run.compaction-trigger默认是5 + 3);
private final CompressOptions compression; // 压缩方式,由配置'spill-compression'绑定,默认是zstd
private final MemorySegmentPool memoryPool; // 内存池,由配置'sort-spill-buffer-size'绑定,默认是64mb
@Nullable private IOManager ioManager; // IO管理器
public MergeSorter(
CoreOptions options,
RowType keyType,
RowType valueType,
@Nullable IOManager ioManager) {
this.sortEngine = options.sortEngine();
this.spillThreshold = options.sortSpillThreshold();
this.compression = options.spillCompressOptions();
this.keyType = keyType;
this.valueType = valueType;
this.memoryPool =
new CachelessSegmentPool(options.sortSpillBufferSize(), options.pageSize());
this.ioManager = ioManager;
}
(2) mergeSort() -- 合并排序的入口
- 如果ioManager不为null 且读取器的数量(其实就是Sorted Run的数量) >
sort-spill-threshold,则选择溢出合并spillMergeSort(),目的防止内存OOM - 否则,走无溢出合并
mergeSortNoSpill()
java
// 合并排序方法的入口,由MergeTreeReaders.readerForSection()调用
public <T> RecordReader<T> mergeSort(
List<SizedReaderSupplier<KeyValue>> lazyReaders,
Comparator<InternalRow> keyComparator,
@Nullable FieldsComparator userDefinedSeqComparator,
MergeFunctionWrapper<T> mergeFunction)
throws IOException {
// 如果ioManager不为null 且读取器的数量(其实就是Sorted Run的数量) > sort-spill-threshold,则选择溢出合并,目的防止内存OOM
if (ioManager != null && lazyReaders.size() > spillThreshold) {
return spillMergeSort(
lazyReaders, keyComparator, userDefinedSeqComparator, mergeFunction);
}
// 否则,走无溢出合并
return mergeSortNoSpill(
lazyReaders, keyComparator, userDefinedSeqComparator, mergeFunction);
}
(3) mergeSortNoSpill() -- 无溢出合并排序
该方法是创建排序合并算法器的入口,流程如下
- 遍历当前Sorted Run创建的readers,调
MergeTreeReaders.readerForRun()获取当前Sorted Run下所有DF文件创建且合起来的RecordReader,其实就是获取当前Sorted Run对应的RecordReader
如果当前Sorted Run下所有DF文件创建的RecordReader,有任何一个有问题,则全部关闭
- 调
SortMergeReader.createSortMergeReader()创建排序合并读取器,这是排序合并算法的入口
java
// 无溢出合并的方法
public <T> RecordReader<T> mergeSortNoSpill(
List<? extends ReaderSupplier<KeyValue>> lazyReaders,
Comparator<InternalRow> keyComparator,
@Nullable FieldsComparator userDefinedSeqComparator,
MergeFunctionWrapper<T> mergeFunction)
throws IOException {
List<RecordReader<KeyValue>> readers = new ArrayList<>(lazyReaders.size());
for (ReaderSupplier<KeyValue> supplier : lazyReaders) {
try {
readers.add(supplier.get()); // 底层调MergeTreeReaders.readerForRun()获取当前Sorted Run下所有DF文件创建且合起来的RecordReader,其实就是获取当前Sorted Run对应的RecordReader
} catch (IOException e) {
// if one of the readers creating failed, we need to close them all.
// 如果当前Sorted Run下所有DF文件创建的RecordReader,有任何一个有问题,则全部关闭
readers.forEach(IOUtils::closeQuietly);
throw e;
}
}
return SortMergeReader.createSortMergeReader(
readers, keyComparator, userDefinedSeqComparator, mergeFunction, sortEngine); // 创建一个排序合并读取器,这是创建排序合并算法器的入口
}
(4) spillMergeSort() -- 溢出合并排序
步骤如下:
- 根据Sorted Run文件进行排序,调
readerForSection.estimateSize() - 计算溢出的size大小 -- spillSize
- 切分文件
- 内存部分:从spillSize位置开始往后取,直接加入到readers中
- 溢出部分:调
spill()单独处理后,放入readers中
- 最后调回
mergeSortNoSpill()处理
java
// 溢出合并的方法
private <T> RecordReader<T> spillMergeSort(
List<SizedReaderSupplier<KeyValue>> inputReaders,
Comparator<InternalRow> keyComparator,
@Nullable FieldsComparator userDefinedSeqComparator,
MergeFunctionWrapper<T> mergeFunction)
throws IOException {
// 1.根据Sorted Run文件进行排序,调的是readerForSection.estimateSize()
List<SizedReaderSupplier<KeyValue>> sortedReaders = new ArrayList<>(inputReaders);
sortedReaders.sort(Comparator.comparingLong(SizedReaderSupplier::estimateSize));
// 2,计算溢出的size大小
int spillSize = inputReaders.size() - spillThreshold;
// 3.获取内存内保留的读取器:数量 = spillThreshold,从spillSize位置取排序后最后面的大文件
List<ReaderSupplier<KeyValue>> readers =
new ArrayList<>(sortedReaders.subList(spillSize, sortedReaders.size()));
// 4.将溢出的部分调spill()去单独处理,处理后,加入到readers中
for (ReaderSupplier<KeyValue> supplier : sortedReaders.subList(0, spillSize)) {
readers.add(spill(supplier));
}
// 5.最后调回mergeSortNoSpill()处理
return mergeSortNoSpill(readers, keyComparator, userDefinedSeqComparator, mergeFunction);
}
(5) spill() -- 溢出处理
溢出处理的流程如下
- 校验IO管理器非空,因为要写入磁盘的临时文件,这个不能为null
- 初始化操作
- 创建临时文件通道
channel - 创建KeyValue序列化器
- 创建压缩块工厂类,指定压缩方式由配置'spill-compression'决定,默认是zstd,压缩块大小是64kb
- 创建临时文件通道
- 创建带压缩的临时文件输出流 --
ChannelWriterOutputView是Paimon封装的字节流 - 遍历每个溢出的
Sorted Run文件创建的对应的RecordReader,调的是MergeTreeReaders.readerForRun()- 遍历批量记录
- 遍历当前批次的每条记录,并序列化写入压缩输出流中
- 释放内存
- 关闭输出流
- 记录临时文件的元数据(通道channel、块数、溢出文件的总写入的字节数)
- 返回临时文件读取器的
supplier
java
// 将溢出的部分,写入磁盘临时文件
private ReaderSupplier<KeyValue> spill(ReaderSupplier<KeyValue> readerSupplier)
throws IOException {
// 0.校验IO管理器非空,因为要写入磁盘的临时文件,这个不能为null
checkArgument(ioManager != null);
// 1.初始化操作
// (1) 创建临时文件通道
FileIOChannel.ID channel = ioManager.createChannel();
// (2) 创建KeyValue序列化器
KeyValueWithLevelNoReusingSerializer serializer =
new KeyValueWithLevelNoReusingSerializer(keyType, valueType);
// (3) 创建压缩块工厂类,指定压缩方式由配置'spill-compression'决定,默认是zstd,压缩块大小是64kb
BlockCompressionFactory compressFactory = BlockCompressionFactory.create(compression);
int compressBlock = (int) MemorySize.parse("64 kb").getBytes();
// 2.创建带压缩的临时文件输出流,这里的ChannelWriterOutputView是Paimon封装的字节流
ChannelWithMeta channelWithMeta;
ChannelWriterOutputView out =
FileChannelUtil.createOutputView(
ioManager, channel, compressFactory, compressBlock);
// 3.遍历每个溢出的Sorted Run文件创建的对应的RecordReader,调的是MergeTreeReaders.readerForRun()
try (RecordReader<KeyValue> reader = readerSupplier.get(); ) {
RecordIterator<KeyValue> batch;
KeyValue record;
// (1) 遍历批量记录
while ((batch = reader.readBatch()) != null) {
// (2) 遍历批量中的每条记录,并序列化写入压缩输出流中
while ((record = batch.next()) != null) {
serializer.serialize(record, out);
}
// (3) 释放批量内存(避免批次数据堆积)
batch.releaseBatch();
}
} finally {
// (4) 最终关闭输出流,确保数据刷盘
out.close();
// (5) 记录临时文件元数据(块数、总写入字节数,供后续读取优化)
channelWithMeta =
new ChannelWithMeta(channel, out.getBlockCount(), out.getWriteBytes());
}
// 4.返回临时文件读取器的supplier
return new SpilledReaderSupplier(
channelWithMeta, compressFactory, compressBlock, serializer);
}
4.调用的SortMergeReader.createSortMergeReader() -- 创建排序合并器
该方法就是根据配置的'sort-engine',去创建对应的算法排序合并器,默认是loser-tree
java
public interface SortMergeReader<T> extends RecordReader<T> {
// 由MergeSorter.mergeSortNoSpill()调用,该方法是创建排序合并算法器的入口
static <T> SortMergeReader<T> createSortMergeReader(
List<RecordReader<KeyValue>> readers,
Comparator<InternalRow> userKeyComparator,
@Nullable FieldsComparator userDefinedSeqComparator,
MergeFunctionWrapper<T> mergeFunctionWrapper,
SortEngine sortEngine) {
// 根据配置的'sort-engine',去创建对应的算法排序合并器,默认是loser-tree
switch (sortEngine) {
case MIN_HEAP: // 最小堆min-heap算法
return new SortMergeReaderWithMinHeap<>(
readers, userKeyComparator, userDefinedSeqComparator, mergeFunctionWrapper);
case LOSER_TREE: // 败者树loser-tree算法
return new SortMergeReaderWithLoserTree<>(
readers, userKeyComparator, userDefinedSeqComparator, mergeFunctionWrapper);
default:
throw new UnsupportedOperationException("Unsupported sort engine: " + sortEngine);
}
}
}
二.总结
-
整个流程 :在
MergeTreeCompactiRewriter中的rewriteCompaction()去创建的RecordReader,该reader包含了排序算法合并器;因此,后面调这个reader去读取、写入等,就会自动调排序算法去操作,比如败者树、最小堆 -
RecordReader的创建过程:- 遍历每个section调
readerForSection()创建对应的一个ReaderSupplier,合并起来返回 - ->
readerForSection()遍历该section下的每个Sorted Run文件,去创建对应的SizedReaderSupplier,然后调MergeSorted.mergeSort()去创建排序算法合并器 - ->
MergeSorted.mergeSort()- 无溢出走
mergeSortNoSpill():- 遍历该
Sorted Run下全部的DF文件 - 再去创建对应DF文件的
RecordReader,合起来形成reader, - 然后调
SortMergeReader.createSortMergeReader(reader,...)创建排序合并读取器,这是排序合并算法的入口
- 遍历该
- 溢出走
spillMergeSort():- 先根据
Sorted Run排序,截取文件[溢出部分, 内存部分] - 溢出部分走
spill()单独处理,内存部分不处理 - 最后将处理后的溢出部分(就是临时文件读取器的
supplier)和内存部分合起来,调回mergeSortNoSpill()
- 先根据
- 无溢出走
- 遍历每个section调
-
溢出处理
spill():- 校验IO管理器非空,因为要写入磁盘的临时文件,这个不能为null
- 初始化操作
- 创建临时文件通道
channel - 创建KeyValue序列化器
- 创建压缩块工厂类,指定压缩方式由配置'spill-compression'决定,默认是zstd,压缩块大小是64kb
- 创建临时文件通道
- 创建带压缩的临时文件输出流 --
ChannelWriterOutputView是Paimon封装的字节流 - 遍历每个溢出的
Sorted Run文件创建的对应的RecordReader是MergeTreeReaders.readerForRun()创建的RecordReader- 遍历批量记录
- 遍历当前批次的每条记录,并序列化写入压缩输出流中
- 释放内存
- 关闭输出流
- 记录临时文件的元数据(通道channel、块数、溢出文件的总写入的字节数)
- 返回临时文件读取器的
supplier