eldenmoon commented on PR #69995: URL: https://github.com/apache/doris/pull/69995#issuecomment-6595258322
## Compaction 数据安全补充:F-04 / Blocker(既有缺陷,Regression: no) 对同一 head `ee6ce712a0381422bc44e4d8d27490cf66f32464` 重新做了两轮针对 compaction 的静态交叉核验后,**需要修正上一条评审“未确认数据丢失”的结论:存在可使已存值消失的受支持路径。** 本次没有编译或运行测试;以下是源码确认的机制与建议回归场景。 ### 触发边界 普通 **non-DOC Variant**、`variant_max_subcolumns_count=1`、表属性 **`storage_format="V3"`**,某个用户 JSON key 包含 `__DORIS_VARIANT_SPARSE__`,并在 full compaction 中从 sparse 提升为 materialized。 这里的 V3 是**表的外置列元数据格式**,[FE 接受显式 V3,省略/default 则返回 V2](https://github.com/apache/doris/blob/ee6ce712a0381422bc44e4d8d27490cf66f32464/fe/fe-core/src/main/java/org/apache/doris/common/util/PropertyAnalyzer.java#L1253-L1271);它与 ColumnVariantV2、倒排索引 V3 不是同一个配置。本例不需要索引、ALTER 或混合版本。 ### 如何从“compaction 前可读”变成“之后丢值” 令 `p="x__DORIS_VARIANT_SPARSE__y"`,使用三个独立输入 segment: | 输入 | 两行内容 | 本地 materialized | |---|---|---| | A | `{p:701,hot_a:11}`;`{hot_a:12}` | hot_a(2 > p 的 1) | | B | `{p:702,hot_b:21}`;`{hot_b:22}` | hot_b | | C | `{p:703,hot_c:31}`;`{hot_c:32}` | hot_c | 所以 p 在**所有输入中都是可读的 sparse 值**。合并统计后 p=3,大于每个 hot 字段的2,[top-N 选择](https://github.com/apache/doris/blob/ee6ce712a0381422bc44e4d8d27490cf66f32464/be/src/exec/common/variant_util.cpp#L897-L925)会将 p 提升为 materialized。值先被正确读出;[sparse merge](https://github.com/apache/doris/blob/ee6ce712a0381422bc44e4d8d27490cf66f32464/be/src/storage/segment/variant/sparse_column_merge_iterator.cpp#L124-L151)随后按目标 selected set 排除 p,唯一输出副本交给新子列。 错误发生在 [V3 元数据分类](https://github.com/apache/doris/blob/ee6ce712a0381422bc44e4d8d27490cf66f32464/be/src/storage/segment/variant/variant_ext_meta_writer.cpp#L98-L115): ```cpp if (rel_path.find("__DORIS_VARIANT_SPARSE__") != std::string::npos) { classification.pending_sparse[root_uid].emplace_back(col); } ``` 该 substring 检查把普通用户子列 p 当成内部 sparse 列,嵌入 root.children_columns,而没有写普通子列的外部 path-index entry。读端只识别完整 sparse 标记或其 bucket 前缀;[遍历 embedded children 时](https://github.com/apache/doris/blob/ee6ce712a0381422bc44e4d8d27490cf66f32464/be/src/storage/segment/variant/variant_column_reader.cpp#L1246-L1311),p 的 `handled=false` 被直接忽略,没有普通子列回退。 **第一次 full compaction 后**:对应行的 `v[p]` 返回 NULL,整列重建遗漏 p;六行及 hot 字段都还在。p 的页面和错放元数据此时可能仍物理存在,不能说第一次就抹除了所有字节。**再加入一个 rowset 并执行第二次 full rewrite 后**,统计、子列树和 sparse map 都找不到 p,没有任何读取流复制这些值,新输出 segment 不再包含它们。 现有校验不会阻止此例:行数不变;[`check_path_stats`](https://github.com/apache/doris/blob/ee6ce712a0381422bc44e4d8d27490cf66f32464/be/src/exec/common/variant_util.cpp#L984-L1044)只遍历 **output paths**,消失的 input-only p 根本不参加比较,其余 hot 计数仍相等。须使用 FULL compaction 验证,因为[该类型会禁用 ordered 文件链接](https://github.com/apache/doris/blob/ee6ce712a0381422bc44e4d8d27490cf66f32464/be/src/storage/compaction/compaction.cpp#L755-L770)。列组宽度1/2、三个 sparse shard 均不消除此链路。 ### 归因与修复 这是 **Blocker / data-error**:静默错误结果,后续重写还会丢掉剩余的有效值副本。**但不是本 PR 新引入的 codec 回归**:相关 classifier 在 merge base `49bfbdecffb5b37aeadb0982700533e8daf7768d` 与 head 完全相同,读端遗漏也已存在;escaping 只处理点号/百分号,不能隔离这个内部名称。它仍影响本 PR 支持的 escaped Variant 表。 建议统一 writer/reader 的内部列身份判断,避免 substring 判定。完整路径语法和物理类型校验可修复本例,但 canonical-looking 用户 key 仍需明确的 synthetic-role 元数据或不相交的命名约定。只把 `find` 改成 `starts_with` 不是完整修复。对已经写出的异常 V3 segment,应能恢复可明确识别的普通 child,或在再次 compaction 前报错,不能继续静默忽略。 补上以上不同本地热度、两次真实 rewrite、逐行701/702/703和整列内容的回归,不能只检查 count(*)。统计完整且没有行过滤的 DUP 场景还应校验 input-only 路径遗漏;MOW/delete 场景须尊重 survivor 语义。 此前 F-01/F-02/F-03 保持;累计已确认 **1 Blocker + 1 Major + 2 Minor,REQUEST_CHANGES**。本轮其余普通路径迁移、编码/分片、root 属性传播和 doc 重建没有确认另一条独立丢值路径,但这不构成无条件安全保证。 -- This is an automated message from the Apache Git Service. To respond to the message, please log on to GitHub and use the URL above to go to the specific comment. To unsubscribe, e-mail: [email protected] For queries about this service, please contact Infrastructure at: [email protected] --------------------------------------------------------------------- To unsubscribe, e-mail: [email protected] For additional commands, e-mail: [email protected]
