同事反馈网关偶发 503,响应体是
no healthy upstream,而 CI/CD 那边显示发布成功。我第一版的结论是改就绪探针的时间参数,当场被打回:「我看你只是做了 probe 的时间修改仅此而已吗?」打回得对,我手里没有任何东西能把 503 和探针连起来。后来把 Service 端点列表的状态按秒录下来,看到发版期间它空了 101 秒。我的判断是:根因是部署策略选了 Recreate,端点列表空着的那段时间里网关没有任何上游可挑,探针参数怎么调都不会让这段时间消失。
摘端点不看探针
Pod 一进 Terminating,端点控制器就把它从 Service 端点列表里摘掉。这一步和探针无关——那一秒探针还在返回 200。
就绪探针管的是另一头:新 Pod 什么时候被加进列表。initialDelaySeconds 往小调,新 Pod 能早几秒进去。
Recreate 的顺序是先杀后建:旧 Pod 被摘 → 列表空 → 新 Pod 就绪 → 列表恢复。探针参数能压缩的只有后半段,列表空掉这个状态它碰不到。
改探针参数的真正风险是把必现改成偶发。同一个 Recreate 配置我测过两轮,列表空了一次 15 秒、一次 101 秒,差别只是节点上有没有缓存过这个镜像。在一个本身就在 15 到 101 秒之间浮动的窗口上做「改之前有 503、改之后没有」的对比,改探针、改副本数、甚至什么都不改重测一遍,都可能得到「好了」。而一个每次发版都复现的故障被改成偶尔复现,排查成本反而上去了,道理和静默失败比报错难查一样。
早期还有一个方案是用 nodeSelector 把 Pod 钉到固定节点。钉住之后镜像常驻本机,发版时省掉拉取那一段,列表空着的时间跟着变短,现象同样会变成偶发。这个方案被否掉的理由和 503 无关:「如果一个操作需要通过绑定节点这种硬编码的方式来解决,那么他本身就是错的」。
探测点要和报故障的人在同一条路上
第一轮复现我从一个调试 Pod 里打 Service 的 ClusterIP。353 次探测里 22 次失败,全是连接被拒和超时,一个 503 都没有。
失败的类型就不对。no healthy upstream 是网关(Envoy)自己生成的响应体,意思是请求已经到了网关、网关在上游集群里挑不出健康成员;连接被拒和超时比它早一步,连都没连上。直连 ClusterIP 不经过网关,我抓到的失败落在 kube-proxy 那一层,和同事看到的那串字符是两回事,靠它只能推断。
同事最初怀疑服务代码,这个响应体正好能排除掉:它由网关生成,请求没到过后端进程;而端点列表空着的那段时间后端进程根本不存在,没有哪行代码能在那段时间返回任何东西。
第二轮改成在网关 Pod 内部发探测,走的路和线上请求完全相同:网关 → 端点列表 → 后端 Pod,这一轮才拿到带 no healthy upstream 的响应体。同一轮还每秒拉一次端点列表,记下里面有几个地址、各属于哪个 Pod。两条线按秒对齐,哪一秒列表变空、哪一秒开始出 503,直接对得上。
只测好配置证明不了因果
这个服务原本是单副本加 Recreate。改成 RollingUpdate、maxUnavailable 设 0、maxSurge 设 1,再跑一轮 rollout:新旧 Pod 的端点在列表里重叠了 16 秒,地址数一次都没掉到 0,752 次探测 0 个 503。
maxUnavailable 设 0 的含义是任何时刻就绪副本数不许低于期望值,于是新 Pod 必须先进列表,旧 Pod 才允许被摘。那 16 秒重叠是这条约束逼出来的,我没动过任何时间参数。
这组数据只能说明新配置不出 503,没有排除别的原因——那天的网络、某个版本的服务代码、同事那边的客户端重试,换配置之后它们都可能只是恰好没发作。「好配置里没抓到」属于最弱那一档证据,我把否定性证据分过级,这一档只够说「这次没出现」。
把 Recreate 临时改回去,同一套探测原样再跑:端点列表直接变成 EMPTY,持续 101 秒,2595 次探测里 840 个带 no healthy upstream,和同事报的响应体逐字一样。
两轮合起来才串成一条链:Recreate 先杀后建 → 端点列表空 → 网关挑不出健康上游 → 503。坏配置复现得出来、好配置复现不出来,两轮之间只差部署策略这一个字段,503 跟着它出现和消失。验收追问的是「你是通过 rollout 看的?基于事实回答」——两轮都真发了版、端点列表按秒录在手上,数字是 rollout 当时的读数。
把坏配置改回去,等于在共享环境上主动制造 101 秒中断。做之前确认这段时间没人用,跑完立刻改回来。
101 秒比之前那轮的 15 秒长出来的部分,是现拉镜像。真实发版的中断时长取决于镜像在不在本机,15 秒是乐观下界,不是典型值。
这轮实验里不可信的部分
实验中途别人的 CI 往同一个环境推了一次,镜像 tag 被换掉,我有一组数据引用的是十分钟前读到的镜像值。核对出来之后那组重跑了。报数据之前得重新读一遍环境,上一个实验窗口的读数不算。
改成 RollingUpdate 还牵出两个前置条件,都还没跟 503 一起验过:滚动期间会短暂存在两个 Pod,两个都会跑数据库迁移,需要先有一把分布式锁;服务挂了节点本地目录,新 Pod 落到别的节点会读不到数据。
「CI/CD 显示成功、服务其实还没好」这个现象,我只把 Recreate 这条路径对上了。CI 凭什么判成功,没查。