Doris4.0.5在写入时通过PARTITION(*)自动检测分区的方式会报错分区创建失败

Viewed 25

可直接看图,该表执行方式是:
INSERT OVERWRITE TABLE xxx PARTITION(*) SELECT ...

之前的版本一直用的2.1.8,原来一直没有问题的SQL,升级到4.0.5后,这个语句会有问题。
我们有大量overwrite partition(*)的逻辑,有一些能成功,但有一些会失败,失败语法类似下图,我们用了auto partition,理论上自动检测分区应该会忽略已经存在的分区,而不会出现下述重新创建的情况。然后我们去掉了auto partition,在要写入分区已经存在的情况下,再次执行同样SQL,此时SQL一直卡着不动了。
这块应该是doris-4.0.5的BUG,在自动检测分区时有异常。
image.png

1 Answers

我通过AI分析源码,分析结果如下:

问题定位
问题 1:开启自动分区时报"创建分区失败"(分区已存在)
位置: FrontendServiceImpl.java:3700-3831在 createPartition 方法中,当 BE 调用 FE 创建自动分区时:
java
for (AddPartitionClause addPartitionClause : addPartitionClauseMap.values()) {
try {
Env.getCurrentEnv().addPartition(db, olapTable.getName(), addPartitionClause, false, 0, true, null);
} catch (DdlException e) {
errorStatus.setErrorMsgs(
Lists.newArrayList(String.format("create partition failed. error:%s", e.getMessage())));
result.setStatus(errorStatus);
return result;
}
}
这里调用 addPartition 时,没有设置 IF NOT EXISTS 标志。而在 InternalCatalog.java:1479-1488 中:
java
if (olapTable.checkPartitionNameExist(partitionName)) {
if (singlePartitionDesc.isSetIfNotExists()) {
return; // 安全跳过
} else {
ErrorReport.reportDdlException(ErrorCode.ERR_SAME_NAME_PARTITION, partitionName);
}
}
根因: 在 auto-detect overwrite 场景下,如果多个 BE 实例并发处理数据,或者同一个分区范围的数据被分批处理,可能会对同一个已存在的分区发起多次 createPartition RPC。第一次创建成功,第二次就会因为分区已存在而抛出 ERR_SAME_NAME_PARTITION 错误。关键线索: PartitionExprUtil.getAddPartitionClauseFromPartitionValues 在 PartitionExprUtil.java:216-225 创建 SinglePartitionDesc 时:
java
SinglePartitionDesc singleRangePartitionDesc = new SinglePartitionDesc(true, partitionName, ...);
注意第一个参数传的是 true,这个参数在 SinglePartitionDesc 构造函数中通常是 ifNotExists 标志。但问题可能在于 getAddPartitionClauseFromPartitionValues 返回的 addPartitionClauseMap 的 key 是 partitionName,如果并发请求中同一个 partition value 被处理多次,map 内部会去重,但不同 RPC 请求之间没有去重机制。
问题 2:关闭自动分区时一直卡住
位置: vrow_distribution.cpp:570-580
cpp
if (_vpartition->is_auto_detect_overwrite() && !_deal_batched) {
st = _generate_rows_distribution_for_auto_overwrite(input_block, block.get(), ...);
} else if (_vpartition->is_auto_partition() && !_deal_batched) {
st = _generate_rows_distribution_for_auto_partition(...);
} else {
st = _generate_rows_distribution_for_non_auto_partition(block.get(), ...);
}
当 is_auto_detect_overwrite() == true 但 enable_auto_create_when_overwrite == false 时,进入 _generate_rows_distribution_for_auto_overwrite 的 else 分支:
cpp
} else {
RETURN_IF_ERROR(_tablet_finder->find_tablets(_state, block, num_rows, _partitions,
_tablet_indexes, _skip));
}
RETURN_IF_ERROR(_replace_overwriting_partition());
在 _replace_overwriting_partition() 中(vrow_distribution.cpp:173-253):
cpp
for (const auto* part : _partitions) {
if (part != nullptr) {
if (_new_partition_ids.contains(part->id)) {
// skip new partition
} else {
id_deduper.insert(part->id);
}
} else if (_missing_map.empty()) {
return Status::InvalidArgument("Cannot found origin partitions in auto detect overwriting...");
}
}
如果 _partitions 中有 nullptr(表示某些行没有找到对应分区),且 _missing_map 为空(因为关闭了自动分区,没有 missing map),就会返回错误。但"卡住"更可能的原因是:位置: InsertOverwriteManager.java:294-302
java
public void recordRunningTableOrException(DatabaseIf db, TableIf table) {
if (!(table instanceof MTMV)) {
return; // <-- 对 OlapTable 直接返回,不做并发保护!
}
...
}
这个并发控制逻辑只对 MTMV 生效,对 OlapTable 直接跳过。这意味着多个 insert overwrite 可以同时在同一张表上执行。在 auto-detect 模式下,多个并发任务可能导致:
临时分区命名冲突
partitionPairs 状态混乱
replacePartition RPC 死锁或无限等待
但这可能不是直接原因。更直接的"卡住"原因可能是:在 InsertOverwriteTableCommand.java:211-218:
java
if (isAutoDetectOverwrite(getLogicalQuery())) {
taskId = insertOverwriteManager.registerTaskGroup(targetTable.getId());
insertIntoAutoDetect(ctx, executor, taskId);
insertOverwriteManager.taskGroupSuccess(taskId, (OlapTable) targetTable);
}
insertIntoAutoDetect 执行后,FE 等待 taskGroupSuccess。但如果 BE 端的 replacePartition RPC 因为某种原因(比如 id_deduper.empty() 直接返回 OK 而没有实际替换,或者返回错误)没有正确完成,可能导致 FE 和 BE 之间的状态不一致。
问题 3(最可能的根因):replacePartition 中 needReplace == false 时的逻辑缺陷
在 FrontendServiceImpl.java:4104-4154:
java
needReplace = overwriteManager.tryReplacePartitionIds(taskGroupId, reqPartitionIds, resultPartitionIds);
if (needReplace) {
// 创建临时分区并替换
...
}

