一.抽象父类CompactTask
其实现子类如下图
- MergeTreeCompactTask是主键表的合并流程;
- 其他两个,都是BucketedAppendCompactanager里面的内部类,也就是仅追加表的合并流程;
二.MergeTreeCompactTask
1.属性和构造函数
2.doCompact() -- 核心流程
(1) doCompact()
流程如下
- 遍历每个
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
- SUB-CASE-1:当前file的文件大小 <
- CASE-1:当前
- 遍历完后,再调
rewrite()处理剩余待合并文件,避免遗漏 - 返回最终合并结果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()
逻辑如下:
- CASE-1:当前没有待合并的section文件,则return
- CASE-2:当前待合并的section文件只有一个,进一步判断
- 当前section文件没有Sorted Run文件,说明是空文件,return即可
- 当前section文件只有一个Sorted Run文件,遍历该Sorted Run下面的Data File物理文件,并调
upgrade()去升级合并(可能重写文件,也可能不重写)
- 其他情况,说明有多个文件(要么多个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()流程
升级+合并逻辑如下:
- 当前Data File的层级 == outputLevel(要合并到的目标层级),是该文件已经处理过了,直接return,防止重复处理
- 该文件的outputLevel不是最大Level 或者 当前Data File文件没有被删除过,那么调
ChangelogMergeTreeRewriter.upgrade()进一步处理(可能重写,可能不重写),然后将结果合调toUpdate.merge()合并到最终结果中 - 其他情况,需要走
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是个接口,其实现子类如下图
以ChangelogMergeTreeRewriter为例,流程如下
- 如果需要产生changelog,那么则调
rewriteOrProduceChangelog()去重写文件或产生changelog- 只有配置了'changelog-producer' = 'lookup'或'full-compaction',才会走生产changelog
- 且只有lookup模式的非deduplicate模型,才会既生成changelog,也rewrite
- 如果不需要,则直接
调父类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(),流程如下
- 变量初始化
- 数据读取、写入器创建
CASE-1: 需要重写压缩File,调KeyValueFileWriterFactory.createRollingMergeTreeFileWriter()创建滚动合并文件写入器,后续进行重写合并操作
CASE-2: 需要产生changelog,调KeyValueFileWriterFactory.createRollingChangelogFileWriter()创建滚动变更日志文件写入器,后续进行changelog产生写入 - 遍历文件,然后该重写重写,该产生changelog产生changelog
- 资源和异常处理
- 返回结果(旧文件,新文件,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,其实现子类如下
2个接口方法
rewrite(): 由MergeTreeCompactRewriter实现,这是rewrite的核心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() -- 核心
核心步骤如下:
- 创建滚动文件写入器
- 标记当前文件的输出层级为outputLevel
- 按照target-file-size去自动切分文件,避免生成过大|过小的文件
- 创建MergeTree读取器,这是关键,他会做如下的关键事情
(1) 并行读取待合并Sorted Run文件:RecordReader内部会有多个ReaderSupplier去读数据;
(2) 按照比较器去比较和排序:key的比较器、sequnce-group的比较器、seqNumber的比较;
(3) 归并合并:底层会采用MIN_HEAP或LOSER_TREE的算法,将多个有序流合并成一个全局有序流;
(4) 合并逻辑:由mfFactory.create()去创建对应的merge-engine,去执行合并逻辑 - 将合并的数据写入新文件中,由
RecordReaderIterator将reader转为迭代器,逐个读取合并后的kv记录 - 资源清理
- 返回合并结果,结果包含:旧文件、新文件
@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()代码很相似,
- 用
SingleFileWriter和RollingFileWriter去执行写入和滚动文件操作 - 用
ReducerMergeFunctionWrapper去执行聚合逻辑 - 用
readerForMergeTree()最后由SortMergeReader去执行特定的合并算法,去将文件进行排序合并重写