CI 不稳定测试如何治理:自动化构建健康管理系统实践

在某海外大型科技公司,工程团队每天会运行超过 35,000 次构建,以及数百万次自动化测试。如此庞大的持续集成(CI)和自动化测试规模下,偶发失败和“不稳定测试”几乎不可避免。与此同时,一些新的代码提交也确实会导致构建失败,从而阻碍开发人员发布新版本。

在这样的规模下,如何治理不稳定测试、维护构建健康、减少 CI/CD 流水线中的人工干预,就变得至关重要。例如,临时禁用不稳定测试、回滚导致构建失败的提交、通知测试负责人等,都不应该长期依赖人工处理。

为此,团队构建了一套自动化构建健康管理系统,用于管理构建健康状况,并自动帮助构建保持稳定。

CI 不稳定测试如何治理:自动化构建健康管理系统实践

过去如何维护构建健康?

为了确保基本正确性,所有代码都会经过一系列提交前测试。

而像 UI 测试这类更全面的端到端测试,由于稳定性不足、运行速度较慢且成本较高,并不适合在代码合并到主分支之前运行。因此,这些测试只会在代码合并后运行,团队称之为“提交后测试”。

团队要求提交前测试和提交后测试都通过,才能发布新版本。

为了让构建保持“绿灯”状态,团队最初建立了一个轮值机制:由值班人员回滚那些导致提交后测试失败的提交,并临时禁用或“隔离”不稳定测试。

随着时间推移,这个轮值机制的运维负担变得越来越重。团队不得不把责任分散到多个小组,让他们共同承担维护构建健康状况的压力。

此外,由于人工隔离不稳定测试的效率很低,提交前测试也变得缓慢而令人沮丧。于是,团队开始思考一个更可持续的解决方案。

自动化构建健康管理系统登场

团队最终构建了一项新服务,用于管理构建健康状况,并尽可能减少人工干预。

这套系统主要通过两种方式降低保持构建绿灯所需的人力成本:

  • 识别会导致测试确定性失败的提交,并通知作者回滚该提交;
  • 识别不稳定或不可靠的测试,并自动将其隔离。

不稳定测试治理的难点在哪里?

判断一次测试失败到底是确定性故障,还是偶发性失败,并不容易。

归根结底,测试代码也是任意的用户代码,因此测试可能以各种方式失败。团队观察到的三类主要非确定性测试失败,分别是:非封闭测试、不稳定测试和基础设施不稳定。

非封闭测试

封闭测试只使用已声明的依赖项,并且不依赖构建和测试环境之外的任何资源,因此其结果是可复现的。

有些测试依赖于难以伪造的外部资源,例如时间。团队经常发现,当新的一天到来,或者到达月末时,某些测试就会开始失败。

例如,用于测试某个折扣是否有效的代码,会在折扣过期后开始失败。团队将这类问题称为环境故障。

为缓解这个问题,团队会跟踪最新的“稳定提交”。所谓稳定提交,是指所有测试都通过的提交。每当一个新提交通过了所有测试,团队就将其标记为稳定提交。

如果某个测试在稳定提交上运行失败,那么它很可能存在非封闭行为,或者遇到了环境故障。

不稳定测试

不稳定测试是指在输入不变的情况下,行为仍然不确定的测试。

造成这种不确定性的潜在原因包括:依赖随机数、线程调度、超时设置不当,以及并发问题。

对于不稳定测试,团队无法百分之百确定某个提交是否真的破坏了测试。

为缓解这个问题,团队会最多重复运行 10 次测试。如果多次运行结果不一致,就可以确认该测试存在不稳定现象。经过一些实验后,团队最终将重试次数确定为 10 次。

基础设施不稳定

多年来,团队投入了大量精力,确保测试拥有可靠的资源保障,从而保证测试结果一致。

测试运行在容器中,并通过资源配额获得稳定的 CPU 和内存配置,同时还会进行 CPU 与 NUMA 绑定。

