Paimon源码解读 -- Compaction-1.MergeTreeCompactTask

93 阅读11分钟

一.抽象父类CompactTask

其实现子类如下图

image.png

  • MergeTreeCompactTask是主键表的合并流程;
  • 其他两个,都是BucketedAppendCompactanager里面的内部类,也就是仅追加表的合并流程;

二.MergeTreeCompactTask

1.属性和构造函数

2.doCompact() -- 核心流程

(1) doCompact()

流程如下

  1. 遍历每个section文件(bucket中的有序分段组,主键范围无重叠),进行判断
    • CASE-1:当前section内有多个Sorted Run文件说明这些文件的主键存在重叠,必须要合并,加入到candidate待合并列表中
    • CASE-2:当前section内只有1个Sorted Run文件,需要遍历Data File进一步判断
      • SUB-CASE-1:当前file的文件大小 < minFileSize(默认是target-file-size的70%,小文件阈值),将其包装成单个Sorted Run加入待合并列表
      • SUB-CASE-2:当前file的文件大小 >= minFileSize,说明是大文件,需要调rewrite()先合并candidate中的文件得到result结果,再用该result去调upgrade()`升级当前file的level
  2. 遍历完后,再调rewrite()处理剩余待合并文件,避免遗漏
  3. 返回最终合并结果result
@Override
protected CompactResult doCompact() throws Exception {
    // 1. 初始化变量
    // candidate:待合并的SortedRun列表(每个元素是一个"需合并的Sorted Run集合")
    List<List<SortedRun>> candidate = new ArrayList<>();
    // result:合并结果容器(存储新生成的合并文件、升级的文件、删除向量文件等)
    CompactResult result = new CompactResult();

    // 核心逻辑:遍历当前分桶(Bucket)内的所有"有序分段组(section)"
    // 约束:不能跳过中间文件合并,否则会破坏数据按排序键的全局有序性
    // 层级关系:Bucket(分桶)→ 多个section(有序分段组,排序键范围无重叠)→ 每个section包含多个Sorted Run(有序分段,可能重叠)→ 每个Sorted Run包含多个Data File(物理文件)
    for (List<SortedRun> section : partitioned) {
        if (section.size() > 1) {
            // CASE-1:当前section内有多个Sorted Run(说明这些分段的排序键范围重叠/连续),因为不同的Sorted Run可能具有重叠的主键范围,因此查询的时候必须要合并
            // 必须加入待合并列表,通过合并保证有序性
            candidate.add(section);
        } else {
            // CASE-2:当前section内只有1个Sorted Run(说明无主键重叠)
            SortedRun run = section.get(0);
            // 遍历该Sorted Run中的所有物理文件(Data File)
            for (DataFileMeta file : run.files()) {
                if (file.fileSize() < minFileSize) { 
                    // SUB-CASE-1:文件大小 < minFileSize(默认是target-file-size的70%,小文件阈值)
                    // 小文件不单独处理,包装成单个Sorted Run加入待合并列表,积累后批量合并(减少IO开销)
                    candidate.add(singletonList(SortedRun.fromSingle(file)));
                } else {
                    // SUB-CASE-2:文件大小 ≥ minFileSize(大文件)
                    // 1. 先合并之前积累的所有待合并文件(避免小文件堆积)
                    rewrite(candidate, result);
                    // 2. 升级当前大文件的Level(仅调整文件层级,不重写文件内容,降低IO成本)
                    upgrade(file, result);
                }
            }
        }
    }
    // 3. 合并循环结束后,处理剩余未合并的待合并文件(避免遗漏)
    rewrite(candidate, result);
    // 4. 补充合并后的删除向量文件(如果表启用了删除向量,清理标记删除的数据)
    result.setDeletionFile(compactDfSupplier.get());
    // 返回最终合并结果(新文件、升级文件、删除文件)
    return result;
}

(2) 调用的rewrite()

逻辑如下:

  1. CASE-1:当前没有待合并的section文件,则return
  2. CASE-2:当前待合并的section文件只有一个,进一步判断
    1. 当前section文件没有Sorted Run文件,说明是空文件,return即可
    2. 当前section文件只有一个Sorted Run文件,遍历该Sorted Run下面的Data File物理文件,并调upgrade()去升级合并(可能重写文件,也可能不重写)
  3. 其他情况,说明有多个文件(要么多个section段,要么section内多个Sorted Run),都需要调rewriteImpl()去重写文件合并
// 合并重写的路由逻辑
private void rewrite(List<List<SortedRun>> candidate, CompactResult toUpdate) throws Exception {
    // 注意:section是List<SortedRun>
    // CASE-1: 如果没有待合并的section文件,则return退出
    if (candidate.isEmpty()) {
        return;
    }
    // CASE-2: 如果待合并的列表内只有1个section的文件,轻度升级
    if (candidate.size() == 1) {
        List<SortedRun> section = candidate.get(0);
        // 若当前section内没有SortedRun,则return退出
        if (section.size() == 0) {
            return;
        }
        // 若当前section内只有1个SortedRun,则遍历其中的所有物理文件(DataFile),调用upgrade()升级合并(可能重写文件,也可能不重写)
        else if (section.size() == 1) {
            for (DataFileMeta file : section.get(0).files()) {
                upgrade(file, toUpdate);
            }
            candidate.clear(); // 合并完,清空状态,避免重复合并
            return;
        }
    }
    // CASE-3: 待合并列表有多个section(SortedRun集合),或单个section内有多个SortedRun,走重度处理(重写文件)
    rewriteImpl(candidate, toUpdate);
}

(3) 调用的upgrade() -- 核心1

<1> upgrade()流程

升级+合并逻辑如下:

  1. 当前Data File的层级 == outputLevel(要合并到的目标层级),是该文件已经处理过了,直接return,防止重复处理
  2. 该文件的outputLevel不是最大Level 或者 当前Data File文件没有被删除过,那么调ChangelogMergeTreeRewriter.upgrade()进一步处理(可能重写,可能不重写),然后将结果合调toUpdate.merge()合并到最终结果中
  3. 其他情况,需要走rewriteImpl()重度合并
// 升级+合并的逻辑
private void upgrade(DataFileMeta file, CompactResult toUpdate) throws Exception {
    // CASE-1: 当前Data File文件已经在输出目标层级了,直接退出,防止重复处理
    if (file.level() == outputLevel) { // 这里的outputLevel是当前文件的输出level,比如L0层文件的outputLevel是L1
        return;
    }
    // CASE-2: 允许直接升级:调用rewriter.upgrade调整层级;判断依据:目标层级不是最大 Level 或者 当前Data File文件没有被删除过
    // 这里的maxLevel 和配置'num-levels'绑定,默认是none;若没配置该参数,则会取当前文件的最大level+1
    if (outputLevel != maxLevel || file.deleteRowCount().map(d -> d == 0).orElse(false)) {
        CompactResult upgradeResult = rewriter.upgrade(outputLevel, file); // 这里底层会调ChangelogMergeTreeRewriter.upgrade() 需要进一步分析
        toUpdate.merge(upgradeResult); // 将升级level后的结果合并到最终结果
        upgradeFilesNum++; // 统计升级文件数
    }
    // CASE-3: 不允许直接升级,需要走rewriteImpl()重写合并
    else {
        // files with delete records should not be upgraded directly to max level
        List<List<SortedRun>> candidate = new ArrayList<>();
        candidate.add(new ArrayList<>());
        candidate.get(0).add(SortedRun.fromSingle(file));
        rewriteImpl(candidate, toUpdate);
    }
}
<2> 底层CompactRewriter.upgrade()

CompactRewriter是个接口,其实现子类如下图 image.png

以ChangelogMergeTreeRewriter为例,流程如下

  1. 如果需要产生changelog,那么则调rewriteOrProduceChangelog()去重写文件或产生changelog
    1. 只有配置了'changelog-producer' = 'lookup'或'full-compaction',才会走生产changelog
    2. 且只有lookup模式的非deduplicate模型,才会既生成changelog,也rewrite
  2. 如果不需要,则直接调父类AbstractCompactRewriter.upgrade()其实就是将file的level升级一下,new一个新的outputLevel的Data File文件
@Override
public CompactResult upgrade(int outputLevel, DataFileMeta file) throws Exception {
    // 调实现子类的upgradeStrategy()方法
    UpgradeStrategy strategy = upgradeStrategy(outputLevel, file);
    // 如果需要产生changelog,那么则调`rewriteOrProduceChangelog()`去重写文件或产生changelog
    if (strategy.changelog) { // 只有配置了'changelog-producer' = 'lookup'或'full-compaction',才会走这里
    // 且只有lookup模式的非deduplicate模型,才会既生成changelog,也rewrite
        return rewriteOrProduceChangelog(
                outputLevel,
                Collections.singletonList(
                        Collections.singletonList(SortedRun.fromSingle(file))),
                forceDropDelete,
                strategy.rewrite);
    }
    // 不需要,则直接调父类AbstractCompactRewriter.upgrade() 其实就是将file的level升级一下,new一个新的outputLevel的Data File文件
    else {
        return super.upgrade(outputLevel, file);
    }
}
<3> UpgradeStrategy枚举类
/** Strategy for upgrade. */
protected enum UpgradeStrategy {
    NO_CHANGELOG_NO_REWRITE(false, false), // 不生成changelog,也不需要rewrite
    CHANGELOG_NO_REWRITE(true, false), // 生成changelog,但不需要rewrite
    CHANGELOG_WITH_REWRITE(true, true); // 生成changelog,也需要rewrite

    private final boolean changelog;
    private final boolean rewrite;

    UpgradeStrategy(boolean changelog, boolean rewrite) {
        this.changelog = changelog;
        this.rewrite = rewrite;
    }
}
《1》FullChangelogMergeTreeCompactRewriter的upgradeStrategy策略

仅当配置'changelog-producer' = 'full-compaction'时,才会走FullChangelogMergeTreeCompactRewriter,而它只有在配置了'num-levels',且当前file的目标层级为该参数的时候,才仅仅是产生changelog,而不rewrite

@Override
protected UpgradeStrategy upgradeStrategy(int outputLevel, DataFileMeta file) {
    // 只有当outputLevel = maxLevel(就是num.levels参数)的时候,才会返回CHANGELOG_NO_REWRITE(仅生产changelog,不rewrite)
    // 否则,全部都是不生产changelog,也不rewrite
    return outputLevel == maxLevel ? CHANGELOG_NO_REWRITE : NO_CHANGELOG_NO_REWRITE;
}
《2》LookupMergeTreeCompactRewriter的upgradeStrategy策略
@Override
protected UpgradeStrategy upgradeStrategy(int outputLevel, DataFileMeta file) {
    // 1.非L0层,不需要产生changelog,也不需要rewrite
    if (file.level() != 0) {
        return NO_CHANGELOG_NO_REWRITE;
    }

    // 2.当启用了删除向量,且文件有删除记录,那么需要产生changelog,也需要rewrite
    // 在deletionVector模式下,由于需要drop删除,所以当delete行数>0时需要重写
    if (dvMaintainer != null && file.deleteRowCount().map(cnt -> cnt > 0).orElse(true)) {
        return CHANGELOG_WITH_REWRITE;
    }
    // 3.当outputLevel = maxLevel(就是num.levels参数)的时候,需要产生changelog,但是不需要rewrite
    if (outputLevel == maxLevel) {
        return CHANGELOG_NO_REWRITE;
    }

    // 4.当前merge-engine是deduplicate 并且没有配置sequnce-field,则只需要产生changelog,不需要rewrite
    if (mergeEngine == MergeEngine.DEDUPLICATE && noSequenceField) {
        return CHANGELOG_NO_REWRITE;
    }

    // 5.其他引擎情况,需要产生changelog,也需要rewrite
    return CHANGELOG_WITH_REWRITE;
}
<4> 调用的rewriteOrProduceChangelog() -- 核心

其实调的是父类ChangelogMergeTreeRewriter.rewriteOrProduceChangelog(),流程如下

  1. 变量初始化
  2. 数据读取、写入器创建
    CASE-1: 需要重写压缩File,调KeyValueFileWriterFactory.createRollingMergeTreeFileWriter()创建滚动合并文件写入器,后续进行重写合并操作
    CASE-2: 需要产生changelog,调KeyValueFileWriterFactory.createRollingChangelogFileWriter()创建滚动变更日志文件写入器,后续进行changelog产生写入
  3. 遍历文件,然后该重写重写,该产生changelog产生changelog
  4. 资源和异常处理
  5. 返回结果(旧文件,新文件,changelog文件)
/**
 * 重写 或 生产changelog (也可能重写+生产changelog)
 * @param outputLevel: 目标文件层级
 * @param sections: 有序段
 * @param dropDelete: 是否生产删除向量
 * @param rewriteCompactFile // 是否需要重写压缩File
 * @return
 * @throws Exception
 */
private CompactResult rewriteOrProduceChangelog(
        int outputLevel,
        List<List<SortedRun>> sections,
        boolean dropDelete,
        boolean rewriteCompactFile)
        throws Exception {
    // 1.变量初始化
    CloseableIterator<ChangelogResult> iterator = null; // 用于遍历合并后的数据流
    RollingFileWriter<KeyValue, DataFileMeta> compactFileWriter = null; // 合并后的数据文件写入器
    RollingFileWriter<KeyValue, DataFileMeta> changelogFileWriter = null; // changelog文件写入器
    Exception collectedExceptions = null; // 捕获异常

    try {
        // 2.数据读取、写入器创建
        iterator =
                readerForMergeTree(sections, createMergeWrapper(outputLevel))
                        .toCloseableIterator(); // 通过readerForMergeTree创建并行读取器
        // CASE-1: 需要重写压缩File,创建滚动合并文件写入器,后续进行重写合并操作
        if (rewriteCompactFile) {
            compactFileWriter =
                    writerFactory.createRollingMergeTreeFileWriter(
                            outputLevel, FileSource.COMPACT);
        }
        // CASE-2: 需要产生changelog,创建滚动变更日志文件写入器
        if (produceChangelog) {
            changelogFileWriter = writerFactory.createRollingChangelogFileWriter(outputLevel);
        }
        // 3.遍历文件,然后该重写重写,该产生changelog产生changelog
        while (iterator.hasNext()) {
            ChangelogResult result = iterator.next();
            KeyValue keyValue = result.result();
            // 写入合并后的数据文件
            if (compactFileWriter != null
                    && keyValue != null
                    && (!dropDelete || keyValue.isAdd())) {
                compactFileWriter.write(keyValue);
            }
            // 写入changelog
            if (produceChangelog) {
                for (KeyValue kv : result.changelogs()) {
                    changelogFileWriter.write(kv);
                }
            }
        }
    } catch (Exception e) {
        collectedExceptions = e;
    } finally {
        // 4.1资源清理
        try {
            IOUtils.closeAll(iterator, compactFileWriter, changelogFileWriter);
        } catch (Exception e) {
            collectedExceptions = ExceptionUtils.firstOrSuppressed(e, collectedExceptions);
        }
    }
    // 4.2异常处理
    if (null != collectedExceptions) {
        if (compactFileWriter != null) {
            compactFileWriter.abort();
        }
        if (changelogFileWriter != null) {
            changelogFileWriter.abort();
        }
        throw collectedExceptions;
    }
    // 5.结果构建,返回(旧文件,新文件,changelog文件)的结果体
    List<DataFileMeta> before = extractFilesFromSections(sections);
    List<DataFileMeta> after =
            compactFileWriter != null
                    ? compactFileWriter.result()
                    : before.stream()
                            .map(x -> x.upgrade(outputLevel))
                            .collect(Collectors.toList());

    if (rewriteCompactFile) {
        notifyRewriteCompactBefore(before);
    }

    List<DataFileMeta> changelogFiles =
            changelogFileWriter != null
                    ? changelogFileWriter.result()
                    : Collections.emptyList();
    return new CompactResult(before, after, changelogFiles);
}

(4) 调用rewriteImpl() -- 核心2

上面的upgrade()里面提到了rewriter,他是一个接口CompactRewriter,其实现子类如下

image.png

2个接口方法

  1. rewrite(): 由MergeTreeCompactRewriter实现,这是rewrite的核心
  2. upgrade(): 由AbstractCompactRewriter实现,底层就是调的DataFileMeta的upgrade(),其实就是重新new了一个level=outputLevel的DataFileMeta对象
<1> 先看rewriteImpl()
private void rewriteImpl(List<List<SortedRun>> candidate, CompactResult toUpdate)
        throws Exception {
    // 核心:调MergeTreeCompactRewriter.rewrite()去合并重写文件
    CompactResult rewriteResult = rewriter.rewrite(outputLevel, dropDelete, candidate);
    toUpdate.merge(rewriteResult); // 将结果汇总到toUpdate中
    candidate.clear();// 清空待合并列表,防止重复合并
    // 至此,形成闭环:doCompact(筛选待合并文件)→ rewrite(路由逻辑)→ 要么 upgrade(轻量升级),要么 rewriteImpl(重度重写)→ 结果汇总到 toUpdate
}
<2> 再看最底层的MergeTreeCompactRewriter实现的rewrite() -- 核心

核心步骤如下:

  1. 创建滚动文件写入器
    • 标记当前文件的输出层级为outputLevel
    • 按照target-file-size去自动切分文件,避免生成过大|过小的文件
  2. 创建MergeTree读取器,这是关键,他会做如下的关键事情
    (1) 并行读取待合并Sorted Run文件RecordReader内部会有多个ReaderSupplier去读数据;
    (2) 按照比较器去比较和排序:key的比较器、sequnce-group的比较器、seqNumber的比较;
    (3) 归并合并底层会采用MIN_HEAP或LOSER_TREE的算法,将多个有序流合并成一个全局有序流;
    (4) 合并逻辑:由mfFactory.create()去创建对应的merge-engine,去执行合并逻辑
  3. 将合并的数据写入新文件中,由RecordReaderIterator将reader转为迭代器,逐个读取合并后的kv记录
  4. 资源清理
  5. 返回合并结果,结果包含:旧文件、新文件
@Override
public CompactResult rewrite(
        int outputLevel, boolean dropDelete, List<List<SortedRun>> sections) throws Exception {
    // 直接调rewriteCompaction
    return rewriteCompaction(outputLevel, dropDelete, sections);
}

// 核心逻辑
protected CompactResult rewriteCompaction(
        int outputLevel, boolean dropDelete, List<List<SortedRun>> sections) throws Exception {
    /* 1.创建滚动文件写入器
        - 标记当前文件的输出层级为outputLevel
        - 按照target-file-size去自动切分文件,避免生成过大|过小的文件
     */
    RollingFileWriter<KeyValue, DataFileMeta> writer =
            writerFactory.createRollingMergeTreeFileWriter(outputLevel, FileSource.COMPACT);
    RecordReader<KeyValue> reader = null;
    Exception collectedExceptions = null;
    try {
        /* 2.创建MergeTree读取器,这是关键,他会做如下的关键事情
            (1) 并行读取待合并Sorted Run文件:RecordReader内部会有多个ReaderSupplier去读数据;
            (2) 按照比较器去比较和排序:key的比较器、sequnce-group的比较器、seqNumber的比较;
            (3) 归并合并:底层会采用MIN_HEAP或LOSER_TREE的算法,将多个有序流合并成一个全局有序流;
            (4) 合并逻辑:由mfFactory.create()去创建对应的merge-engine,去执行合并逻辑
         */
        reader =
                readerForMergeTree(
                        sections, new ReducerMergeFunctionWrapper(mfFactory.create())); // 这里mfFactory.create()会创建对应的MergeFunction子类,传给ReducerMergeFunctionWrapper进行相应的合并引擎逻辑
        // 针对删除的记录,进行特殊处理
        if (dropDelete) {
            reader = new DropDeleteReader(reader);
        }
        // 3.将合并的数据写入新文件中,这里RecordReaderIterator将reader转为迭代器,逐个读取合并后的kv记录
        writer.write(new RecordReaderIterator<>(reader));
    } catch (Exception e) {
        collectedExceptions = e;
    } finally {
        try {
            IOUtils.closeAll(reader, writer);
        } catch (Exception e) {
            collectedExceptions = ExceptionUtils.firstOrSuppressed(e, collectedExceptions);
        }
    }
    // 4.资源清理
    if (null != collectedExceptions) {
        writer.abort();
        throw collectedExceptions;
    }
    // 5.返回合并结果
    List<DataFileMeta> before = extractFilesFromSections(sections); // 提取旧文件列表
    notifyRewriteCompactBefore(before); // 钩子函数,默认空实现,子类会去重写
    return new CompactResult(before, writer.result()); // 结果包含:旧文件、新文件
}

三.总结

upgrade()rewriteImpl()代码很相似,

  1. SingleFileWriter和RollingFileWriter去执行写入和滚动文件操作
  2. ReducerMergeFunctionWrapper去执行聚合逻辑
  3. readerForMergeTree()最后由SortMergeReader去执行特定的合并算法,去将文件进行排序合并重写