Lazy loaded image
两次 CI 排障复盘
字数 2024阅读时长 6 分钟
2026-4-12
type
Post
status
Published
date
Apr 12, 2026
slug
CISummarize
summary
tags
技术探索
category
icon
password
这篇文章总结改Bug的经验:
报错分析,复现问题,定位、分析代码,定点修复,多层验证修复
我的两个案例:
  • dubbo-go-hessian2 PR #389:workflow、测试命令、JDK 兼容和日志噪音混在一起。
  • llm-access-gateway Stage 7:main workflow 连续失败,最后定位到是 Go 流式 fallback 测试里的并发同步出了问题。
阶段
关键问题
典型动作
报错分析
报错到底来自哪一层
拆 workflow step、拆测试职责、区分 warning 和 failure
复现问题
能不能不依赖完整 CI 重现核心问题
本地命令、Linux 容器、逐包收缩
定位代码
哪段代码真正解释了现象
workflow、runtime helper、router test
最小修复
哪个改动刚好修掉根因
改触发语义、拆命令、改同步方式
多层验证
修复是否只是偶然通过
actionlint、vet、test、race、stage7 contract、workflow green

Case 1:Dubbo Hessian2 PR #389

1. 报错信息

它们来自不同层:
  • workflow 触发语义
  • action runtime 版本
  • Go test 输出噪音
  • JDK 运行时差异
  • benchmark 和 correctness test 的职责混跑

2. 报错分析

我先把报错信息拆开来,由报错信息分析出了这三个原因导致CI报错
第一层是 workflow 触发语义。旧 action warning 是 pull_request_target ,会使用目标分支上的 workflow,所以 PR 分支里的 workflow 更新并不会直接体现在这条检查里。
第二层是日志噪音。Matcher failedgo test -v 输出过大后,GitHub Actions issue matcher 正则匹配超时。这是 CI 可观测性问题,不是 Go 语义错误。
第三层是测试职责。benchmark、coverage、race test 原来容易混在一起,导致一个报错里夹了多个含义。

3. 复现问题

  • actionlint:验证 workflow 语义,而不是靠 GitHub 页面猜
  • go test ./... -race:验证包级回归
  • go test . -race -coverprofile=coverage.txt:单独验证 coverage 产物
  • go test . -run '^$' -bench . -race:让 benchmark 独立运行,不再隐式重复整套单测
我把问题拆成四个复现,分别验证是否是这些地方出了问题

4. 定位、分析代码

我根据报错原因定位到三个位置:
位置
干什么的?
.github/workflows/github-actions.yml
解释旧 action warning 为什么能在 PR 上持续出现,说明问题来自 workflow 触发语义而不是业务代码
java_runtime_test.go
解释同一套测试为什么会在不同 JDK 下表现不同,说明问题来自 runtime 差异而不是 Hessian 语义本身
decode_benchmark_test.go
解释 benchmark 校验为什么会被 map 顺序污染,说明问题来自测试而不是解码逻辑的问题

5. 定点修复

我用多个最小修复分别闭环各自的信号来源:
  • 移除 pull_request_target 对 PR workflow 观察的干扰
  • 升级旧 action 版本
  • 去掉 go test -v 带来的 matcher timeout 噪音
  • 把 benchmark、coverage、race test 拆成不同命令职责
  • 把 JDK 差异收敛进 runtime helper
多次小修复后,CI顺利通过,之前试过一次大补丁,反而修完这个点,另一个点又出问题,一直改,返工改麻了:
notion image

6. 多层验证

最终验证链路是:
最终提交链是逐层分析细化出来的:
最终 PR 状态是 2 / 2 passed
这个结果证明 workflow、runtime、test command 和 benchmark 报错都被拆开并各自闭环了。

Case 2:LLM Gateway Stage 7

notion image

1. 报错信息

这个报错看不出问题在哪,因为 Stage 7 static contract 同时覆盖:
  • Go tests
  • Go vet
  • deployment 资产
  • Grafana dashboard JSON
  • Stage 7 required assets
所以这里最容易犯的错,就是把 exit code 1 当成一个单点问题。

2. 报错分析

第一步是把一个大 step 拆成多个可定位 step:
拆完之后,问题从“Stage 7 不过”细化成了“Stage 7 Go tests 不过”。
这一步把模糊失败第一次变成了可解释失败

3. 复现问题

这次有了经验,很快就复现到了问题所在
先把 Go tests 改成逐包串行并打印 package:
逐包收缩后的原始输出先把失败压到 package 和单个测试:
这条收缩链很关键:
我把失败从主 workflow 收缩成了最小验证操作。

4. 定位、分析代码

这个出问题的测试验证的是 streaming fallback 最敏感的一段语义:
旧测试的问题在于,它把“状态可见”误当成了“事件已发生”。
旧写法是:
atomic.Bool 只保证读写安全,不保证写它的 goroutine 已经被调度。测试读到 secondary chunk,只能说明 fallback 已经发生,不能说明 primary goroutine 已经观察到 ctx.Done()
得出是并发测试语义出了问题

5. 定点修复

修复把断言对齐真实事件,用 channel 表达同步:
  • secondary chunk 证明 fallback 已经发生
  • primaryCanceled 证明 primary attempt 的 cancel 事件已经被 goroutine 观察到
也就是说,断言从“读到一个状态”变成了“等到一个事件”。

6. 多层验证

验证链路分三层:
最后 main workflow 恢复通过,runtime-ci 的关键 job 重新变绿。
我先把失败缩成一个最小复现入口,再把它压到并发语义边界,最后用更符合事件模型的断言收口。

两个案例共同点

阶段
Dubbo Hessian2 PR #389
LLM Gateway Stage 7
报错信息
workflow warning、Go test、matcher timeout 混在一起
Stage 7 static contract 只给粗失败
报错分析
区分 workflow、日志噪音、JDK、benchmark/test 职责
拆 Stage 7 contract 为多个可定位 step
复现问题
actionlintgo testbenchmark 建立独立入口
从 workflow 缩到 package,再缩到单个测试
代码分析
workflow、JDK helper、benchmark test
provider router stream fallback test
最小修复
分层修补 workflow、runtime、test command
把状态断言改成事件同步
多层验证
actionlint、vet、test、race、benchmark、CI
package 压测、Stage 7 static、main workflow

结论

以后改Bug都要这样spec:报错分析,复现问题,定位、分析代码,定点修复,多层验证修复。

题外话

notion image
无论再小的问题,都要认真细致完成,别把问题想太简单了,狮子搏兔亦用全力!
回到首页