// result: [1 2 5 6], make it [7 8 5 6]
int idx = 0;
if (needReplace) {
for (int i = 0; i < reqPartitionIds.size(); i++) {
if (reqPartitionIds.get(i).equals(resultPartitionIds.get(i))) {
resultPartitionIds.set(i, newPartitionIds.get(idx++));
}
}
}
当 needReplace == false 时(即所有分区都已经被替换过了),resultPartitionIds 直接就是 tryReplacePartitionIds 返回的新分区 ID 列表。这部分逻辑看起来是对的。但问题可能出在:当关闭自动分区时,BE 端 _replace_overwriting_partition() 中 id_deduper 为空,直接返回 Status::OK(),然后 BE 继续发送数据到原始分区。而 FE 端 insertIntoAutoDetect 完成后调用 taskGroupSuccess,此时 partitionPairs 是空的(没有任何替换记录),relations.entrySet() 为空,oldNames 和 newNames 都是空的,InsertOverwriteUtil.replacePartition 中 replacePartitionClause.getTempPartitionNames().isEmpty() 为 true,直接 return。这意味着:
数据写入了原始分区(没有替换)
但这是一个 overwrite 操作,应该替换分区!
这就是"关闭自动分区时卡住"或行为不正确的核心原因:auto-detect overwrite 在关闭自动分区时,没有正确执行分区替换逻辑,而是直接写入了原始分区。
总结:最可能的 Bug
Bug 1:开启自动分区时"创建分区失败"
文件: FrontendServiceImpl.java:3819-3831createPartition 中调用 addPartition 时,如果分区已存在会抛错。虽然 getCurrentPartitionFuture 有并发控制,但不同 RPC 请求之间没有共享这个控制。当多个 BE 实例或多次 RPC 对同一个分区发起创建时,第二次会失败。
Bug 2:关闭自动分区时"卡住"或行为异常
文件: vrow_distribution.cpp:461-500_generate_rows_distribution_for_auto_overwrite 在 enable_auto_create_when_overwrite == false 时:
find_tablets 后如果有行找不到分区(part == nullptr),_replace_overwriting_partition() 会返回错误
但如果所有行都找到了分区,_replace_overwriting_partition() 中 id_deduper 可能为空(如果所有分区都已经在 _new_partition_ids 中),直接返回 OK
这导致 BE 没有真正发起 replacePartition RPC,数据直接写入原始分区
FE 端 taskGroupSuccess 时 partitionPairs 为空,不做任何替换
最终效果:insert overwrite 变成了 insert into,没有覆盖效果
建议修复方向
对于 Bug 1: 在 FrontendServiceImpl.createPartition 中,当 addPartition 抛出 DdlException 且错误信息包含 "already exists" 时,应该安全跳过而不是直接返回错误。或者确保 SinglePartitionDesc 的 ifNotExists 标志被正确设置。
对于 Bug 2: 在 _generate_rows_distribution_for_auto_overwrite 中,当 enable_auto_create_when_overwrite == false 时,如果 find_tablets 后存在 nullptr partition,应该明确报错("找不到分区且不允许自动创建"),而不是让流程继续到 _replace_overwriting_partition() 中返回一个误导性的错误。同时需要确保 每次写入前都正确调用 replacePartition RPC,而不是依赖 _new_partition_ids 的缓存状态