原文:https://dev.to/remdore/your-zero-downtime-deploy-is-probably-fine-check-your-p99-before-you-believe-it-46g2(作者 @remdore)
在滚动重启过程中寻找丢弃的请求时,我发现了比丢弃请求更烦人的事情:一个看似完美却并非如此的部署。
测试设置故意保持简单。两个 Node/Express 副本在 nginx 后面,十个客户端向一个需要三秒的端点发起请求,通过 docker stop 在中途停止一个副本。这个应用是我们大多数人曾经部署过的版本,完全没有信号处理:
app.get('/work', async (req, res) => {
await new Promise(r => setTimeout(r, 3000));
res.json({ ok: true, pid: process.pid });
});
app.listen(8080);
没有 SIGTERM 处理程序。Docker 发送信号,Node 退出,任何进行中的请求都会随之消亡。我预期会有一堆 502 错误。
total=65 ok=65 failed=0
零。三次运行,每次都是零。
故障存在,只是 nginx 为其买单
请求确实失败了。nginx 在任何响应头发送之前捕获到上游连接断开,因此它悄悄地向另一个副本打开连接并重新执行整个请求。客户端一无所知。
这种行为是 proxy_next_upstream,默认开启,我想公平地说,因为它在后端在请求中途消失时正是你希望反向代理做的事情。它也是你的仪表盘可以报告完美部署而部署的东西却悄悄损坏的原因,这对于指标来说是一个奇怪的位置。
它唯一显现的地方是延迟:
naive app: p50 = 3.01s p99 = 5.94s graceful app: p50 = 3.02s p99 = 3.04s
相同的零错误结果,相同的负载,相同的一切。受影响的请求花费了两倍的时间,因为它们被执行了两次。如果你监控错误率,你什么也看不到。如果你监控 p99,你可能已经学会了忽略每次部署时的尖峰。
三秒的额外延迟是可以忍受的。做两次工作可能不是,这完全取决于工作是什么:重试的搜索查询不花费你任何东西,重试的出站电子邮件会给你带来重复,重试的支付授权会给你带来来自财务人员的电话。nginx 不知道它刚刚重新运行了哪一个。
移除安全网
许多设置没有那个重试。Kubernetes Service 是 iptables 或 IPVS,它不会重新运行你的请求。L4 负载均衡器不会。直接与你的应用通信的客户端肯定不会。一旦头信息在网络上,即使是 nginx 也无法改变。
相同的测试,proxy_next_upstream off:
naive app: 5 / 70 requests failed (7%)
那就是我想要找的一堆 502 错误。应用没有任何改变。唯一的区别是上游是否有东西在覆盖它。
修复,以及为什么它只是大部分的修复
应用级修复是每个人都写过的。停止接受新连接,让进行中的连接完成,然后退出:
const server = app.listen(8080);
process.on('SIGTERM', () => {
server.close(() => process.exit(0));
});
相同的测试:
naive: 5 / 70 failed graceful: 1 / 71 failed
更好了。没有完全修复。最后一个失败很顽固,它在所有三次运行中都出现,它是有趣的一个。
进行中的请求现在安全了。剩下的是在 server.close() 和负载均衡器弄清楚这个实例已经消失之间的间隙到达的请求,在这个间隙中 nginx 仍然将地址保留在其上游列表中,所以它做了合理的事情,向一个刚刚停止接受连接的套接字打开一个新连接,这为没有做错任何事的客户端产生了一个 502。
再多的应用代码也无法修复这一点。应用已经做了正确的事情。负载均衡器仍然指向它。
首先将其从轮换中移除
从负载均衡器中移除实例,给更改一些时间稳定,然后发送 SIGTERM:
graceful + drained from the LB first: 0 / 70 failed (3 runs)
那就是整个顺序。先停止路由到它,然后停止它。在 Kubernetes 中,这就是 preStop 钩子的作用,它也是为什么 preStop: sleep 5 看起来像 hack 而不是。sleep 不是为了你的应用,它已经完成了。它是为了让端点删除在容器消失之前传播。
我在途中搞错的地方
我第一次尝试排空测试的结果是 3、1 和 1 次失败,我几乎写了一段解释说排空没有你希望的那么有帮助。
那是我的错误。我以只读方式挂载了 nginx.conf,所以交换排空配置的命令在没有抱怨的情况下失败了,排空从未发生,我实际上做的是运行相同的测试两次,然后为两个相同的东西之间的差异编写了一个解释。使用可写挂载,它是零 out of seventy,连续三次运行。
值得大声说出来,因为失败模式如此普通:我的测试工具以产生看似合理数字的方式损坏了。
完整测试结果
失败/总数 p50 p99 朴素方案, nginx 重试开启(默认) 0 / 65 3.01s 5.94s 优雅方案, nginx 重试开启 0 / 70 3.02s 3.04s 朴素方案, 无重试 5 / 70 3.01s 3.05s 优雅方案, 无重试 1 / 71 3.02s 3.05s 优雅方案 + 先从负载均衡器排空 0 / 70 3.02s 3.05s
每组运行三次,每次结果完全一致。
周一上班时我会检查这些
错误率不会告诉你是否遇到了这个问题。如果你的应用前面有一个会重试的代理,错误率恰恰是会掩盖问题的指标。
相反,请在部署期间观察 p99 延迟。尾部延迟在几秒内大约翻倍,然后恢复平稳——这就是典型特征,因为它反映了一个请求被运行、被终止、然后在别处重新运行的过程。
接下来需要分别检查两个部分,因为它们各自独立失败。你的应用有没有处理 SIGTERM 信号的程序来完成正在处理的工作?你的平台在发送这个信号之前,是否停止向该实例路由请求?只有前者而没有后者,仍然会泄露请求,只是泄露的数量会少一些。
所有测试都可以在笔记本电脑上运行。只需要两个容器、nginx 和一个统计结果的负载生成器。无需云账户,无需注册任何服务。
原文:https://dev.to/remdore/your-zero-downtime-deploy-is-probably-fine-check-your-p99-before-you-believe-it-46g2(作者 @remdore)



