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
相关推荐
whinc1 小时前
Rust技术周刊 2026年第17周
后端·rust
whinc1 小时前
Rust技术周刊 2026年第18周
后端·rust
whinc2 小时前
Rust技术周刊 2026年第16周
后端·rust
方向研究2 小时前
盈利因子策略
大数据
jieyucx2 小时前
Go语言深度解剖:Map扩容机制全解析(增量扩容+等量扩容+渐进式迁移)
开发语言·后端·golang·map·扩容策略
Slow菜鸟2 小时前
Codex CLI 教程(五)| Skills 安装指南:面向 Java 全栈工程师打造个人 ECC(V1版)
大数据·前端·人工智能
王码码20352 小时前
Go语言的内存管理:原理与实战
后端·golang·go·接口
Lee川2 小时前
打字机是怎么炼成的:Chat 流式输出深度解析
前端·后端·面试
Lee川3 小时前
Token 无感刷新与 Logout:前端安全会话管理实战
前端·后端·react.js
狒狒热知识3 小时前
2026品效合一深度落地:软文营销平台重构企业品牌与业绩双增长新路径
大数据·人工智能·重构