2996 字
约 9 分钟
13
S16-A16 · 缺陷逃逸分析:跑到线上才发现的那批
2024-12-31
2026-11-09

线上出了个问题,回溯发现测试里有一条被 @Ignore 跳过了。这条事实很容易被处理成"某人的疏忽",但它真正说明的是:检测链条上有一个环节没有闭合,而没有人知道它没闭合。跳过那条用例的人可能留了备注,可能等了三天没人修 fixture,也可能根本没人注意到。

逃逸分析的对象不是缺陷本身,而是这条链条上的缺口。缺陷已经修完了,缺口还在;下一批缺陷会从同一个缺口继续漏出去。这篇按"缺口在哪 → 为什么通过 → 输出什么 → 什么时候不做"四层来拆。


1. 逃逸分析的对象:检测链条,不是缺陷

同一个未校验的边界值,在五个关口都会漏,但漏出的形态完全不同:

关口 漏出形态 缺口性质
需求 验收标准写成"合理返回",没有可判定的阈值 判据不可检验
设计 超时、重试、并发语义没写成约束项 约束只存在于口头
编码 异常分支直接 return null,路径不可达 分支不可观察
评审 只读 diff,不读调用方与被调方 上下文缺失
测试 用例覆盖了主路径,边界值靠"经验判断" 断言缺失或环境失真

注意这张表里没有"人"这一列。归因到人,缺口就无法修复——把一条用例被跳过归因为"态度问题",处理方式是提醒,下个季度还会被跳过;归因为"fixture 依赖一个每月挂三次的外部服务",处理方式是把那条用例的依赖换成容器内桩,问题才真正消失。

─── 逃逸单的最小字段:让缺口可检索
defect_id: D-2841
escaped_to: prod
detected_by: [customer_report]       // 线上是被谁发现的
chain_holes:
  - stage: test
    hole: case_skipped                // 没测 / 没覆盖 / 没断言 / 环境不同
    evidence: "@Ignore 存在 47 天"
  - stage: review
    hole: no_context_read             // diff-only
related_change: PR-1182

chain_holes 是这张单的核心。没有它,逃逸分析会退化成"再补几条用例",而补错层的用例等于给已经闭合的环节加锁。

缺口只有四类,判定证据各不相同:

逃逸原因 判定证据 对应改进
没测 用例列表里没有该输入形态 补用例,先复现
测了没覆盖 用例存在但分支未执行(覆盖率报告指出空洞) 补分支或用例参数化
覆盖了没断言 分支执行了,用例只断言"不抛异常" 补业务不变量断言
断言了但环境不同 断言在预发成立、生产不成立 环境同构校验,而非加断言

四类的改进方向完全不同:前两类是"量"的问题,第三类是"判据"的问题,第四类是"环境"的问题。用同一句"加强测试"覆盖四种原因,等于什么都没做。

─── 静默跳过是缺口的放大器
@pytest.mark.skip(reason="flaky, 等 fixture 修好")   // 没有到期时间
// CI 配置里 skip 必须二选一:
//   strict: true      → 出现 skip 直接失败
//   skip_budget: 0    → 超过预算即失败,按季度清零
// 只允许带 issue 编号与到期日的 skip

上面这段配置的意义在于:让"没测"这件事在流水线上可见。只要跳过是静默的,缺口就永远不会进入任何报表。


2. "测试通过、线上失败"的三种机理

失败几乎都落在三类差异上,判断顺序建议从环境到数据再到时序——成本依次上升。

机理 典型表现 定位动作 能否被灰度拦住
环境差异 本地/预发通过,线上 500;版本、JVM 参数、中间件配置不同 拉两侧配置与依赖版本做 diff 能,灰度环境若与生产同构
数据差异 特定字符集、NULL、超大值、并发写触发 从线上取真实样本回放 部分,取决于灰度流量是否含该数据形态
时序差异 重试叠加、消息乱序、缓存过期竞态、时钟漂移 构造可控时序,固定时钟与队列 很难,低频竞态要足够长的观察窗

三种机理的排查成本差一个量级:环境差异常常是配置 diff 十分钟出结论;时序差异可能要写一个可重复的并发脚本,跑几百轮才复现一次。

数据差异的取证有个硬要求:样本必须来自线上,不能手工构造。构造出来的脏数据总是恰好触发你想到的那个分支,而线上触发的是你没想到的那个组合。取真实样本时先脱敏,再固定成回放夹具——夹具里保存的是"能触发缺陷的输入形态",不是完整业务数据。

─── 补测试的顺序不能反
1 复现:在旧代码上让用例失败(红)
2 收窄:固定数据、固定时钟、固定并发度,失败必须可重复
3 补断言:断言落在"业务不变量"上,不是落在实现输出上
4 验证:改代码后转绿,再回滚代码确认重新变红
// 先补测试再验证 = 写了一条永远不会失败的用例

第四条是区分"补测试"与"补覆盖"的唯一手段:回滚代码必须重新变红。做不到这一点的用例,只增加了行覆盖率,没有增加检测能力。


3. 输出:把逃逸换成检测手段,而不是换成追责

