bigywater.tech
微服务 · 89

滚动发布时客户端看到的错误窗口:readiness 摘流的时间差与 preStop

发布一个新版本的时候,用户会不会看到错误?错误从哪来?readinessProbe、preStop、应用优雅退出各解决哪一段?这篇用 kind 集群上的 3 副本 Deployment 和集群里的压测客户端,把每一种配置的失败数、失败类型逐项量出来。

13.38% → 0
没有保护的滚动发布与 preStop sleep 5 + readinessProbe 的失败率(各跑了一次)
126 → 4
加 readinessProbe 之后 refused 的数量(启动阶段的失败基本消失)
53 → 0
长连接客户端在 preStop sleep 5 + 立刻退出与 + 应用优雅退出下的失败数

裸滚动发布

实测 · 本机执行输出
  客户端请求 1173 个,失败 157 个(13.38%),其中 /slow 请求失败 2 个;失败类型 {'refused': 126, 'timeout': 31};第一个到最后一个失败相隔 4.0 s;最长一段没有任何成功响应的间隔 2.41 s;客户端第一次收到 v2 在 8.8 s,最后一次收到 v1 在 6.4 s(从客户端开始算);kubectl rollout 耗时 1 s

没有 readinessProbe 时新 Pod 一启动就算 Ready,应用还没监听;旧 Pod 同时被杀,出现了 2.41 s 没有任何成功响应的窗口。

readinessProbe

实测 · 本机执行输出
  客户端请求 2229 个,失败 65 个(2.92%),其中 /slow 请求失败 16 个;失败类型 {'reset': 16, 'refused': 4, 'timeout': 45};第一个到最后一个失败相隔 9.6 s;最长一段没有任何成功响应的间隔 0.11 s;客户端第一次收到 v2 在 10.4 s,最后一次收到 v1 在 18.1 s(从客户端开始算);kubectl rollout 耗时 13 s

新 Pod 没准备好不接流量,启动阶段的 refused 基本消失;终止阶段的 reset 和 timeout 还在。

preStop sleep

实测 · 本机执行输出
  客户端请求 2495 个,失败 0 个(0.00%),其中 /slow 请求失败 0 个;失败类型 {};没有失败;最长一段没有任何成功响应的间隔 1.02 s;客户端第一次收到 v2 在 9.4 s,最后一次收到 v1 在 18.3 s(从客户端开始算);kubectl rollout 耗时 12 s

让旧 Pod 在被摘出 Service 的传播期间继续活着,这次测量终止阶段的失败是 0。

应用优雅退出

实测 · 本机执行输出
  客户端请求 2464 个,失败 0 个(0.00%),其中 /slow 请求失败 0 个;失败类型 {};没有失败;最长一段没有任何成功响应的间隔 1.00 s;客户端第一次收到 v2 在 9.0 s,最后一次收到 v1 在 18.0 s(从客户端开始算);kubectl rollout 耗时 12 s

收到 SIGTERM 后继续服务到空闲,不用 preStop 也是 0 失败;但这个启发式依赖流量模式。

两者叠加

实测 · 本机执行输出
  客户端请求 2593 个,失败 0 个(0.00%),其中 /slow 请求失败 0 个;失败类型 {};没有失败;最长一段没有任何成功响应的间隔 1.00 s;客户端第一次收到 v2 在 9.4 s,最后一次收到 v1 在 19.3 s(从客户端开始算);kubectl rollout 耗时 13 s

三种有保护的配置失败都是 0,差别在别的场景里才看得出来。

长连接

实测 · 本机执行输出
  keep-alive、readinessProbe + preStop sleep 5、应用立刻退出:请求 2499 个,失败 53 个(2.12%),其中 /slow 失败 0 个;失败类型 {'reset': 53};第一个到最后一个失败相隔 8.5 s;最长无成功响应的间隔 1.00 s
  keep-alive、readinessProbe + preStop sleep 5 + 应用优雅退出:请求 2498 个,失败 0 个(0.00%),其中 /slow 失败 0 个;失败类型 {};没有失败;最长无成功响应的间隔 1.00 s
说明(哪些是测到的,哪些是推测)
  每种配置只跑了一次,没有重复,失败数每次运行会不一样(开发中的一次运行里,第一种配置失败 129 个,第二种读日志时客户端还没结束);「最长一段没有成功响应的间隔」在没有任何失败的几种配置里也有大约 1 s,我没有查清原因(可能是客户端自己的停顿),所以只有第一种配置里的 2 s 多的间隔能说明问题;timeout 失败的原因是推测(旧 Pod 已经删掉,往它的 IP 发的连接没有任何回应),没有抓包验证;第二种配置里 reset 的数量与 /slow 失败的数量相同,我认为是正在处理的慢请求被进程退出打断,没有逐条核对;应用的「优雅退出」是我手写的(SIGTERM 后继续服务到空闲 1 s),不代表框架自带的行为

preStop 管不到已建立的长连接,进程退出时连接被重置;优雅退出时响应带 Connection: close 才能让客户端换连接。

分步演示

发布时的错误窗口
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 arm64 / Python 3.14.8 / Go / nginx / Envoy / etcd / Consul / Nacos / OpenTelemetry / Prometheus(按各篇注明用到哪些及版本);所有服务都是本机自己起的进程,端口 9150 以上,只监听 127.0.0.1,不连任何外部服务,不碰本机已有的实例;故障用 kill -9 进程、让连接断开或在代码里注入延迟与错误来制造;计时用 time.perf_counter,数字每次运行会有小幅抖动。kind 集群(2 个 worker),应用是 python:3.12-alpine 里手写的 HTTP 服务(启动要 3 s,SIGTERM 的两种处理方式由环境变量切换),Deployment 3 副本,maxSurge 1、maxUnavailable 0,readinessProbe 为 httpGet 每秒一次;客户端是集群里的一个 Pod(50 个线程约 100 个请求/秒,每 20 个请求有一个 /slow,每次新建连接,超时 5 s,长连接场景复用连接);每种配置只跑了一次,失败数每次运行不同(开发中的一次运行里,第一种配置失败 129 个)
    • 没有验证:timeout 失败的原因(推测是往已删除的 Pod IP 发连接没有回应,没有抓包);「最长无成功响应的间隔」在零失败的场景里也约 1 s,没有查清原因,所以只用了第一种配置里的 2.41 s;重复多轮后各配置失败数的分布;maxUnavailable 大于 0 或 maxSurge 为 0 的发布策略;PodDisruptionBudget、terminationGracePeriodSeconds 过短的情形;Ingress 或服务网格代理(Envoy、nginx)自身的连接排空;gRPC 与 HTTP/2 长连接的 GOAWAY 行为
    京ICP备2026067502号-1京公网安备11010502063487号