然而,在拥有上千个节点的集群中,性能波动和个别主机性能不足仍然在所难免。

最常见的基础设施不稳定情况,是测试在性能较差的主机上超时。对此,团队会在另一台主机上重试测试,以降低误判概率。

这套系统如何自动管理构建健康?

为了应用上述缓解措施,团队创建了这套自动化构建健康管理系统。它会监控测试结果,重新运行失败测试,以判断它们是不稳定还是确实被破坏,并采取隔离等措施。

系统会监控所有新代码提交的测试结果。如果某个提交后测试在几个小时内多次失败,系统会将其标记为“噪声测试”。

这是一种临时状态,因为系统此时还无法确定该测试失败到底是由不稳定、基础设施问题,还是代码故障导致的。

CI 不稳定测试如何治理:自动化构建健康管理系统实践

在提交前测试中,系统会暂时忽略那些产生大量噪声的测试。但这些测试仍会在提交后继续运行,并影响整体构建状态。

这样做可以减少新代码提交的失败率,避免开发人员浪费时间调查并非由自己造成的问题,同时也能确保团队不会发布存在缺陷的版本。

CI 不稳定测试如何治理:自动化构建健康管理系统实践

如何检测不稳定测试?

为了判断测试是否不稳定,系统会使用此前测试通过的提交版本重新运行该测试。

如果测试在多个主机上反复出现“有时通过、有时失败”的情况,系统就会将其隔离。

为了应对环境故障,系统还会使用最新的稳定提交版本重新运行测试。

如何检测真正的构建故障?

故障检测更加复杂。

对于每个出现异常的测试,系统会运行二分查找,找出测试结果从通过变为失败的潜在转换点。随后,系统会在该转换点重新运行测试 10 次,以确认测试确实被破坏,而不仅仅是不稳定。

下面的 Python 伪代码解释了故障检测算法。def find_offending_commit(test, commits, latest_stable_commit): # commits where the test might have transitioned from pass to fail potential_transition_points = [] for commit in reversed(commits): if is_test_failing(test, commit) or is_test_unknown(test, commit): potential_transition_points.append(commit) else: # if we see a pass, we’re guaranteed not to have broken the commit before # this point break if not potential_transition_points: return NoIssues() # the test isn’t broken if len(potential_transition_points) == 1: # potential culprit commit = potential_transition_points[0] # deflake the test: run it 10 times to confirm it’s broken is_failure = trigger_test(test, commit, deflake_num_runs=10) if not is_failure: return NoIssues() # just a flake # rerun on the stable commit to confirm it’s not environmental issues is_failure = trigger_test(test, latest_stable_commit) if not is_failure: return Breakage(test=test, commit=commit) # bisect logic midpoint = (len(potential_transition_points) / 2) – 1 middle = potential_transition_points[midpoint] is_failure = is_test_failing(test, middle, run_if_unknown=True) # run bisect with a limited set if is_failure: new_candidates = potential_transition_points[:midpoint+1] else: new_candidates = potential_transition_points[midpoint+1:] return find_offending_commit(test, new_candidates, latest_stable_commit)

自动化测试隔离的效果如何?

这套系统已经在生产环境中运行了几个月。测试隔离数量增加了一倍,而且团队不再需要手动隔离测试。

运维成本

现在,系统采取的隔离措施比以往更加严格。这或许会让人感到担忧。

但实际上,这样做是为了确保提交前测试样本不受噪声干扰,并为所有测试结果设定统一的质量标准。

系统不会自动回滚提交,但它会向提交作者和负责测试套件的团队发送通知。

虽然自动化回滚并不难,但将构建版本恢复到稳定版本并不是当前最紧急的问题。因为在提交前阶段,系统已经会忽略有问题的测试结果。而且,自动回滚可能会干扰关键问题修复。因此,团队暂时没有启用这一步。

CI 容量如何优化?

