Paimon源码解读 -- Compaction-3.MergeSorter

前言

上篇文章Paimon源码解读 -- Compaction-1.MergeTreeCompactTask解析了Paimon-Compaction阶段的大概流程

其中Paimon的compaction操作由如下几个部分组成,

  1. SingleFileWriter和RollingFileWriter去执行写入和滚动文件操作 -- 详情看文章Paimon源码解读 -- Compaction-2.SingleFileWriter和RollingFileWriter
  2. ReducerMergeFunctionWrapper去执行聚合逻辑 -- 详情看文章Paimon源码解读 -- PartialUpdateMerge
  3. 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() -- 合并排序的入口

  1. 如果ioManager不为null 且读取器的数量(其实就是Sorted Run的数量) > sort-spill-threshold,则选择溢出合并spillMergeSort(),目的防止内存OOM
  2. 否则,走无溢出合并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() -- 无溢出合并排序

该方法是创建排序合并算法器的入口,流程如下

  1. 遍历当前Sorted Run创建的readers,调MergeTreeReaders.readerForRun()获取当前Sorted Run下所有DF文件创建且合起来的RecordReader,其实就是获取当前Sorted Run对应的RecordReader

如果当前Sorted Run下所有DF文件创建的RecordReader,有任何一个有问题,则全部关闭

  1. 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() -- 溢出合并排序

步骤如下:

  1. 根据Sorted Run文件进行排序,调readerForSection.estimateSize()
  2. 计算溢出的size大小 -- spillSize
  3. 切分文件
    1. 内存部分:从spillSize位置开始往后取,直接加入到readers中
    2. 溢出部分:调spill()单独处理后,放入readers中
  4. 最后调回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() -- 溢出处理

溢出处理的流程如下

  1. 校验IO管理器非空,因为要写入磁盘的临时文件,这个不能为null
  2. 初始化操作
    1. 创建临时文件通道channel
    2. 创建KeyValue序列化器
    3. 创建压缩块工厂类,指定压缩方式由配置'spill-compression'决定,默认是zstd,压缩块大小是64kb
  3. 创建带压缩的临时文件输出流 -- ChannelWriterOutputView是Paimon封装的字节流
  4. 遍历每个溢出的Sorted Run文件创建的对应的RecordReader,调的是MergeTreeReaders.readerForRun()
    1. 遍历批量记录
    2. 遍历当前批次的每条记录,并序列化写入压缩输出流中
    3. 释放内存
    4. 关闭输出流
    5. 记录临时文件的元数据(通道channel、块数、溢出文件的总写入的字节数)
  5. 返回临时文件读取器的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);
        }
    }
}

二.总结

  1. 整个流程 :在MergeTreeCompactiRewriter中的rewriteCompaction()去创建的RecordReader,该reader包含了排序算法合并器;因此,后面调这个reader去读取、写入等,就会自动调排序算法去操作,比如败者树、最小堆

  2. RecordReader的创建过程

    1. 遍历每个section调readerForSection()创建对应的一个ReaderSupplier,合并起来返回
    2. -> readerForSection()遍历该section下的每个Sorted Run文件,去创建对应的SizedReaderSupplier,然后调MergeSorted.mergeSort()去创建排序算法合并器
    3. -> MergeSorted.mergeSort()
      • 无溢出走mergeSortNoSpill()
        1. 遍历该Sorted Run下全部的DF文件
        2. 再去创建对应DF文件的RecordReader,合起来形成reader,
        3. 然后调SortMergeReader.createSortMergeReader(reader,...)创建排序合并读取器,这是排序合并算法的入口
      • 溢出走spillMergeSort()
        1. 先根据Sorted Run排序,截取文件[溢出部分, 内存部分]
        2. 溢出部分走spill()单独处理,内存部分不处理
        3. 最后将处理后的溢出部分(就是临时文件读取器的supplier)和内存部分合起来,调回mergeSortNoSpill()
  3. 溢出处理spill()

    1. 校验IO管理器非空,因为要写入磁盘的临时文件,这个不能为null
    2. 初始化操作
      1. 创建临时文件通道channel
      2. 创建KeyValue序列化器
      3. 创建压缩块工厂类,指定压缩方式由配置'spill-compression'决定,默认是zstd,压缩块大小是64kb
    3. 创建带压缩的临时文件输出流 -- ChannelWriterOutputView是Paimon封装的字节流
    4. 遍历每个溢出的Sorted Run文件创建的对应的RecordReaderMergeTreeReaders.readerForRun()创建的RecordReader
      1. 遍历批量记录
      2. 遍历当前批次的每条记录,并序列化写入压缩输出流中
      3. 释放内存
      4. 关闭输出流
      5. 记录临时文件的元数据(通道channel、块数、溢出文件的总写入的字节数)
    5. 返回临时文件读取器的supplier
相关推荐
码事漫谈2 小时前
C++链表环检测算法完全解析
后端
ShaneD7712 小时前
Spring Boot 实战:基于拦截器与 ThreadLocal 的用户登录校验
后端
老蒋新思维2 小时前
创客匠人深度洞察:创始人 IP 打造的非线性增长模型 —— 知识变现的下一个十年红利
大数据·网络·人工智能·tcp/ip·重构·数据挖掘·创客匠人
计算机学姐2 小时前
基于Python的商场停车管理系统【2026最新】
开发语言·vue.js·后端·python·mysql·django·flask
hans汉斯2 小时前
【人工智能与机器人研究】人工智能算法伦理风险的适应性治理研究——基于浙江实践与欧美经验的整合框架
大数据·人工智能·算法·机器人·数据安全·算法伦理·制度保障
aiopencode2 小时前
iOS 应用如何防止破解?从逆向链路还原攻击者视角,构建完整的反破解工程实践体系
后端
Lear2 小时前
【JavaSE】IO集合全面梳理与核心操作详解
后端
鱼弦2 小时前
redis 什么情况会自动删除key
后端
秋刀鱼 ..2 小时前
【IEEE出版】第五届高性能计算、大数据与通信工程国际学术会议(ICHBC 2025)
大数据·人工智能·python·机器人·制造·新人首发