崩溃排查:先分清是上游的 bug 还是自己的 bug
问题从哪里来
最近在给一套流处理引擎做表达式支持,把 BETWEEN、NOT LIKE 这类 SQL 表达式落到本地 C++ 算子执行。开发 BETWEEN 时,某些用例一跑就崩。
一开始很自然怀疑自己的代码。但同一类崩溃出现在不同路径,崩溃栈也五花八门。改了几处都觉得不对 —— 因为根本不确认」这到底是不是我的 bug」。
真正要回答的问题其实很简单:这套崩溃,是上游 Flink / Calcite 逻辑的缺陷,还是我们 native 实现的问题? 两者的处理方式完全不同:上游的锅要规避,自己的锅才要修。
真正卡住的是哪一步
卡住的是 归因。同一个 BETWEEN,换了路径结果就不一样:
- 投影 + CAST 路径(
CAST(c BETWEEN 20 AND 10 AS INT))崩。 - filter 路径(
WHERE c BETWEEN 10 AND 20)也崩。 - 但崩溃栈和表现完全不同。
如果把所有崩溃都当成自己的 bug 去改,等于在不知道目标的情况下动代码。更麻烦的是,BETWEEN 的反向区间(low > high)本身是边界语义,很容易让人以为是自己没处理边界条件。
我最后怎么处理
用 vanilla 做对照组:把同一批用例同时跑在原生 Flink 和我们的 native 实现上,对比」谁崩谁不崩」。
flowchart TD
A[BETWEEN 用例崩溃] --> B{vanilla 是否也崩}
B -->|是| C[上游 Flink 1.16.3 bug]
B -->|否| D[本侧 native bug]结果立刻分成两类。
第一类:vanilla 同样崩溃 → 上游 bug。 投影 CAST 路径,例如 CAST(c BETWEEN 20 AND 10 AS INT)。vanilla 的异常栈指向 Sarg.isComplementedPoints ← ImmutableRangeSet.span:反向区间在 Calcite 的 SearchArgument 里生成了空的 range set,span 直接炸掉。这是 Flink 1.16.3 源码的缺陷,与我们无关。结论是测试主动规避这类输入,不做 golden。
第二类:vanilla 正常、native 崩溃 → 自己的 bug。 filter 路径,例如 WHERE c BETWEEN 10 AND 20。vanilla 能返回正确结果,我们的 native 在 FilterCodeGen 生成 BetweenExpr 时崩了。这属于自己的问题,修复目标立刻明确。
一个对照组,把」甩锅还是背锅」变成了可验证的结论。后续的修复和测试策略都很清楚:上游缺陷记录并规避,自己的 bug 修掉。
这件事留下的经验
崩溃排查先归因,再动手。 别急着改代码,先用最省的验证手段确认问题归属。vanilla 对照法在」上游 + 二次开发」的项目里几乎是必用工具。
同样崩溃、不同路径,要分类。 把失败用例按路径和根因分组,才可能给出统一的处理策略,而不是头痛医头。
上游缺陷要用文档和测试固化下来。 明确结论、异常栈、规避方式都沉淀进交接文档,避免下次又踩一遍,又以为是自己代码的问题。
把排查方法沉淀成 skill。 这次的方法最终提炼成了」开发完自动触发审计」的 skill,把项目特有的坑写进去,让后续开发自动避开。
参考资料
- Apache Flink 官方文档:https://flink.apache.org/
- Apache Calcite 文档(
Sarg、RangeSet所在):https://calcite.apache.org/ - 问题定位基于 Flink 1.16.3 源码行为。