过去,团队会在大多数提交中自动运行最耗资源的测试,例如 UI 测试,以便人工更容易识别问题。

有了这套系统后,团队可以尝试对这些测试进行“限速”,将测试套件的运行频率限制为最多每 10 分钟一次。

但这也意味着,团队失去了通过可视化扫描快速查找导致测试失败提交的能力。此外,系统起初没有公开任何运行进度信息,导致团队对系统缺乏信任。

大家会问:“它真的还在运行吗?”

为了解决这个问题,团队添加了一个简单的用户界面,用于可视化运行进度。这帮助开发人员理解系统当前正在做什么,也让系统更接近自助服务,从而减少了支持工作量。

限速机制确保 CI 中的容量使用缓慢增长,并降低了高峰时段主机需求的峰值波动。总体而言,团队将测试集群规模减少了约 8%。

接下来如何继续优化 CI/CD?

团队还没有将这套系统应用于桌面端测试,因此下一步计划就是推进这项工作。这将涉及理解新的故障模式,例如不同操作系统及其变体带来的问题。

对于故障检测,虽然目前使用的是二分查找,但一些集群仍有足够的剩余容量。因此,团队可以尝试使用 “n-sect” 方法,并行运行所有可能转换点上的测试,以便更快发现故障。

最后,团队还希望测试一种策略:如果构建失败持续数小时,是否可以自动回滚。理论上,这不应该干扰关键修复。

结论:用自动化治理不稳定测试,提升开发者效率

持续集成(CI)让组织能够快速、安全地发布产品,但它也会带来一些成本:构建管理工作量、提交前阶段不稳定失败带来的挫败感,以及高昂的机器成本。

像这样的自动化构建健康管理系统,可以显著降低这些成本。对于研发团队来说,治理不稳定测试不能只停留在 CI 工具层面,还需要和需求、缺陷、测试、发布、知识沉淀等研发流程连接起来。PingCode 这类智能化研发管理工具,可以帮助团队把目标、需求评审、开发测试、发布上线和 Wiki 经验沉淀串联起来,让测试治理、构建健康和研发效能改进更容易被追踪、复盘和持续优化。

团队学到的经验包括:

第一,保持通知的高精准度。向开发者发送大量不准确或不可执行的通知很容易,但开发者也会很快习惯性忽略它们。高精准度通知有助于维持用户对系统的信任,并促使他们采取必要的人工行动。

第二,自动化有助于减少无谓争论。就像代码格式化工具可以帮助避免关于代码风格的争论一样,自动化测试隔离也有助于在测试套件中保持高标准,而不必反复争论哪些测试需要隔离以及为什么。

第三,为长时间运行的操作展示进度。任何长时间运行的异步工作流,都需要某种方式表明它没有卡住。这有助于减少用户困惑,也能维持用户对系统正常运行的信任。

致谢

本文中的许多想法都源自某海外科技公司在一次自动化测试大会上的分享。相关演讲启发团队构建了自动隔离和忽略噪声测试的系统,也让团队认识到自动化在减少无谓争论方面的强大作用。

此外,许多自动化测试和 CI 领域的研究与实践经验,也帮助团队设计了故障检测系统,以及实现这一目标所需的 CI 基础能力。

还要感谢参与原型设计和关键功能建设的工程师们。他们构建了系统的第一个原型,并加入了许多重要功能,例如在提交前阶段忽略噪声测试、自动隔离,以及更简化的用户体验。

如果你也对大规模开发者基础设施建设感兴趣,可以继续关注相关实践。

文章包含AI辅助创作:CI 不稳定测试如何治理:自动化构建健康管理系统实践,发布者:cai,转载请注明出处:https://worktile.com/kb/p/3979975

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
cai的头像cai

发表回复

登录后才能评论
注册PingCode 在线客服
站长微信
站长微信
电话联系

400-800-1024

工作日9:30-21:00在线

分享本页
返回顶部