我们如何把部署的 502 错误降到零
每次部署 Mythex 的后端时,都会有少量请求以 502 Bad Gateway 失败。数量不多,但部署期间出现 502,意味着有人的点击没有任何反应,或者他正在盯着的构建停顿了一下。我们把它追溯到两个独立的问题:一个与部署相互打架的自愈看门狗,以及服务器停止监听与负载均衡器停止向其发送流量之间的时间差。在修复之后连续三次生产部署中,计数从 10 降到了 6,再降到 0。
架构
我们的 API 和编排器以容器的形式运行在一小组服务器上,这些服务器位于 AWS Application Load Balancer(ALB)之后。部署会一次更新一台服务器:排空旧容器,启动新容器,然后进入下一台。每台服务器上都有一个看门狗,它会检查容器是否正常运行,并重启任何未运行的容器——它的存在意义是让凌晨 3 点崩溃的进程能够自行恢复,而无需任何人被叫醒。
这两者单独来看都很合理。合在一起,却出了问题。
问题一:看门狗与部署相互竞争
看门狗每分钟运行一次,并且在一次健康检查失败时就进行「修复」。而部署会有意地排空服务,最长可达 150 秒——在此期间,健康检查「应该」失败。因此,看门狗在每一次部署期间都会触发。
大多数时候,它输掉这场竞争,无伤大雅。但有一次,它赢了:它在部署中途重写了 compose 文件(描述应当运行哪些容器的文件),结果某台服务器上完全没有了网关容器——而部署任务却报告成功。
修复方案:
- 统一加锁。 部署现在会获取与服务器引导脚本相同的文件锁,因此看门狗与部署永远不会同时运行。
- 四次失败才行动,而不是一次。 看门狗需要连续四次健康检查失败才会采取行动。一个正在排空的服务不再被误认为已经死亡。
- 单一定义。 compose 文件现在只有一个来源,发布到服务器上,而不是可能相互偏离的多个副本。
- 验证,而不是信任。 部署任务不再因为某条命令返回了结果就算通过。它会先检查每台服务器上的 API、编排器、网关和发布工作进程,然后才能成功。
问题二:服务器比流量先离开
第二个问题更加隐蔽。部署排空一台服务器时,服务器会开始用 503 响应其健康端点,让负载均衡器将其移出轮转。我们的服务器这样做了 5 秒,然后关闭。
但 ALB 只会不时地检查健康状况,并且需要连续多次失败才会停止向某个目标路由流量——从头到尾大约需要 60 秒。因此在大约 55 秒的时间里,负载均衡器仍会把真实请求发送到一台已经停止监听的服务器。这些就是我们的 502。
修复方法是让两条时间线保持一致:
- 目标组现在每 5 秒检查一次,连续 2 次失败后将目标标记为不健康——大约 10 秒即可发现。
- 排空过程在开始报告健康检查失败后,仍会为真实流量提供服务 15 秒,因此两者始终有重叠。
- 这些设置写在负责配置基础设施的脚本中,而不是某次被人点击过一次的控制台里。
让各项数字相互印证
这些超时设置彼此依赖。排空时间必须长于负载均衡器发现问题所需的时间;排空加上关闭,必须在容器被强制杀死之前完成。六个月后,有人改动其中一个数字,就很容易无声无息地重新引入这个 bug。
因此,我们把这些数字之间的关系写成了测试:排空时间必须大于健康检查间隔乘以失败阈值;重新被标记为健康所需的时间必须保持较短;整个排空过程必须在容器的停止宽限期之内完成。改动其中某个数字而不同时调整其他数字,CI 就会失败。我们还对测试本身做了变异测试——把每个数字改成错误的值,并检查是否有测试能够捕获它。
测量部署很容易出错
我们最初的测量结果显示,一次干净的部署大约有 3% 的请求出错。事实并非如此。我们当时统计的是所有非 200 的响应,而排空期间健康端点返回的 503 并不是失败——那是排空在履行职责,告诉负载均衡器在它继续为真实请求提供服务时不要选中该服务器。
用户真正感受到的是 502:请求根本没有到达任何地方。只统计这些,才能得到真实的情况——在三次生产部署中分别为 10、6、0。
我们的收获
- 自愈机制需要了解计划内的停机。 一个无法区分部署与崩溃的看门狗,迟早会与部署对抗。
- 排空是一次握手。 服务器必须持续提供服务,直到负载均衡器可以确定已经停止发送流量为止,而这一时序是在负载均衡器上设定的,而不是在服务器上。
- 把不变量编码为测试。 运维相关的数字会逐渐漂移。把它们彼此关联起来的测试不会。
- 在测量之前决定要测量什么。 「错误」是错误的指标;「没有到达任何地方的请求」才是正确的指标。
如果你在 Mythex 上发布应用,这些对你都不可见——而这正是目的所在。相关阅读:你的已发布应用如何在重新发布时保持在线。