逃逸分析的产物应该是检测手段的改动,可以落到三处:新增断言、新增环境同构校验、新增发布前检查项。写不出这三样,分析就只是在复述事故经过。

输出类型 合格形态 不合格形态
用例 能复现、能回滚变红 "已补充相关测试"
环境 配置项进同构校验清单 "注意预发与生产一致性"
流程 跳过用例必须在 CI 里显式失败 "加强测试纪律"
监控 新增可观测信号与阈值 "加强线上巡检"

定级必须与普通缺陷分开。逃逸缺陷的价值不在严重度,而在它证明某道关口失效;混进普通缺陷池按严重度排序,它会因为"影响面小"被排到最后,而它携带的缺口信息就此丢失。单独一个标签、单独一个池,按缺口类型聚合。

逃逸率作为度量,只看趋势与聚集,不看绝对值:生产发现缺陷 / 全部缺陷 的分母随登记习惯变化,跨团队比较没有意义;但同一团队连续四个周期的走向,以及"哪个关口漏得最多"的分布,是可靠信号。

─── 检测关口分布:找出漏得最多的那道关口
SELECT hole_stage, hole_type, COUNT(*) AS n,
       ROUND(100.0 * COUNT(*) / SUM(COUNT(*)) OVER (), 1) AS pct
FROM escape_defect
WHERE escaped_at >= DATE_SUB(NOW(), INTERVAL 180 DAY)
GROUP BY hole_stage, hole_type
ORDER BY n DESC;
// 同一 (stage, hole) 组合连续两个周期都在前三 = 改进没落地

同一类逃逸重复出现,说明改进动作没有落到执行层。这时不该再写一条新规则,而该检查上一次的规则为什么没生效——是被绕过、被豁免、还是根本没人执行。

─── 四个度量,各自回答一个问题
escape_rate: prod_found / all_found        // 门禁有效吗(只看趋势)
gate_distribution: group_by(stage, hole)   // 漏在哪道关口
recurrence_rate: same_hole / resolved      // 改进落地了吗
backfill_validity: red_on_revert / backfilled  // 补的用例有检测力吗
window: 180d
exclude: [env_class, known_limitation]
// backfill_validity 最容易被跳过,也最能拆穿"已补测试"

backfill_validity 是这样算的:补进去的用例,把修复代码回滚后仍然变绿的比例。这个比例高,说明补的是覆盖而不是检测。它不需要全量统计,每月抽十条就足够看出问题。


4. 排错:分析做不下去的五种情况

现象 原因 处理
每张逃逸单都归到"人为疏忽" 缺口字段没填,只能写人 强制 chain_holes 至少一条,且必须是环节名
补了测试,同类问题还漏 补在已闭合的关口 按缺口定关口,不按缺陷定关口
逃逸率忽高忽低 分母随登记习惯波动 改用绝对单数 + 关口分布
复盘变成追责会 结论要求"谁负责" 只允许结论指向可改动的环节
灰度上线后仍逃逸 灰度流量不含该数据形态 检查灰度流量构成,而非扩大灰度比例

第三条值得展开:灰度与金丝雀能拦住的是行为差异——新逻辑对同一输入给出不同输出。它拦不住数据形态差异(灰度流量里没有那种脏数据)和低频时序(观察窗不够长)。所以"上灰度了"不能作为关闭逃逸分析的理由。


5. 边界:什么时候完整逃逸分析是过度投入

场景 该做的动作 不该做的
变更量极小(一行文案) 记一条缺口备注,五分钟 开一小时的复盘会
系统已冻结、即将下线 记录不修,转成迁移清单 为旧系统补测试
缺陷影响可忽略(显示错位) 单条登记 全链条回溯
同一缺口本月第三次出现 停掉单次分析,改流程 继续逐单复盘
逃逸原因明确且已修复 验证改进落地 再写一份分析文档

频率上,全量逃逸分析按事件触发即可:事故级必须做,普通线上缺陷抽样做(比如每月抽三张),同类重复时升级为流程动作。每张逃逸单都做深度分析,成本会迅速超过它挽回的损失——一次完整回溯通常要半天到一天人力,而多数缺陷的缺口是同一个。

判断过度的界线很朴素:分析的边际产出是"新的缺口类型"还是"第 N 张同类型单"。前者值得做,后者只值得记一笔。所以抽样不是偷懒,而是把预算留给还没被识别过的缺口形态。


6. 自测

  1. 逃逸分析的对象为什么不是缺陷本身?两者在产出上差在哪?
  2. "补测试"和"补覆盖"用什么动作区分?
  3. 三种失败机理里,哪一种最难被灰度拦住?为什么?
  4. 逃逸缺陷为什么要与普通缺陷分开定级?
  5. 同一类逃逸连续两个周期出现,下一步该做什么?
  6. 哪些场景下只登记、不做完整回溯?
S16-A16 · 缺陷逃逸分析:跑到线上才发现的那批
https://www.clxhxhhr.top/posts/6309/
作者
clxstart
发布于
2024-12-31
许可协议
CC BY-NC-SA 4.0
评论
0 条
还没有评论,先写一条吧。