红灯并不都是同一种红灯
一句话总结
流水线的失败分成基础设施问题、不稳定测试、真正的缺陷三类。不分类的话,人们就会一律用“再跑一遍”来应对,而真正的缺陷就藏在里面。
为什么需要它
失败几天才出现一次,人会去读日志;一天出现十次,就不读了。重试按钮先被按下,通过之后就忘了。在养成这种习惯的团队里,真正的缺陷也会被重试两三次,碰巧通过后就原样合并了。 这就是红灯不再是信号的那一刻。
所以需要的不是更好的日志,而是分类。先确定这次失败属于哪一类,再对每一类采取不同的应对。
- 基础设施。 下载失败、名称解析失败、磁盘不足、内存超限、Runner 被回收。与代码无关。应对是有上限的重试和容量调整,并且统计件数来观察趋势。
- 不稳定(flaky)。 同一个提交,结果却不一样。这是依赖时间、随机数、运行顺序、并发、外部依赖的测试。应对是隔离和修复,用重试掩盖的话,它就会永远留着。
- 真正的缺陷。 同一个提交永远失败。应对只有修复或回退。
区分的方法出乎意料地简单:同一个提交再跑一遍,结果是否有分歧。 有分歧就是不稳定或基础设施问题,始终一样就是真正的缺陷。所以流水线必须在结果中留下“这次运行对应哪个提交”。
把它变成可复现的失败
要区分不稳定和真正的缺陷,就必须能重新构造出同样的条件。所以每次运行都要把下列内容记录在结果里。
- 随机数种子。 如果打乱测试顺序或使用随机数据,就把种子打印到日志中,并且能够重新传入这个种子来运行。不留种子的随机化,会造出无法复现的失败。
- 时间。 依赖日期的测试,只会在月末、午夜或闰年出错。如果能把测试所用的时间固定下来,就能原样重现那种条件。
- 环境。 工具版本、区域设置、时区、并行度。同一个提交结果却有分歧,通常就是其中之一不同。
仅仅留下这三样,“复现不了”这类报告就会大幅减少。
找出什么时候坏的
查找失败的原因提交时,倒着读日志的方法,耗时与提交数成正比。二分查找则是对数乘以常数。git bisect 文档写道,675 个修订大约经过 10 步、337 个大约经过 9 步就能缩小范围。也就是说,1000 个提交经过十次就能缩小范围。
要自动化,有一个条件:判定脚本必须是确定性的。 如果同一个提交不能给出同样的答案,二分查找就会把毫不相干的提交指认为罪魁。所以不能用不稳定的测试来跑二分查找。
git bisect run 依据脚本的退出码做出判定。约定是明确规定好的。
0 이 커밋은 정상(good/old)
1..127 이 커밋은 문제 있음(bad/new) 단, 125 는 제외
125 판정할 수 없음 — 이 커밋은 건너뛴다(skip)
128..255 이분 탐색 자체를 중단한다
该代码块中的韩文说明依次表示:0 表示该提交正常(good/old);1 到 127(125 除外)表示该提交有问题(bad/new);125 表示无法判定,跳过该提交(skip);128 到 255 表示中止二分查找本身。
125 单独存在的原因很重要。如果把构建根本通不过这类无法判定的提交报告为“有问题”,罪魁就会被错误地引向那一边。而 126 和 127 是 POSIX shell 用来表示“无法执行”和“找不到命令”的值,所以 125 被选为这一用途所能使用的最大值。编写判定脚本时,要当心不要使用 exit -1 之类的写法。那个值会变成 255,使整个查找被中止。
反馈时间预算
如果不给反馈时间设一个数字上限,流水线就会悄悄变慢。一点一点增加的时间没有人会察觉,终有一天就变成了“它本来就要跑很久”。
超过上限之后该做什么,最好事先定好。比如把阻止合并的步骤和不阻止合并的步骤分开,把慢的挪到后面;或者拆分测试;或者根据变更范围,跳过那些不常出问题的领域的测试。无论哪种做法,都必须记录放弃了什么。如果不记下放弃的东西,几个月后那个领域出了事故,没有人能解释当初为什么没有抓到。
在现场相遇的样子
- 开始统计失败类别的第一周,通常会得出“基础设施占一半”的结果。这时就很清楚了:要修的对象不是测试,而是容量和重试策略。
- 建好不稳定测试的清单之后,遇到新的失败就会先看它是否在清单里,调查时间会大幅缩短。清单里原本没有的测试表现出不稳定,这本身就是一个事件。
- “昨天还是好的啊”,通常并不是昨天。跑一遍二分查找,得到 2 周前的提交是常有的事。只是这期间没有人动过那条路径而已。
- 如果把产物出自哪个提交,用
git describe之类的值写进产物里,出事故时回溯“现在运行的是哪个提交”,几分钟就能完成。
参考
- git bisect:https://git-scm.com/docs/git-bisect
- git describe:https://git-scm.com/docs/git-describe
- 持续集成:https://martinfowler.com/articles/continuousIntegration.html
下一项实验要做什么
创建一个 git 仓库,堆叠提交,让它在中间某处被弄坏,然后用二分查找找出罪魁。先手动运行 git bisect start / good / bad,数一数经过几步就能缩小范围,再用判定脚本把同样的事情自动化。确认脚本遵守退出码约定,故意插入一个无法构建的提交,亲眼看看如果不用 125 跳过,罪魁会被错误指认成什么样子。然后放入结果有分歧的判定脚本,确认二分查找会崩塌;最后编写一个按类别统计失败日志并报告的分类脚本。