1. 问题背景
先说两个最近半年处理过的真实故障,都是 502/504,但根因完全不同。
故障一:支付回调服务,每天固定几个时间窗口出现零星 502,占比不到 0.1%。对账时发现少量回调没有落库,追查才暴露出来。表面上看是"Ingress 报 502",实际上跟 Ingress 本身几乎没有关系,根因在发布流程:每次滚动更新时,业务 Pod 收到 SIGTERM 后立刻退出,而 Ingress 的 upstream keepalive 连接池里还持有到这个 Pod 的长连接,下一次复用时连接已经被对端关闭,nginx 记一次 upstream prematurely closed connection,返回 502。
故障二:报表导出接口,页面偶发 504,规律是数据量大的月份必现。根因更直白:导出接口处理时间 65 秒左右,超过了 ingress-nginx 默认的 proxy-read-timeout 60 秒。业务方希望"把超时调大就行",但直接全局调到 300 秒会让 nginx 和后端应用的连接、线程在慢请求洪峰时被打满,所以正确做法是按 Ingress 单独加 annotation,同时给导出接口单独限流。
这两个故障的共同点是:错误码出现在 Ingress 层,但根因可能在应用层、发布流程、网络层、参数配置任何一环。很多初中级运维同学的排查习惯是"看到 502 就重启 ingress-controller"或者"看到 504 就无脑调大超时",前者大概率无效还制造了一次无谓的抖动,后者把定时炸弹从 60 秒延长到 300 秒,爆炸时杀伤力更大。
这篇文章的目标是把 502/504 的排查做成一条有证据链的流水线:每一层用什么命令看、看哪些字段、什么输出对应什么结论、改完怎么验证、出问题怎么回滚。文中以 ingress-nginx(ingress-nginx controller)为主要对象,因为它在国内 k8s 集群里占比最高,但排查思路对 Traefik、HAProxy Ingress、云厂商 Ingress、APISIX 同样适用,参数对应关系会在相应位置说明。
2. 适用场景
以下场景适用本文的排查路径和配置方法:
- 通过 Ingress 暴露的 HTTP/HTTPS 服务间歇性或持续性返回 502 Bad Gateway;
- 通过 Ingress 暴露的接口固定超过 60 秒左右返回 504 Gateway Timeout;
- 滚动更新、缩容、节点 drain 期间出现 502 毛刺;
- 大文件上传、长轮询、SSE、AI 推理等长耗时接口经过 Ingress 被掐断或报超时;
- 上游是 Gunicorn/Uvicorn/Node.js 等默认 keep-alive 空闲时间很短的应用,出现无法复现的零星 502;
- 需要为不同接口规划合理的超时链路(LB → Ingress → 应用),避免各层超时参数互相打架;
- 需要给 502/504 建立监控告警和复盘机制。
以下情况本文只能提供参考,不能完全照搬:
- 使用 Istio/Envoy sidecar 的集群,Envoy 的超时模型(
timeout、stream_idle_timeout、idleTimeout)和 nginx 不同,路由级超时要看 VirtualService/DestinationRule;
- 使用云厂商托管 Ingress(如 ALB Ingress、MSE Ingress)时,部分参数在云控制台或 CRD 里配置,不在 ConfigMap;
- 四层 CLB/NLB 透传 TCP 的场景,此时 502/504 由集群内的 nginx 产生,但连接超时行为由 LB 的空闲连接超时决定。
3. 核心知识点
3.1 一次请求经过的完整链路
排查 502/504 之前,必须先在脑子里画出请求路径。国内典型的生产部署是这样的:
客户端
│
▼
DNS / CDN
│
▼
云负载均衡 LB(SLB/CLB/ALB) ← 有自己的空闲连接超时、健康检查
│ NodePort / LoadBalancer / 直接挂 Ingress Pod
▼
ingress-nginx controller Pod ← 502/504 最常见的"报告者",未必是"制造者"
│ upstream = Service 的 EndpointSlice
▼
kube-proxy(iptables/IPVS)或直连 Pod IP
│
▼
业务 Pod(Tomcat / Gunicorn / Node / ...) ← 真正的根因高发区
关键点一:ingress-nginx 生成的 nginx 配置里,upstream 不是写死的 Service ClusterIP,而是从 EndpointSlice 同步的 Pod IP 列表(ingress-nginx 使用自己维护的 Lua 动态后端,绕过 kube-proxy)。这意味着 Service 的 ClusterIP 通不通和 Ingress 通不通是两件事,排查时要分开验证。
关键点二:502/504 这三个字出现在浏览器里,不代表是 Ingress 产生的。业务 Pod 里如果也跑了一个 nginx(很常见的双 nginx 结构:入口 nginx + 容器内 nginx),容器内 nginx 返回的 502 会被入口 ingress-nginx 原样透传。后面 3.4 节会讲怎么用日志字段区分这两种情况,这是整个排查流程的分水岭。
关键点三:每一层都有自己的超时和空闲连接回收时间。在"主动复用连接的一方"(代理)和"被动接受连接的一方"(后端)之间,只要后端的空闲连接回收时间比代理侧的连接复用窗口短,就会产生竞态,表现为无法稳定复现的零星 502。这是 502 排查里最隐蔽也最高频的一类,方向判断见 3.7。
3.2 502 与 504 的准确语义
这两个错误码在 nginx 语义里界限非常清楚,先记牢:
502 Bad Gateway:代理没能从上游拿到一个有效的 HTTP 响应。 具体触发原因(都会记录在 ingress-nginx 容器的 error.log 里):
| error.log 关键串 |
含义 |
典型根因 |
connect() failed (111: Connection refused) while connecting to upstream |
TCP 连接被拒 |
Pod 进程已死/正在重启,但 Endpoint 还没摘除 |
recv() failed (104: Connection reset by peer) while reading response header from upstream |
对端发了 RST |
应用崩溃、连接被中间设备重置、conntrack 表异常 |
upstream prematurely closed connection while reading response header from upstream |
对端在返回完整响应前正常关闭连接 |
keepalive 竞态、应用 worker 被回收、请求超时被应用先掐断 |
upstream sent invalid header / upstream sent too big header |
上游响应头非法或过大 |
检查响应头;过大时评估 proxy-buffer-size(large-client-header-buffers 管客户端请求头) |
no live upstreams while connecting to upstream |
该 upstream 所有后端都被标记为不可用 |
被动健康检查(max_fails)把所有 endpoint 熔断,通常伴随前面几种错误批量出现 |
504 Gateway Timeout:与上游通信的某一阶段超时。 连接、发送、读取阶段均可能超时;以下是读取响应头时超时的例子:
upstream timed out (110: Connection timed out) while reading response header from upstream
排查时看 error.log 的 while connecting、while sending、while reading:分别核对建连、发送和读取阶段的超时及上游状态。
一句话区分:502 多与连接错误或无效响应有关,504 表示与上游通信超时。 具体失败阶段以 error.log 为准。
3.3 ingress-nginx 的超时与 keepalive 参数链
ingress-nginx 的相关参数有三个层级:全局 ConfigMap、Per-Ingress annotation、controller 启动参数。最常打交道的是前两个。
默认值(以当前主流 v1.x 版本为准,个别参数在小版本间有调整,用 7.3 节的方法查自己集群的实际生效值):
| 参数 |
默认值 |
含义 |
注意点 |
proxy-connect-timeout |
5s |
与 Pod 建立 TCP 连接的超时 |
建连阶段超时产生的是 504(error.log 为 upstream timed out ... while connecting to upstream);连接被拒/reset 才是 502 |
proxy-read-timeout |
60s |
两次从上游"读"之间的间隔超时 |
注意:不是请求总耗时,是分块响应两次读取之间的间隔 |
proxy-send-timeout |
60s |
两次向上游"写"之间的间隔超时 |
大请求体上传慢时相关 |
upstream-keepalive-connections |
320 |
每个 worker 与 upstream 保活的空闲连接池大小 |
设为 0 会关闭 upstream keepalive,每次请求新建连接,高 QPS 下 TIME_WAIT 暴涨,不要随意关 |
upstream-keepalive-timeout |
60s |
空闲连接在池中保留多久 |
upstream-keepalive-time 限制单条连接总存活时间,含义不同 |
upstream-keepalive-requests |
10000 |
单条复用连接最多服务多少个请求 |
超过后主动关闭,是一种周期性保护 |
proxy-next-upstream |
error timeout |
满足哪些条件时 nginx 会换后端重试 |
上游返回 HTTP 502/503/504 默认不触发重试 |
proxy-next-upstream-timeout |
0(不限额) |
换后端重试的总时间上限 |
单位秒;0 表示不设置额外上限 |
proxy-next-upstream-tries |
3 |
尝试后端的次数上限 |
以所用版本文档和生成配置为准 |
两个容易踩的细节:
-
proxy-read-timeout 是"两次读之间的间隔"。一个流式接口每次隔 30 秒吐一块数据,只要间隔不超过 60 秒,即使总耗时 10 分钟也不会 504;反过来,一个接口 70 秒不出任何字节,哪怕它第 65 秒就要返回结果,也会在第 60 秒被 nginx 掐掉并返回 504。这决定了长任务接口要"定期有心跳输出"或者单独调大超时。
-
annotation 的值不带单位(写 proxy-read-timeout: "120" 表示 120 秒),这是 ingress-nginx annotation 的惯例,和 ConfigMap 里部分带 s 后缀的键不同。写错单位是高频低级错误,nginx -T 检查时如果发现生成的是 1 秒,基本都是这里写错了。
3.4 判断 502/504 到底是谁产生的:upstream_status 对账
ingress-nginx 的 access log 里,$status 是最终返回客户端的状态码,$upstream_status 记录上游尝试关联的状态(多次尝试以逗号分隔)。它们有助于定位重试,但不能单凭两个值确定错误由哪一层生成:连接失败时 NGINX 也可能在 $upstream_status 中记录 502。
| status |
upstream_status |
结论 |
下一步 |
| 502 |
- |
未取得可用于记录的上游状态 |
查 $upstream_addr、EndpointSlice 和 error.log |
| 502 |
502 |
可能是上游返回,也可能是 NGINX 建连失败等原因产生 |
对照 error.log 与后端日志 |
| 200 |
502, 200 |
第一次尝试失败、下一次成功;客户端收到 200 |
看 $upstream_addr 与各次耗时 |
| 504 |
- 或 504 |
存在超时或上游返回 504 的可能 |
用 error.log 区分失败阶段和透传响应 |
诊断时同时保留 $upstream_addr、$upstream_response_time 和 error.log;若自定义日志缺字段,参考 8.1 节补充。结合后端日志确认是否真的返回过该 HTTP 状态码。
客户端侧还可以用一个辅助判断:浏览器/curl 收到的响应如果带 Server: nginx 且 body 是 nginx 默认样式的 <html>502 Bad Gateway</html>,大概率是 nginx 直接产生的(上游产生的 502 往往带业务自己的响应头或 JSON body)。但这个方法只能做初判,不同版本/自定义 error_page 会失真,最终以日志对账为准。
3.5 Endpoints 状态与错误码的对应关系
ingress-nginx 的后端列表来自 EndpointSlice。三种状态对应三种结果:
- EndpointSlice 为空(Service selector 不匹配、没有任何 ready 的 Pod):ingress-nginx 返回 503,不是 502。如果用户报"502"但日志里是 503,说明用户描述和真实错误码有出入,一切以日志为准。
- Endpoint 存在但 Pod 已死/正在终止:TCP 连接被拒或重置 → 502。滚动更新窗口的 502 基本都是这个状态造成的时间窗问题。
- Endpoint ready=true 但应用假死(进程活着但不回包):连接能建立、请求发出去没人处理 → 504。典型场景是 JVM Full GC 停顿、线程池耗尽、应用死锁。
所以 kubectl get endpointslices -l kubernetes.io/service-name=<svc> -n <ns> 的输出要结合错误码类型来读:504 类故障里如果 ENDPOINTS 数量"看起来正常",恰恰不能排除应用假死,ready 探针的探测粒度和业务接口未必一致。
3.6 重试机制的双面性
ingress-nginx 默认 proxy-next-upstream 为 error timeout:连接或通信错误、超时时,在可重试条件下可能尝试另一个后端;上游实际返回 HTTP 502/503/504 默认不在列表中,若要重试须显式配置。重试带来两个后果:
好处是掩盖了大部分瞬时故障,用户无感知。坏处有两个:
-
耗时放大。 后端超时 60 秒 + 重试到另一后端再 60 秒,用户侧最坏要等 120 秒以上才看到 504,客户端可能早就断了。排查时如果发现"用户说等了很久"而单条 upstream_response_time 只有 60 秒,就要意识到日志里的一条记录可能对应了两跳后端。
-
非幂等请求需单独核验。 NGINX 默认不会在请求已发送给上游后把 POST/LOCK/PATCH 换到下一后端;显式添加 non_idempotent 会改变这一限制。尚未发送请求时仍可能换后端。支付、下单等接口应结合请求缓冲、重试配置和业务幂等设计实测(见 11.3 节)。
3.7 keepalive 竞态:零星 502 的头号根因
把这一节单独拎出来,因为故障一就是它。
nginx 作为反向代理,默认会把到后端的连接放进 keepalive 池复用(由 3.3 节的 upstream-keepalive-* 三个参数控制)。复用的前提是:这条连接在后端还活着。问题在于后端对"空闲连接多久关闭"有自己的参数,且很多应用框架的默认值小得惊人:
| 后端 |
空闲 keep-alive 默认值 |
备注 |
| Gunicorn |
--keep-alive 2 秒 |
经典大坑,几乎所有"Gunicorn + Nginx 随机 502"帖子都源于此 |
| Uvicorn |
--timeout-keep-alive 5 秒 |
同上 |
| Node.js(http.Server) |
keepAliveTimeout 5 秒 |
Node 18+ 默认 5s;且 headersTimeout 默认 60s 必须大于 keepAliveTimeout |
| Tomcat/Spring Boot |
keepAliveTimeout ≈ connectionTimeout,约 20 秒 |
Boot 2.3+ 有独立 server.tomcat.keep-alive-timeout |
| PHP-FPM |
不适用(不维持 HTTP keep-alive) |
通常前面还有一层容器内 nginx,看那层的 keepalive_timeout |
| 容器内 nginx |
keepalive_timeout 60/75 秒 |
看具体镜像配置 |
竞态发生的时序:
t0: nginx 与后端建立连接,请求完成后连接进入空闲池(池内保留 60s)
t0+2s: Gunicorn worker 的 keep-alive 2 秒到期,关闭连接(FIN)
t0+2s+ε: FIN 在网络上/内核缓冲里有个传播窗口,nginx 此刻恰好从池中取出这条"看似存活"的连接发出新请求
结果: 后端早已关闭连接,nginx 写入后收到 RST 或读到 EOF →
"upstream prematurely closed connection" → 502(幂等请求会自动重试成功,用户可能无感但日志有记录;
若命中 no live upstreams 或其他失败形态则用户可见)
特征是:量小、随机、与流量高峰弱相关、重启 Pod 或流量停一会儿就好、tcpdump 抓到现场永远是 RST/FIN 竞态。判定和修复见 5.10 节和 7.4 节。
同类竞态还有两个变种:
-
LB 与 ingress 之间: 把 3.7 的规则套到这一跳:ingress 是"被复用方",LB 是"复用方",ingress 对客户端侧的 keepalive 空闲(ConfigMap 键 keepalive-timeout,默认约 75 秒,以 nginx -T 为准)必须大于 LB 的空闲连接超时,否则 ingress 先关、LB 不知情复用死连接,表现为 LB 侧 5xx 或 connection reset。AWS ALB 空闲超时默认 60 秒 < ingress 默认 75 秒,默认参数恰好是安全的;把 ingress 侧 keepalive-timeout 往下调、或把 ALB 空闲超时往上调到 75 秒以上,都会亲手把这个安全裕度调没。
-
滚动更新窗口: Pod 终止时与 ingress 的连接没有优雅排空,见 5.10 节。
统一的规则只有一条:同一段连接上,被动提供服务一方的空闲关闭定时器,必须晚于主动复用连接一方的连接池保留时间。沿链路看,空闲保活时间向内递增——LB 空闲超时(60s) < ingress 客户端侧 keepalive(75s);ingress upstream 池空闲(60s) < 后端应用 keepalive(≥75s)。谁先关连接,谁就在给对面制造竞态。
4. 整体排查思路
4.1 排查总路线
故障类文章的通病是直接跳进命令堆,先给一条完整的路线,后面每一步都能在这里定位:
现象 → 初步判断 → 命令检查 → 关键指标 → 根因定位 → 修复方案 → 验证结果 → 回滚预案 → 复盘总结
落到 502/504 上,把它展开成七个必须回答的问题,按顺序回答,不许跳:
- 错误码到底是 502 还是 504? (用户口述不可信,以 ingress access log 的 status 为准)
- 是 Ingress 产生的还是上游透传的? (3.4 节的 upstream_status 对账)
- 发生频率和影响面? (全量持续 / 周期性窗口 / 随机零星 / 发布时必现 / 特定路径独有——每种形态对应完全不同的根因方向)
- error.log 里的失败串是哪一类? (3.2 节的对照表直接给出方向)
- Endpoint 和 Pod 生命周期有没有问题? (502 主线)
- 耗时和容量有没有问题? (504 主线)
- 各层超时/keepalive 参数是否成阶梯? (3.7 节,零星 502 主线)
形态与根因方向的先验对应关系,拿到第 3 问的答案就能大幅缩小范围:
| 故障形态 |
最可能根因方向 |
直奔章节 |
| 发布/重启/缩容时必现 502 |
Pod 终止竞态、readiness 不严谨、缺 preStop |
5.10 |
| 全天随机零星 502 |
后端 keepalive 空闲太短、conntrack 丢弃、节点资源 |
5.8、5.10 |
| 固定超过 60s 必现 504,接口可复现 |
proxy-read-timeout 与业务耗时不匹配 |
5.9 |
| 高峰期批量 502/504,平时正常 |
应用线程池/队列打满、conntrack 表满、节点丢包 |
5.8 |
| 特定接口 502,其余正常 |
该接口的后端行为(响应头过大、worker 崩溃、上传体积限制) |
5.3、5.9 |
| 全站 502/503 且持续 |
Endpoint 丢失、ingress-nginx 自身异常、证书/SNI 配置错误路由进空 upstream |
5.5、5.6 |
4.2 证据链纪律
三个纪律,后面所有步骤都遵守:
纪律一:先固化现场再动手。 任何修改动作(改 ConfigMap、删 Pod、重启 ingress)执行前,先把当前的 access log 样本、error.log 片段、kubectl get endpointslices 输出、nginx -T 相关段落存到工单里。故障现场一旦重启就没了,事后无法定根因。
纪律二:单一指标不下结论。 proxy-read-timeout=60 且报错时间都在请求后 60 秒,只是"强相关",要再补一环证据(error.log 的 upstream timed out 串 + 应用日志显示该请求实际处理了 75 秒)才能定根因。
纪律三:改动一次只改一个变量。 既调超时又改探针,修好了不知道是哪个起效,修坏了不知道回滚哪个。灰度验证期尤其要守住这条。
4.3 五个高频误判,先排雷再上路
这五个动作是我见过或犯过最多的,每个都对应一条被排除的正确路径:
误判一:"502 是 Ingress 报的,所以重启 ingress-controller。" 错误码由谁产生和故障由谁引起是两回事(3.1 关键点二)。重启 ingress 唯一确定能造成的是全体域名秒级抖动。重启后"好像好了"通常是故障本身的间歇期到了,或是重试机制恰好开始命中健康后端。
误判二:"504 了,把超时全局调到 300 秒。" 全局值是所有接口的公约数,长尾接口应该由 annotation 单独表达(7.1)。而且调大超时没有改变"慢"这件事本身,只是把慢请求的占用时间放长,容量账(13 节)不算清楚,504 会变成全站雪崩。
误判三:"Endpoints 里有地址,网络肯定没问题。" 3.5 节讲过:ready=true 只说明探针通过,应用假死时 Endpoint 一切正常;反过来 Endpoint 正常也不代表 ingress 到该 IP 的 path 没问题(conntrack、跨节点 overlay)。Endpoint 是必要条件的检查项,不是充分证据。
误判四:"access log 里没有 502,用户误报。" 还有一种可能:502 产生在 ingress 之前(LB→ingress 段连接失败、LB 自身健康检查摘除了 ingress 节点),ingress 根本没收到请求所以没有日志。这时要去 LB 层的访问日志/监控里找对应时间窗的 5xx,3.4 的"错误产生层"判断在 ingress 外侧同样成立。
误判五:"重试一次就好了,零星 502 不用管。" 幂等接口重试自愈,代价是每次竞态多一跳延迟;非幂等接口的重试(3.6)是拿重复执行风险换体验。日志里每条被重试掩盖的 502 都应该进待办,而不是被"用户没投诉"结案。
4.4 环境与假设
本文实战部分假设如下环境(都是常见配置,按自己集群替换名称和命名空间即可):
- ingress-nginx controller 部署在
ingress-nginx 命名空间,DaemonSet/Deployment 名为 ingress-nginx-controller,ConfigMap 名为 ingress-nginx-controller;
- 业务:
pay 命名空间,服务 payment-callback,域名 pay.example.com,Ingress 资源名 pay-ingress;
- kubectl 有业务命名空间的读权限和 ingress-nginx 命名空间的读权限,改 ConfigMap 走变更流程;
- 集群版本 1.26+,ingress-nginx v1.9.x,后端为 Spring Boot(内嵌 Tomcat,无容器内 nginx);
- 有 Prometheus + Grafana,ingress-nginx 的 metrics 端口已被抓取(默认 10254,以实际部署为准)。
如果你的集群里 ingress-nginx 是 kube-system 下或叫别的名字,先用这三条命令把对象摸清楚:
# 找到 ingress controller 的 Pod 和它所在的命名空间
kubectl get pods -A -l app.kubernetes.io/name=ingress-nginx -o wide
# 找到它用的 ConfigMap(一般与 controller 同名)
kubectl get cm -A | grep -i ingress
# 看 controller 的启动参数(不同部署工具差异很大,以这里为准)
kubectl -n ingress-nginx get deploy ingress-nginx-controller \
-o jsonpath='{.spec.template.spec.containers[0].args}'
5. 实战步骤:一条完整的排查闭环
以故障一(支付回调零星 502)为主线走一遍完整流程,故障二(导出接口 504)在对应步骤里作为对照。每一步都按"目的 → 命令 → 预期/实际输出 → 判断 → 下一步"的结构写。
5.1 第一步:现象核实与影响面统计
目的: 用户报的"502"未必是 502,先拿到真实错误码、频率、时间分布、受影响的路径集合,决定后面往哪个方向查。
命令: 先从 ingress access log 里做统计。ingress-nginx 默认把访问日志打到容器 stdout:
# 拉取最近 2 小时的 ingress 访问日志,只看 5xx
kubectl -n ingress-nginx logs -l app.kubernetes.io/name=ingress-nginx \
--since=2h | grep -E ' (50[0-9]) [0-9]+ ' | tail -50
注意:默认的 log format 里状态码位置在固定列,粗暴的 grep 可能误伤(URL 里恰好带 502 字样)。更稳的办法是按默认格式取第 9 列(status 字段,见 8.1 节的格式定义):
kubectl -n ingress-nginx logs -l app.kubernetes.io/name=ingress-nginx \
--since=2h --prefix=false | awk '$9 >= 500 {print $9}' | sort | uniq -c | sort -rn
预期输出形如:
137 502
4 504
如果 awk 取出来的列是乱的,说明集群改过 log-format-upstream,先用 nginx -T 看当前 format 再调整列号(见 8.1)。
再按小时看 502 的时间分布:
# $4 是 [18/Sep/2026:03:12:44 这种时间字段的第一个 token,按实际 format 调整
kubectl -n ingress-nginx logs -l app.kubernetes.io/name=ingress-nginx \
--since=24h | awk '$9==502 {split($4,a,":"); print a[2]}' | sort | uniq -c
# 按 URI 聚合,看 502 是否集中在特定接口
kubectl -n ingress-nginx logs -l app.kubernetes.io/name=ingress-nginx \
--since=24h | awk '$9==502 {print $7}' | awk -F'?' '{print $1}' | sort | uniq -c | sort -rn | head
本案例实际输出与判断: 502 集中出现在每天 02:55、10:30、16:10 前后各 2~3 分钟,URI 全部是 /api/callback。时间有规律 + 路径唯一 → 与某个定时动作强相关。马上关联变更记录:kubectl -n pay rollout history deploy/payment-callback 和发布平台记录,确认这三个时间点都有该服务的发布。至此方向已经收敛到"发布窗口 502",但流程继续走完,把证据链补齐。
对照故障二:504 全部集中在 /report/export,单接口可稳定复现,curl 必现,与时间无关 → 直奔耗时方向(5.9)。
5.2 第二步:定位错误产生层(502 还是透传)
目的: 用 3.4 节的方法把"ingress 自产"和"上游透传"分开,避免查错方向。
命令: 在 ingress access log 里同时取出 status 与 upstream_status。ingress-nginx 的日志字段靠近行尾,先确认自己集群的字段顺序:
kubectl -n ingress-nginx exec deploy/ingress-nginx-controller -- \
sh -c "nginx -T 2>/dev/null | grep -n 'log_format'"
输出里能看到完整的 format 定义,对照 8.1 节找到 $status、$upstream_addr、$upstream_status 的位置。然后精确取字段:
# 直接抓 502 的原始行,人眼读 upstream_addr / upstream_response_time / upstream_status 三个尾段字段
kubectl -n ingress-nginx logs -l app.kubernetes.io/name=ingress-nginx \
--since=2h | grep '" 502 ' | head -20
本案例实际输出(截取关键字段):
POST /api/callback HTTP/1.1" 502 157 "-" "okhttp/4.9.3" ... 10.244.7.19:8080 0 0.000 - c3d2a91f...
行尾按默认格式依次是 $upstream_addr(10.244.7.19:8080)、$upstream_response_length(0)、$upstream_response_time(0.000)、$upstream_status(-)、$req_id。两个关键读数:$upstream_status 为 -,提示这次没有取得可记录的上游响应状态;结合下一节 error.log 判断 502 的来源;$upstream_response_time 为 0.000,说明这一跳记录的耗时极短;仍需结合 error.log 核对失败阶段,指向"连接被拒/被重置/提前关闭"。
同时看 $request_time 与 $upstream_response_time 的差值:本案例两者都接近 0,说明没有发生有效重试,与"发布窗口"假设吻合。
提醒一点:失败形态不同,$upstream_status 取值不总一致——"reset/prematurely closed"这类请求已发出后才失败的 502,个别版本会把它合成成 502 写进该字段,此时和"上游透传"无法只用这一个字段区分,必须结合 error.log 原文(下一节)定性。
下一步: 5.3 节看 error.log 拿失败原因原文。(如果后端日志证实其返回了 HTTP 502,再转向业务容器内网关日志排查。)
5.3 第三步:error.log 字符串对号入座
目的: nginx 的 error.log 会把失败原因写得非常明确,这是离根因最近的一条日志。
命令:
# ingress-nginx 的 error log 也在 stdout/stderr,直接过滤
kubectl -n ingress-nginx logs -l app.kubernetes.io/name=ingress-nginx \
--since=2h --tail=500 | grep -E '\[error\]|\[warn\]' | tail -30
本案例实际输出:
2026/09/18 02:56:11 [error] 42#42: *2201543 upstream prematurely closed connection
while reading response header from upstream, client: 10.244.1.3, server: pay.example.com,
request: "POST /api/callback HTTP/1.1", upstream: "http://10.244.7.19:8080/api/callback",
host: "pay.example.com"
upstream prematurely closed connection —— 对照 3.2 节的表:候选根因是 keepalive 竞态、应用 worker 被回收、Pod 终止。结合 5.1 的时间规律(发布窗口),keepalive 竞态/Pod 终止的嫌疑最大,5.10 节做针对性确认。
对照故障二的 error.log:
2026/09/18 14:22:07 [error] 42#42: *2298712 upstream timed out (110: Connection timed out)
while reading response header from upstream, ... upstream: "http://10.244.9.31:8080/report/export"
upstream timed out (110) → 读超时,直接对应 proxy-read-timeout=60s,走 5.9。
风险提醒: error.log 量大的集群,长期开着 info/debug 级别会打爆磁盘,调整 controller 启动参数 --error-log-level 只作为短期取证手段,取证完必须恢复(见 8.2)。
5.4 第四步:检查 Service、EndpointSlice、readiness
目的: 确认 ingress 拿到的后端列表是否正确、ready 判定是否可信。
命令:
# EndpointSlice 是否有目标 Pod、数量对不对
kubectl -n pay get endpointslices -l kubernetes.io/service-name=payment-callback
# 对照 Service 定义(selector、targetPort)
kubectl -n pay get svc payment-callback -o yaml | sed -n '/spec:/,/status:/p'
# 对照 Pod:readiness 是否真的在探业务端口
kubectl -n pay get pods -l app=payment-callback \
-o custom-columns=NAME:.metadata.name,READY:.status.containerStatuses[0].ready,RESTARTS:.status.containerStatuses[0].restartCount,IP:.status.podIP
判断逻辑:
- EndpointSlice 里 ENDPOINTS 为空 → ingress 会返回 503 而不是 502,检查 Service selector 和探针配置,方向立刻转 5.5。
- EndpointSlice 正常 → 继续看 readiness 探测的是什么。本案例发现:
readinessProbe:
tcpSocket:
port: 8080
periodSeconds: 10
tcpSocket 只探 TCP 端口能否建连。Spring Boot 的 Tomcat 在应用上下文还没完全就绪、或者正在优雅下线时,端口都可能是通的。readiness 探测粒度不够,Pod "ready"不等于"能正确服务"。 另外 periodSeconds 10 意味着就绪状态变化最多有约 10 秒才被发现;Endpoint 摘除虽然由 Pod 进入 Terminating 直接触发(比 readiness 快),但传播到 ingress-nginx 的 Lua 后端表仍有毫秒到秒级延迟,空窗见 5.10。
5.5 第五步:检查业务 Pod 健康与生命周期
目的: 确认 502 窗口内 Pod 本身的状态、重启原因、探针失败记录。
命令:
# 发布窗口相关的 Pod 事件
kubectl -n pay get events --sort-by=.lastTimestamp | grep -iE 'payment-callback' | tail -30
# 容器退出原因:OOMKilled?137(SIGKILL)?143(SIGTERM)?
kubectl -n pay get pod <pod-name> \
-o jsonpath='{.status.containerStatuses[0].lastState}'
在业务节点上确认是否发生 cgroup OOM:
dmesg -T | grep -i 'killed process' | tail
判断逻辑:
- lastState 里
reason: OOMKilled → 内存超限被杀,502 只是表象,根因是内存;
exitCode: 143(128+15,SIGTERM)是正常终止信号,说明 Pod 是被滚动更新终止的,回到发布竞态方向;
exitCode: 137(128+9,SIGKILL)且 reason 是 Error → 可能是超过 terminationGracePeriodSeconds 被硬杀,也可能是节点资源驱逐,看 kubectl describe pod 的 Events 里有没有 Evicted。
本案例:exitCode: 143、时间戳与 502 窗口完全吻合 → Pod 是被正常滚动更新终止的,但终止过程不够"优雅",5.10 做最终定位。
5.6 第六步:分层连通性测试(把网络层和应用层切开)
目的: 无论 5.1~5.5 有没有结论,这一步都要做,它把"网络可达性"和"应用行为"彻底分开,也是 504 类故障的必做项。
命令: 逐层向内探测,各层结果交叉对比:
# 拿到一个后端 Pod IP
POD_IP=$(kubectl -n pay get pod -l app=payment-callback \
-o jsonpath='{.items[0].status.podIP}')
# 1) 从 ingress-nginx Pod 内部直接打 Pod IP —— 最贴近 ingress 的视角
ING_POD=$(kubectl -n ingress-nginx get pod -l app.kubernetes.io/name=ingress-nginx \
-o jsonpath='{.items[0].metadata.name}')
kubectl -n ingress-nginx exec "$ING_POD" -- sh -c \
"timeout 3 sh -c 'echo > /dev/tcp/${POD_IP}/8080' && echo OPEN || echo CLOSED"
# 2) ClusterIP 层(验证 Service/kube-proxy 规则,与 ingress 后端同步相互独立,用于交叉对比)
kubectl -n pay run curl-tmp --rm -i --restart=Never \
--image=curlimages/curl:8.7.1 --command -- sh -c \
'curl -s -o /dev/null -w "clusterip http_code=%{http_code} time=%{time_total}\n" --max-time 5 \
http://payment-callback.pay.svc.cluster.local:8080/actuator/health'
# 3) 经 Ingress 完整路径(带分段计时)
curl -s -o /dev/null \
-w "ingress http_code=%{http_code} t_connect=%{time_connect} t_total=%{time_total}\n" \
--max-time 70 -H 'Host: pay.example.com' \
https://<ingress-lb-ip>/api/health
curl -w 输出各字段的判断逻辑:
time_connect 异常大或直接 Connection refused → 连接层问题,查网络/conntrack(5.8);
- 直连 Pod IP 通、经 Ingress 不通 → 问题在 ingress-nginx 的配置或它的后端同步,查
nginx -T 生成的 upstream 段和 controller 日志;
- 全部层通、但特定接口慢 → 应用层耗时问题(5.9)。
风险提醒: kubectl run 会在集群里创建临时 Pod,务必带 --rm 确保退出即删;镜像建议用私有仓库里同步过的 curl 镜像,避免节点拉不到外网镜像造成误判"网络不通"。探测完成后 kubectl -n pay get pod curl-tmp 确认已删除。
5.7 第七步:应用日志交叉对账
目的: 把 ingress 侧记录的失败请求时间戳,拿到应用日志里找同一条请求,确认应用"是否收到、处理了多久、怎么结束的"。
命令: ingress-nginx 默认会在响应头回写 X-Request-ID(对应日志里的 $req_id),用它做全链路串联:
# 1) 复现一次请求并抓 request id
curl -sk -D - -o /dev/null --max-time 70 \
-H 'Host: report.example.com' https://<ingress-lb-ip>/report/export | grep -i x-request-id
# 2) 用这个 id 去应用日志里找
kubectl -n report logs -l app=report-svc --since=10m | grep '<request-id>'
判断逻辑:
- 应用日志里没有这条请求 → 请求根本没进应用,结合 5.6 分层结果判断丢在哪一跳;
- 有请求开始、没有结束 → 应用在处理中被打断(Pod 终止、worker 被杀),或仍在处理;看后续日志里这条请求实际完成耗时——故障二正是这里实锤的:应用日志显示该导出请求实际耗时 74 秒,而 ingress 在第 60 秒已经返回 504。证据链闭合:接口耗时 74s > proxy-read-timeout 60s。
故障二根因到此定位。修复方案不是简单调大超时,见 5.11 与第 7 节的组合方案。
5.8 第八步:网络层排查(conntrack、队列溢出、节点丢包)
目的: 排除或确认"随机 502/连接重置"类故障的网络层根因。高峰期集中爆发的场景尤其要查。
8a. conntrack 表满(高频隐形杀手):
# 内核是否已出现 conntrack 丢弃日志(在业务节点上执行)
dmesg -T | grep -i conntrack | tail
# 典型输出:nf_conntrack: table full, dropping packet
# 当前用量 vs 上限
sysctl net.netfilter.nf_conntrack_count net.netfilter.nf_conntrack_max
# 有 conntrack-tools 的话看丢弃计数
conntrack -S | grep -E 'drop|insert_failed'
count 逼近 max 即确认。k8s 节点很容易撞这个上限,因为 overlay 网络和大量短连接都会占表项。修复方向:调大 nf_conntrack_max(约占 300 字节/条,用内存换容量);缩短 nf_conntrack_tcp_timeout_established(默认 5 天)可加速释放但影响长连接,要谨慎。修改内核参数属于高风险操作:必须先备份现值、按节点灰度、观察至少 30 分钟,回滚就是 sysctl 改回备份值:
# 示例:先备份,再逐节点调整(值按机器内存决定,不要照抄)
sysctl -a 2>/dev/null | grep nf_conntrack > /root/conntrack-backup-$(date +%F).txt
sysctl -w net.netfilter.nf_conntrack_max=1048576
# 确认有效后再写入 /etc/sysctl.d/90-conntrack.conf 持久化;
# 注意:持久化文件写好、确认无误之前不要重启节点,未持久化的值重启即丢(重启也是一种回滚)
8b. TCP accept 队列溢出(应用处理慢的旁证):
# 监听队列溢出的累计计数
netstat -s | grep -iE 'listen|overflow'
# 典型输出:X times the listen queue of a socket overflowed
# 实时观察监听队列水位
ss -ltnp | grep 8080
# 输出中 Send-Q 是 backlog 上限,Recv-Q 是当前排队数,Recv-Q 逼近 Send-Q 即危险
应用侧配合同看:Tomcat 的 server.tomcat.accept-count、应用线程池 busy 数。accept 队列溢出时,内核默认(tcp_abort_on_overflow=0)静默丢弃连接报文而不是回 RST,nginx 侧表现为建连变慢甚至 proxy_connect_timeout 超时(504,error.log 为 upstream timed out ... while connecting to upstream);若开了 syncookie,握手能成功但请求会在队列里长时间排队,$upstream_response_time 大幅拉高最终 504。这是"高峰期批量 502/504"的经典链路之一。
8c. 节点间丢包(overlay/物理网络问题):
# 网卡级错误与丢包计数是否持续增长
ip -s link show <网卡名>
# 到 ingress 节点的系统性延迟/丢包(在其他节点或压测机上)
mtr -rwzbc 200 <ingress节点IP>
# 需要现场抓包时(务必限时限量,防止磁盘与 CPU 被打爆)
timeout 60 tcpdump -i any -nn -c 5000 -w /tmp/pay-502.pcap \
'tcp port 8080 and (tcp[tcpflags] & (tcp-rst|tcp-fin) != 0 or tcp[tcpflags] & tcp-syn != 0)'
tcpdump 的 RST/FIN 过滤器专门用来确认"谁先关的连接"。故障一就是靠这条命令拿到铁证的:每次 502 前,都是 Pod 侧先发 FIN,ingress 侧随后复用该连接发请求被 RST。
8d. 节点资源: 磁盘 IO 打满会让应用日志写入阻塞导致响应变慢、CPU 打满会让探针超时误杀 Pod,顺手用 top、iostat -x 1、vmstat 1 扫一遍异常节点,非本文主线不展开。
5.9 第九步:耗时与容量分析(504 主线)
目的: 对 504 类故障,量化"接口到底多久、超时预算够不够、容量扛不扛得住"。
命令与观察: access log 的 $request_time 可以做粗统计(列位置先对照 8.1 确认,不要照抄列号):
# 按 URI 统计平均 request_time 与样本数,取最慢的 20 个接口。
# 不数死列号(UA 字段含空格会打乱列):request_time 是默认格式里
# 第一个形如 [$proxy_upstream_name] 的方括号字段的前一个字段,用循环定位它
kubectl -n ingress-nginx logs -l app.kubernetes.io/name=ingress-nginx --since=1h \
| awk '{for(i=10;i<=NF;i++) if($i ~ /^\[/){rt=$(i-1)+0; break}
uri=$7; sub(/\?.*/,"",uri); s[uri]+=rt; c[uri]++}
END {for (u in s) printf "%.3f %6d %s\n", s[u]/c[u], c[u], u}' \
| sort -rn | head -20
更可靠的是用 Prometheus 直方图(指标名以实际 exporter 为准,见 8.3):
# 各后端接口的 p99 响应时间(近 15 分钟)
histogram_quantile(0.99,
sum by (le, service) (
rate(nginx_ingress_controller_request_duration_seconds_bucket[15m])))
故障二的对账过程:
- Prometheus 显示
/report/export p99 约 74s,p50 约 40s,全部在 40s 以上;
- ingress error.log:
upstream timed out (110);
- 应用日志:导出任务实际处理 74s,期间无异常;
- ingress
nginx -T 确认 proxy_read_timeout 60s。
四个证据互相咬合,根因确定:接口正常耗时超过全局读超时。这不是"故障",是参数规划缺失——修复要同时回答"这个接口业务上允许多久"和"调大超时后连接与线程够不够"两个问题,结论见 5.11。
504 但接口平时很快、只在高峰期超时 则是另一条线:查应用线程池指标(Tomcat busy 线程数、连接池等待)、JVM GC 停顿、数据库慢查询——应用自己排不过队,与 ingress 无关,只是 504 由 ingress 报出来。判断依据是应用侧指标尖刺与 504 时间窗同步。
5.10 第十步:发布窗口与 keepalive 竞态专项(零星 502 主线)
目的: 确认"发布/终止竞态"与"keepalive 空闲竞态"这两类最难复现的 502。
10a. 发布窗口时间轴对账。 把滚动更新时间、Endpoint 摘除时间、502 发生时间放进一张表:
# 发布历史与 ReplicaSet 时间线
kubectl -n pay rollout history deploy/payment-callback
kubectl -n pay get rs -l app=payment-callback \
-o custom-columns=NAME:.metadata.name,DESIRED:.spec.replicas,READY:.status.readyReplicas,AGE:.metadata.creationTimestamp
# 旧 Pod 终止事件时刻
kubectl -n pay get events --sort-by=.lastTimestamp | grep -i killing | tail
故障一的输出对账结果:旧 Pod 的 Killing 事件时刻与 502 窗口完全重叠,且每个发布窗口的 502 次数与旧 Pod 数正相关。判定:ingress 在旧 Pod 进入 Terminating 后仍向其发送了请求。
10b. 确认优雅终止配置缺失。
kubectl -n pay get deploy payment-callback -o yaml | grep -E 'preStop|terminationGracePeriodSeconds|lifecycle'
故障一集群实际输出:terminationGracePeriodSeconds: 30,无 preStop。机制说明:kubelet 在 Pod 进入 Terminating 后并行做两件事——把 Pod 从 Endpoint 摘除(异步传播到 ingress),以及执行 preStop 钩子(没有钩子则立刻发 SIGTERM)。没有 preStop 时,应用几乎立即关闭监听,而 Endpoint 变更传播到 ingress-nginx 的 Lua 后端表需要时间,这个空窗里到达的连接就是 Connection refused / prematurely closed → 502。
10c. keepalive 空闲竞态抓包实证。 对 5.3 节怀疑"后端空闲关闭太快"的场景,在业务 Pod 所在节点抓包:
# 在业务节点执行,过滤 8080 上带 FIN/RST 的包,限时 60 秒防止刷爆磁盘
timeout 60 tcpdump -i any -nn -c 300 \
'tcp port 8080 and (tcp[tcpflags] & (tcp-fin|tcp-rst) != 0)'
观察:空闲连接由 Pod 侧发 FIN 前的空闲时长。若恰好等于应用框架默认值(Gunicorn 2s / Uvicorn 5s / Node 5s),而 ingress 侧 upstream-keepalive-timeout 是 60s,竞态成立。修复见 7.4。
10d. 区分两类竞态的小技巧: 发布窗口的 502 与发布强同步、窗口内密集;keepalive 竞态的 502 全天随机、低峰期反而更容易出现(空闲连接在低峰更容易攒出来)、upstream_response_time 接近 0,且幂等请求常常一次重试就成功(表现为 upstream_addr 出现两个 IP、upstream_status 出现 502, 200)。
5.11 第十一步:修复方案与实施顺序
两个故障的最终修复清单:
故障一(发布窗口 502)修复组合:
- 业务 Deployment 加 preStop sleep(给 Endpoint 摘除传播留时间)+ 适当 grace 期(7.5 节 YAML);
- readiness 从 tcpSocket 改为 HTTP 探针打真实健康端点,Spring Boot 开启优雅停机(
server.shutdown=graceful);
- ingress-nginx 侧维持默认重试(幂等 GET 类接口靠重试自愈)。注意:
/api/callback 是 POST,重试有重复回调风险——这正是 3.6 节说的风险。实施前确认了支付回调按业务单号做了幂等去重,因此保留默认重试;若业务不具备幂等,需对该 Ingress 显式收紧 proxy-next-upstream,宁可牺牲自愈能力。
故障二(导出接口 504)修复组合:
- 仅对报表 Ingress 加 annotation 把
proxy-read-timeout/proxy-send-timeout 提到 120s(7.1 节),不动全局;
- 应用侧异步化导出(提交任务 + 轮询取结果),从根上消灭长同步请求——这是长期方案,调参只是止血;
- 给导出接口在应用层配独立线程池隔离,防止慢导出拖垮快接口;
- 若报表域名走独立 LB,LB 空闲超时同步 ≥ 120s 对齐(7.6 节),否则 LB 层先掐连接。
实施顺序纪律:先上 preStop/探针(纯增强、无副作用),观察一个发布窗口;再动超时参数;一次一个变量,每步都有第 11 节的验证和第 12 节的回滚预案兜底。
补一个与前两个形态不同的 502,因为它容易被误判成网络问题。
现象: 登录接口在灰度发布后 30% 的用户随机 502,重试一次就好。没有规律、和发布动作的时间关系只有"发布之后才出现"。
排查链: access log 显示 502、$upstream_status 为 -、$upstream_response_time 正常(1~2 秒,不是超时也不是瞬时拒绝);error.log 给出直接证据:
upstream sent too big header while reading response header from upstream
根因: nginx 用 proxy_buffer_size(ingress-nginx ConfigMap 键 proxy-buffer-size,默认 4k)缓冲上游响应头。这个服务灰度后在 Set-Cookie 里塞了一个完整 JWT,加上原有 cookie,响应头超过 4k。"只有一部分用户"是因为只有登录成功的请求才带这个大 cookie——概率问题伪装成了网络抖动。
修复: 全局 ConfigMap 调整(该参数没有 per-Ingress annotation,属于必须动全局的场景,走 12 节的备份与灰度流程):
data:
proxy-buffer-size: "16k"
# 该键为全局参数,修改影响所有 Ingress;buffer 调大会按连接数增加内存占用,
# 16k 对绝大多数服务足够,不要一步给到 128k
复盘要点: 与业务方约定响应头大小上限(比如 8k)写进接口规范——运维侧的 buffer 只是护栏,不是让应用无限塞 cookie 的许可。护栏调大后加告警:upstream sent too big header 在 error.log 出现一次就告警,防止下一次塞了个 20k 的进来。
5.13 三个案例的证据链汇总
| 维度 |
案例一:发布窗口 502 |
案例二:导出接口 504 |
案例三:响应头过大 502 |
| 现象形态 |
与发布同步的窗口性 |
单接口稳定复现 |
部分用户随机、重试可成功 |
| upstream_status |
-(已发出后失败的形态见 5.2 提醒) |
- |
- |
| upstream_response_time |
≈0(瞬时失败) |
≈60s 整数 |
正常耗时 |
| error.log 失败串 |
prematurely closed |
upstream timed out (110) |
sent too big header |
| 决定性证据 |
Killing 事件 × 502 窗口 × 抓包 FIN 时序 |
p99 74s vs 超时 60s |
大 cookie 出现时与 502 概率吻合 |
| 根因层 |
Pod 终止流程 |
参数规划缺失 |
应用响应头超代理缓冲 |
| 修复 |
preStop+探针+优雅停机 |
per-Ingress 调参+接口异步化 |
proxy-buffer-size+规范约束 |
| 验证 |
发布期探测非 200 归零 |
>60s 请求返回 200 |
灰度期 error.log 该串归零 |
三个案例的共同点:没有任何单一指标能独立定根因,都是"时间线 + 日志串 + 数值对账"三类证据互相咬合。这也是 4.2 证据链纪律存在的意义。
6. 常用命令速查
按排查层次整理,每条命令给出用途。命令中的标签选择器、名称都按 4.4 节假设,落地前替换。
6.1 kubectl 层
| 命令 |
用途 |
kubectl -n ingress-nginx logs -l app.kubernetes.io/name=ingress-nginx --since=1h | grep '" 50' |
抓 ingress 层的 5xx 访问日志 |
kubectl -n ingress-nginx logs <pod> --tail=200 | grep error |
看 nginx error.log 失败原因原文 |
kubectl -n ingress-nginx exec deploy/ingress-nginx-controller -- sh -c "nginx -T 2>/dev/null | grep -n 'proxy_read_timeout'" |
查配置里生效的读超时 |
kubectl -n pay get endpointslices -l kubernetes.io/service-name=<svc> |
看后端 Endpoint 列表 |
kubectl -n pay get pods -o wide |
Pod 状态、IP、所在节点 |
kubectl -n pay describe pod <pod> | grep -A5 Last\ State |
容器上次退出原因 |
kubectl -n pay get events --sort-by=.lastTimestamp | tail -30 |
最近的调度/探针/终止事件 |
kubectl -n pay rollout history deploy/<name> |
发布历史,对时间线 |
kubectl -n pay rollout undo deploy/<name> |
回滚 Deployment(见 12 节) |
kubectl -n ingress-nginx rollout restart deploy/ingress-nginx-controller |
重启 ingress(高危,见 10 节) |
kubectl -n ingress-nginx get cm ingress-nginx-controller -o yaml > cm-backup.yaml |
ConfigMap 变更前备份 |
6.2 ingress-nginx 配置验证层
# 导出完整生效配置(-T 会把所有 include 展开打到 stdout)
kubectl -n ingress-nginx exec deploy/ingress-nginx-controller -- \
sh -c "nginx -T 2>/dev/null" > /tmp/nginx-running.conf
# 查某个域名对应 server 块的超时(先定位 server 块再向下找)
grep -n 'server_name pay.example.com' -A 80 /tmp/nginx-running.conf | grep -E 'proxy_.*timeout|keepalive'
# 查 upstream 块里的后端 Pod 列表(确认 ingress 看到的后端是谁)
grep -n 'upstream' -A 12 /tmp/nginx-running.conf | grep -B1 -A8 'pay-payment-callback'
# 查 upstream keepalive 池配置
grep -n 'keepalive' /tmp/nginx-running.conf
6.3 节点与网络层
sysctl net.netfilter.nf_conntrack_count net.netfilter.nf_conntrack_max # conntrack 水位
dmesg -T | grep -i conntrack | tail # conntrack 丢弃证据
netstat -s | grep -iE 'listen|overflow' # accept 队列溢出
ss -s # 全机 socket 概览(TIME_WAIT/CLOSE_WAIT 数量)
ss -ltnp | grep <port> # 监听队列水位
ip -s link show <网卡> # 网卡丢包/错误计数
mtr -rwzbc 200 <目标IP> # 链路丢包与延迟分布
timeout 60 tcpdump -i any -nn -c 300 'tcp port <port> and tcp[tcpflags] & (tcp-fin|tcp-rst) != 0' # 抓连接关闭方
6.4 应用与复现层
# 单请求分段计时
curl -s -o /dev/null -w 'dns=%{time_namelookup} connect=%{time_connect} \
ttfb=%{time_starttransfer} total=%{time_total} code=%{http_code}\n' \
--max-time 70 https://pay.example.com/api/callback
# 并发压测复现(hey 是常用的轻量工具,私有仓库镜像或二进制均可)
hey -z 60s -c 20 -m POST -H 'Host: pay.example.com' \
-d '{}' https://<ingress-lb-ip>/api/callback
# 从集群内部压(绕开 LB,专门验证 ingress→pod 段)
kubectl -n pay run hey-tmp --rm -i --restart=Never --image=<私有仓库>/hey:latest -- \
-z 30s -c 10 http://payment-callback:8080/api/health
time_starttransfer(首字节时间)是判断"应用慢还是网络慢"的关键:connect 快、ttfb 慢 = 应用处理慢;connect 就慢 = 网络/连接层慢。
6.5 排查 502/504 常用 annotation 速查
以 nginx.ingress.kubernetes.io/ 为前缀(下表省略)。全部来自 ingress-nginx 官方 annotation 集合,改完用 6.2 节 nginx -T 验证落到了对应 server 块:
| annotation |
默认值 |
与 502/504 的关系 |
proxy-connect-timeout |
5 |
建连超时;调大基本无意义,连不上就是连不上 |
proxy-read-timeout |
60 |
504 第一嫌疑人;长接口在此单独放大 |
proxy-send-timeout |
60 |
大上传/大响应回写方向的超时 |
proxy-body-size |
1m |
上传超限返回 413 不是 502,但常被一起误配,此处备查 |
proxy-next-upstream |
error timeout |
收紧或放宽失败重试的条件 |
proxy-next-upstream-tries |
3 |
尝试后端的次数上限;设 1 表示仅尝试一个后端 |
proxy-next-upstream-timeout |
0(不限额) |
换后端重试的总时间上限,秒 |
proxy-buffering |
off |
流式下载仍需检查实际生成配置,见 7.7 |
proxy-request-buffering |
on |
大文件流式上传场景可关,注意与重试语义的交互 |
注意:annotation 的可用性跟随 ingress-nginx 版本,动手前查你所用版本的官方 annotations 参考页,或直接在测试环境 apply 后用 nginx -T 确认已落到 server 块——落了才算存在,文档和本文都不算数。
6.6 超时阶梯自查脚本(只读)
新接手集群或季度巡检时跑一遍,把散落在 ConfigMap、annotation、nginx -T 里的超时值拉平到一张视图里。脚本为示例思路,只读、不修改任何对象:
#!/usr/bin/env bash
# check-timeout-ladder.sh —— 汇总超时/keepalive 阶梯现状,供人工对照 3.7 规则
# 依赖:kubectl(只读权限即可);用法:./check-timeout-ladder.sh <业务ns> [ingress-ns]
set -euo pipefail
NS="${1:?usage: check-timeout-ladder.sh <业务namespace> [ingress-namespace]}"
ING_NS="${2:-ingress-nginx}"
echo "== 1. ingress 全局 ConfigMap 关键参数 =="
# helm 部署的 ConfigMap 一般带 app.kubernetes.io/name 标签,标签不符时先 kubectl get cm -n <ns> 找名字
kubectl -n "$ING_NS" get cm -l app.kubernetes.io/name=ingress-nginx \
-o jsonpath='{range .items}{.metadata.name}{"\n"}{end}' |
while read -r cm; do
[ -n "$cm" ] || continue
echo "-- ConfigMap: ${ING_NS}/${cm}"
cm_yaml=$(kubectl -n "$ING_NS" get cm "$cm" -o yaml)
for key in proxy-connect-timeout proxy-read-timeout proxy-send-timeout \
upstream-keepalive-connections upstream-keepalive-timeout; do
val=$(printf '%s\n' "$cm_yaml" | awk -v k="${key}:" '$1 == k {print $2; exit}')
printf ' %-32s %s\n' "$key" "${val:-<未设置,用默认>}"
done
done
echo "== 2. 生成配置里的实际生效值(唯一可信来源) =="
kubectl -n "$ING_NS" exec deploy/ingress-nginx-controller -- \
sh -c "nginx -T 2>/dev/null" > /tmp/nginx-running.conf
grep -oE 'proxy_(connect|read|send)_timeout [^;]+;' /tmp/nginx-running.conf | sort | uniq -c
grep -oE 'keepalive(_requests|_timeout)? [^;]*;' /tmp/nginx-running.conf | sort | uniq -c
echo "== 3. 业务命名空间内各 Ingress 的超时 annotation =="
kubectl -n "$NS" get ingress -o jsonpath='{range .items}{.metadata.name}{" read="}' \
'{.metadata.annotations.nginx\.ingress\.kubernetes\.io/proxy-read-timeout}{"\n"}{end}' \
| sed 's/read=$/read=<默认60>/'
echo "== 4. 人工核对项(脚本不猜应用参数) =="
cat <<'CHECK'
- 后端应用 keep-alive > ingress upstream-keepalive-timeout ? (3.7)
- ingress 客户端 keepalive > LB 空闲超时 ? (7.6)
- 长任务接口 read-timeout > 接口 p99 + 余量 ? (5.9)
- preStop sleep + graceful 预算 < terminationGracePeriod ? (7.5)
CHECK
脚本输出只回答"现状是什么","对不对"仍然要人按 3.7 规则判断。set -euo pipefail 下个别 jsonpath 取不到值会直接退出,所以取值处都做了 || true 兜底——生产脚本宁可打印 <未设置> 也不要中途死掉给出半张视图。
7. 超时参数配置示例
配置原则先给出三条:空闲保活时间沿链路向内递增(3.7 节规则,谁被复用谁先关);默认值不动,差异靠 per-Ingress annotation 表达;每一次调大超时之前,先算连接与线程的容量账(第 13 节)。
7.1 Per-Ingress annotation(首选方式)
只影响单个 Ingress,是调整超时最安全的方式。文件就是业务自己的 Ingress YAML:
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: report-ingress
namespace: report
annotations:
# 单位是秒,不要写 "120s",写了不会报错但可能不如预期
nginx.ingress.kubernetes.io/proxy-connect-timeout: "5"
nginx.ingress.kubernetes.io/proxy-read-timeout: "120"
nginx.ingress.kubernetes.io/proxy-send-timeout: "120"
# 长任务接口建议收紧重试:宁可不重试,也不要放大等待和重复请求
nginx.ingress.kubernetes.io/proxy-next-upstream: "off"
nginx.ingress.kubernetes.io/proxy-next-upstream-tries: "1"
# proxy-body-size 只限制请求体大小,与导出响应大小无关;有大文件上传时另行配置
spec:
ingressClassName: nginx
rules:
- host: report.example.com
http:
paths:
- path: /report
pathType: Prefix
backend:
service:
name: report-svc
port:
number: 8080
生效方式:kubectl apply -f report-ingress.yaml 后 controller 自动 reload,无需重启。注意 reload 本身是平滑的(worker 逐个换代),但高频改 annotation 会触发 reload 风暴,见 10 节。
验证生效:按 6.2 节 nginx -T 查这个 server 块的超时是否为 120。
7.2 全局 ConfigMap(影响所有 Ingress,慎用)
只有当全集群都需要新默认值时才动这里,文件路径:ingress-nginx 命名空间下名为 ingress-nginx-controller 的 ConfigMap(helm 部署一般同名,以 kubectl get cm -n ingress-nginx 为准):
apiVersion: v1
kind: ConfigMap
metadata:
name: ingress-nginx-controller
namespace: ingress-nginx
data:
proxy-connect-timeout: "5"
proxy-read-timeout: "75" # 全局只微调,长尾接口用 annotation 解决
proxy-send-timeout: "75"
# upstream keepalive 池:320/worker 是默认值,高 QPS 集群可适当调大
upstream-keepalive-connections: "512"
# 老版本(v0.24 之前)没有 upstream-keepalive-connections,
# 只有 boolean 的 upstream-keepalive: "false" 表示关闭,注意版本差异
upstream-keepalive-timeout: "60" # 整数秒;应与后端空闲超时实测配合,见 7.4
log-format-upstream: >-
$remote_addr - $remote_user [$time_local] "$request" $status $body_bytes_sent
"$http_referer" "$http_user_agent" $request_length $request_time
[$proxy_upstream_name] [$proxy_alternative_upstream_name] $upstream_addr
$upstream_response_length $upstream_response_time $upstream_status $req_id
生效方式:kubectl apply 后 controller 监听 ConfigMap 自动 reload;个别键需要 controller 版本支持,apply 后查 controller 日志有没有 Configuration complete; ready for start up 且无 Error。回滚:kubectl apply -f cm-backup.yaml(12 节)。
7.3 核对实际生效值(不要相信记忆)
三层来源(annotation > ConfigMap 全局 > nginx 编译默认)叠加后的真实值,唯一可信的检查方法是 nginx -T:
kubectl -n ingress-nginx exec deploy/ingress-nginx-controller -- \
sh -c "nginx -T 2>/dev/null" | grep -E 'proxy_(connect|read|send)_timeout|keepalive' | sort | uniq -c
预期输出能看到若干 proxy_read_timeout 60s; 与个别 120s; 并存,并存的那些就是 annotation 覆盖过的 Ingress。
7.4 后端应用侧联动配置(keepalive 阶梯)
核心规则来自 3.7:这一段连接上,ingress 是复用方,后端应用是被复用方,所以后端应用的空闲 keep-alive 关闭时间必须大于 ingress 侧 upstream-keepalive-timeout(默认 60s)。方向对了,连接永远由 nginx 先到期、先从池里优雅撤走,后端不会抢先把 nginx 手里的连接关掉。Gunicorn 默认 2s、Uvicorn/Node 默认 5s 之所以是经典事故源,就是因为它们的默认值远小于 60s,属于把方向配反。
Spring Boot(application.yaml;server.tomcat.keep-alive-timeout 从 Boot 2.3 才有独立配置项,更老版本 keepAliveTimeout 跟随 connectionTimeout,注意版本差异):
server:
shutdown: graceful # 优雅停机:先拒新请求、等存量请求处理完
tomcat:
keep-alive-timeout: 75s # 必须 > ingress upstream-keepalive-timeout(默认60s),让 nginx 先关
max-keep-alive-requests: 11000 # 略大于 ingress 侧 upstream-keepalive-requests(10000),
# 单连接服务满 N 个请求时由 nginx 先到期关闭,时序同样归代理管
spring:
lifecycle:
timeout-per-shutdown-phase: 20s # graceful shutdown 预算,须小于 terminationGracePeriodSeconds
如果后端是共享基础设施、keep-alive 改不动(比如统一镜像里的 Gunicorn),把这条服务标成"已知竞态源",用两条兜底:幂等接口依赖 ingress 自动重试自愈;非幂等接口的 Ingress 加 502 突增告警,别让它无声吃掉业务请求。不存在"后端比代理先关也能把竞态压没"的配置,只有窗口大小的区别。
Gunicorn:
# 默认 --keep-alive 2 太短,调到与 ingress 池空闲错开的量级
gunicorn -w 4 -b 0.0.0.0:8080 --keep-alive 75 app:app
Uvicorn:
uvicorn app:app --host 0.0.0.0 --port 8080 --timeout-keep-alive 75
Node.js(原生 http.Server,Node 18+ 默认 keepAliveTimeout=5s、headersTimeout=60s):
const server = http.createServer(app);
server.keepAliveTimeout = 75000; // 75s,与 ingress 池空闲(60s)错开
server.headersTimeout = 76000; // 必须大于 keepAliveTimeout,否则 Node 自身会报 408/断开
7.5 滚动更新零 502 组合(readiness + preStop + grace)
直接可用的 Deployment 关键片段:
spec:
terminationGracePeriodSeconds: 60
containers:
- name: payment-callback
image: registry.example.com/pay/payment-callback:1.8.2
ports:
- containerPort: 8080
readinessProbe:
httpGet:
path: /actuator/health/readiness # Spring Boot 的 readiness 分组,停机阶段会先转 DOWN
port: 8080
initialDelaySeconds: 10
periodSeconds: 5
failureThreshold: 3
lifecycle:
preStop:
exec:
# sleep 不是魔法数字:覆盖 "Endpoint 摘除传播到 ingress Lua 表" 的时间,
# 大集群/高负载下建议 10~15s,用 5.10 的抓包实测自己的传播耗时
command: ["sh", "-c", "sleep 10"]
要点:
- preStop 的 sleep 与 readiness 无关,它赌的是传播延迟;sleep 结束后 SIGTERM 到达,Spring Boot
shutdown=graceful 开始排空存量请求;
terminationGracePeriodSeconds(60)必须大于 sleep(10) + graceful 预算(20) + 余量,否则 grace 耗尽被 SIGKILL,前功尽弃;
- Spring Boot 需要引入
spring-boot-starter-actuator 才有 /actuator/health/readiness 分组端点。
7.6 云 LB 层对齐
LB→ingress 这一跳,LB 是连接复用方、ingress 是被复用方,套 3.7 规则:ingress 对客户端侧的空闲保活(keepalive-timeout)必须大于 LB 的空闲连接超时,否则 ingress 先关、LB 拿死连接发新请求,表现为 LB 侧 5xx 或 connection reset(不同 LB 报错样式不同,有的直接计 502)。
- 阿里云 CLB 四层 TCP 监听:空闲连接超时默认 900s,此时要把 ingress 客户端侧
keepalive-timeout 控制在远小于 900s(默认 75s 即安全),别让 ingress 比 CLB 先关;
- AWS NLB 空闲超时默认 350s,同理默认即安全;
- AWS ALB 是七层复用,idle timeout 默认 60s < ingress 默认 keepalive 75s,默认即安全。需要重新核对的变更:调整 ingress
keepalive-timeout 或 ALB idle timeout,都会反转方向制造竞态;若业务要拉长 ALB idle timeout,同步把 ingress keepalive-timeout 调到更大并重启/重载生效;
- ALB 自身返回 502/504 时(响应头不是 nginx 样式、body 是 ALB 的错误页),先按 3.4 分清产生层,再查 ALB→ingress 段的健康检查与超时阶梯。
7.7 长连接类接口的超时配置(SSE / WebSocket / 长轮询)
这类接口的 504/断流问题在 AI 推理、实时大屏场景越来越常见,机制各不相同,分开配。
SSE / 流式下载。 核心是 3.3 节说的"read timeout 是两次读的间隔":
- 最省事的方案是应用端保证心跳间隔小于
proxy-read-timeout(比如每 30 秒一条注释行 : keep-alive\n\n),超时参数完全不用动;
- 做不到心跳的(比如模型生成中途可能静默 2 分钟),给该接口单独放大 read timeout 并关闭响应缓冲,否则 nginx 攒着 buffer 不往外吐,客户端反而更收不到心跳:
annotations:
nginx.ingress.kubernetes.io/proxy-read-timeout: "180"
nginx.ingress.kubernetes.io/proxy-buffering: "off"
改完验证方式:用 curl -N(禁用本地缓冲)连接 SSE 端点,观察数据是否逐条实时到达且总时长超过 60 秒不再断流。
WebSocket。 ingress-nginx 对 WebSocket 走 Upgrade 直通,HTTP 层的 proxy_read_timeout 作用于升级后的隧道空闲间隔:超过 read timeout 没有任何帧往来就会被掐。所以 WebSocket 应用必须实现心跳帧(间隔小于 ingress 侧 read timeout,也小于 LB 空闲超时),这是应用协议层的事,代理参数替代不了。典型故障是"凌晨低峰 WebSocket 全部断开"——低峰没人发消息,空闲间隔超过了链路里最小的那个定时器。链路最小空闲值取三者:ingress read timeout、ingress 客户端侧 keepalive、LB 空闲超时,应用心跳间隔必须小于其中最小值并留余量。
长轮询。 长轮询是"服务端 hold 住请求等有数据",hold 时长必须显式小于 proxy-read-timeout:hold 55 秒、超时 60 秒是合理设计;hold 到 60 秒以上就是拿 504 当业务信号用,迟早出事。检查方法:对比业务代码里的 hold 时长与 6.2 节查到的生效超时值。
gRPC。 后端协议标 nginx.ingress.kubernetes.io/backend-protocol: "GRPC" 后,ingress 生成 grpc_pass,超时指令与 HTTP 代理不完全同名、且随 ingress-nginx 版本变化,不要背参数名,直接 nginx -T 看生成的 server 块里 grpc 相关超时指令实际取了什么值,再决定调哪个。gRPC 的 deadline 传播在应用层,运维参数只是天花板。
8. 日志与指标观察方法
8.1 access log 字段判读
ingress-nginx 默认 log format 的字段顺序与含义(nginx -T | grep log_format 核对,不同版本 $proxy_alternative_upstream_name 等字段有增减):
$remote_addr - $remote_user [$time_local] "$request" $status $body_bytes_sent
"$http_referer" "$http_user_agent" $request_length $request_time
[$proxy_upstream_name] [$proxy_alternative_upstream_name] $upstream_addr
$upstream_response_length $upstream_response_time $upstream_status $req_id
排查 502/504 重点盯五个:
| 字段 |
含义 |
判读 |
$status |
最终返回客户端的状态码 |
定性 502/504 |
$request_time |
从收到客户端首字节到发出末字节的总耗时 |
≈ 配置超时值 → 超时掐断 |
$upstream_addr |
实际打到的后端 IP:port(重试会逗号分隔多个) |
出现两个 IP → 发生过重试 |
$upstream_response_time |
上游段耗时(多个后端时逗号分隔各自耗时) |
0.000 → 连接即失败;≈超时值 → 读超时 |
$upstream_status |
上游尝试关联的状态 |
结合 error.log 与后端日志判断产生层(3.4) |
8.2 error log 级别与取证
默认级别 notice(以 controller 启动参数为准)。排查疑难竞态时可临时调到 info 甚至 debug:修改 controller Deployment 的 args 增加/修改 --error-log-level=info,rollout 后观察,取证结束必须调回——info 及以上级别在高流量下日志量增长一到两个数量级,debug 级足以把节点磁盘和 CPU 打满,本身就是故障源。修改 args 属于修改生产配置,走变更 + 灰度(先改一个副本观察,见 13 节)。
8.3 Prometheus 指标与告警建议
ingress-nginx 在 10254 端口暴露 metrics(是否被 ServiceMonitor/PodMonitor 抓取以实际集群为准,以下指标名以你环境里 /metrics 实际输出为准,不同版本有增减):
常用指标:
| 指标 |
类型 |
用途 |
nginx_ingress_controller_requests |
Counter,带 status/class 标签 |
5xx 率计算的核心 |
nginx_ingress_controller_request_duration_seconds |
Histogram |
延迟分布、p99 |
nginx_ingress_controller_nginx_process_connections |
Gauge(state 标签) |
active/waiting 连接数 |
nginx_ingress_controller_config_last_reload_successful |
Gauge |
配置 reload 是否成功,=0 要告警 |
nginx_ingress_controller_success |
Counter |
reload 成功次数 |
NGINXWorkQueue |
Gauge/Counter(版本有差异) |
配置变更队列积压,reload 风暴前兆 |
5xx 比例告警示例:
# PrometheusRule 片段:ingress 5xx 占比,阈值须按业务基线调,不是绝对标准
- alert: IngressHigh5xxRatio
expr: |
sum(rate(nginx_ingress_controller_requests{status=~"5.."}[5m]))
/
sum(rate(nginx_ingress_controller_requests[5m]))
> 0.01
for: 5m
labels:
severity: warning
annotations:
summary: "Ingress 5xx 比例超过 1%(阈值按业务基线调整)"
单服务维度的 502 突增(发布窗口的 502 常常被全集群平均值稀释,务必按 service 拆):
sum by (service, namespace) (rate(nginx_ingress_controller_requests{status="502"}[5m])) > 0.5
Grafana 面板建议:5xx 率(按 service 堆叠)、p50/p99 延迟、reload 成功状态、workqueue 积压。面板字段依赖 exporter 实际指标与标签,导入的社区面板需要按自己环境调整。
8.4 发布窗口观察脚本
发布期间开一个终端跑(示例思路,路径按环境改):
#!/usr/bin/env bash
# watch-502.sh 发布窗口内每秒探测一次目标接口并记录状态码
# 用法:./watch-502.sh https://pay.example.com/api/health
set -u
URL="${1:?usage: watch-502.sh <url>}"
while true; do
# curl 失败时 -w 本身会输出 000,不要再叠加 echo,否则拼接出 000000
code=$(curl -s -o /dev/null -w '%{http_code}' --max-time 10 "$URL") || code="000"
ts=$(date '+%F %T')
echo "$ts $code"
sleep 1
done | tee -a "/tmp/watch-502-$(date +%F).log"
发布结束后 awk '$2!=200' 日志文件 | wc -l 统计非 200 次数,就是这套组合拳的"战损比"。
8.5 抓包判读:连接是谁关的、什么时候关的
零星 502 的最后一步实证往往落在 tcpdump 上。在业务 Pod 所在节点抓 ingress→Pod 方向(5.8 的过滤器),按下表判读:
| 抓包形态 |
时序特征 |
结论 |
| 空闲 N 秒后 Pod 侧发 FIN,随后 ingress 在同一四元组上发数据收到 RST |
FIN 前连接恰好空闲了 N 秒 |
keepalive 竞态实锤,N 就是后端应用的 keep-alive 配置值,对照 3.7 表 |
| Pod 收到 SYN 直接回 RST(无 SYN-ACK) |
与 Pod 终止/进程退出时刻同步 |
进程已退出但 Endpoint 未摘除,走发布竞态线(5.10) |
| ingress 发了请求数据,Pod 回 FIN 且无响应 |
请求发出后约 0 RTT |
应用 worker 在复用连接上处理失败即关,查应用日志该请求 |
| 三次握手都慢(SYN 重传) |
高峰期集中出现 |
accept 队列/网络拥塞,走 5.8b/8c |
| 全程无异常包,但 ingress 记 504 |
请求发出后 Pod 静默超过 read timeout |
应用假死或排队,查 GC/线程池,抓包证明网络没丢 |
两个操作提醒:抓包点选错等于白抓——CNI overlay 下节点物理网卡上看到的源 IP 可能与 Pod 视角不同,用 -i any 或直接在 Pod 网络命名空间里 nsenter -t <pid> -n tcpdump ...(<pid> 取容器内 1 号进程宿主 PID,来自 crictl inspect);PCAP 文件含业务报文,取证结束当天删除,不要长期留在节点。
8.6 三条典型日志行的逐字段判读
拿三行真实形态的日志做判读示范(默认 log format,字段含义见 8.1 表):
样本一:正常请求。
10.244.1.3 - - [18/Sep/2026:09:12:01 +0800] "GET /api/health HTTP/1.1" 200 15 "-" "curl/8.5.0" 84 0.004 [pay-payment-callback-8080] [] 10.244.7.19:8080 15 0.003 200 3c1f
读法:request_time 0.004 秒,其中上游 0.003 秒,单后端、状态 200。基线长这样。
样本二:读超时 504。
10.244.1.3 - - [18/Sep/2026:14:22:07 +0800] "GET /report/export HTTP/1.1" 504 569 "-" "Mozilla/5.0" 62 60.001 [report-report-svc-8080] [] 10.244.9.31:8080 0 60.001 - 8a2e
读法:request_time = upstream_response_time = 60.001,上游状态 -(没等到响应行)——nginx 在第 60 秒掐断,504 由 ingress 产生,直奔 proxy-read-timeout 与接口耗时对账。60 秒这个"整"的数字本身就是超时掐断的签名:真实业务耗时不会这么巧。
样本三:重试救回来的 502。
10.244.1.9 - - [18/Sep/2026:10:31:12 +0800] "GET /api/query HTTP/1.1" 200 2318 "-" "okhttp/4.9.3" 90 0.021 [pay-payment-callback-8080] [] 10.244.7.19:8080, 10.244.8.6:8080 2318, 2318 0.000, 0.018 502, 200 7d44
读法:upstream_addr 两个地址、响应时间 0.000, 0.018、状态 502, 200——第一跳 0 毫秒即失败(连接类错误),重试到第二跳成功,用户拿到 200。这类记录是"被掩盖的 502",用户无感但每一跳都在 3.6/4.3 讨论的风险账上。GET 被重试属于预期行为;如果同样形态出现在 POST 上,立刻查该接口的幂等性。
9. 排查路径速查
把全文压缩成一页可照着走的决策路径:
用户报障
├─ 1. ingress access log 确认真实 status 与时间分布(5.1)
│ ├─ 与发布/重启时间同步 → 发布竞态线(→ 9a)
│ ├─ 全天随机零星 → keepalive/网络线(→ 9b)
│ └─ 特定接口稳定复现 → 耗时/参数线(→ 9c)
├─ 2. upstream_status 关联 error.log 和后端日志(3.4/5.2)
│ └─ 透传 → 进业务容器查容器内网关,本文路径终止
├─ 3. error.log 失败串对号入座(5.3)
│ ├─ connection refused / reset / prematurely closed → 生命周期与网络
│ ├─ upstream timed out(110) → 耗时线
│ └─ no live upstreams → 先查为什么全部被熔断(看它前面的批量失败串)
├─ 4. EndpointSlice/readiness/Pod lastState(5.4/5.5)
├─ 5. 分层 curl 切分网络段与应用段(5.6)
└─ 6. 修复→验证(11)→回滚预案(12)→复盘(14)
9a 发布竞态线:preStop 缺失?grace 不足?readiness 只探 TCP?→ 5.10 + 7.5
9b keepalive/网络线:后端框架空闲 keep-alive 默认值(3.7 表)→ 抓包看谁先 FIN
│ → conntrack/accept 队列/丢包(5.8)
9c 耗时/参数线:p99 与超时预算对账(5.9)→ 接口异步化优先,per-Ingress 调参止血(7.1)
9.1 故障现场 Checklist(处理中逐条打勾)
接障前 5 分钟(只取证,不动手):
- [ ] 从 access log 确认真实状态码与起止时间(5.1),截图进工单
- [ ] 确认 5xx 时间分布形态:持续 / 窗口性 / 零星(5.1 的三种聚合)
- [ ] 保存最近 100 条失败请求的原始日志行(5.2)
- [ ] 保存 error.log 最近 30 条失败串(5.3)
- [ ] 保存
kubectl -n <ns> get endpointslices 与失败后端 Pod 的状态(5.4/5.5)
- [ ] 保存
nginx -T 中目标 server 块的超时值(6.2)
- [ ] 检查变更事件:最近发布、ConfigMap 变更、节点维护、LB 变更(5.1)
定位阶段(每条结论至少两个证据):
- [ ] 结合 upstream_status、error.log 和后端日志确定错误产生层(3.4)
- [ ] error.log 失败串与 3.2 表匹配(5.3)
- [ ] 分层 curl 切分网络段/应用段(5.6)
- [ ] 502:Endpoint 时间线 × 发布时间线 × 错误时间线三线对齐(5.10)
- [ ] 504:接口 p99 × 生效超时值 × 应用内日志耗时对齐(5.9)
- [ ] 零星类:conntrack 水位、accept 队列、网卡计数(5.8)
- [ ] 抓包判定连接关闭方向(8.5)
修复阶段:
- [ ] 变更对象已备份(Ingress YAML / ConfigMap / sysctl 现值)
- [ ] 一次只改一个变量
- [ ] 回滚命令已写在工单里并可执行(12.1)
- [ ] 验证脚本已就位(11.2 / 8.4)
- [ ] 影响面已通知:哪些域名、哪些接口、预计时长
- [ ] 回滚决策线已同步值班群(12.2)
9.2 对外同步模板与升级时机
排查归排查,值班纪律不能停。故障群里的同步用固定三段式,每 15~30 分钟一条,哪怕内容是"无新增结论":
【影响】pay.example.com /api/callback 间歇 502,占比 0.1%,支付回调延迟,无资损(待业务确认)
【现状】已排除网络与容量问题;证据指向发布窗口竞态(Killing 事件与 502 窗口重合),
正在做抓包确认(预计 20 分钟出结论)
【下一步】确认后立即上 preStop 修复;回滚预案见工单 INC-2026-0918-01
三条升级线写进值班手册:影响资损类接口(支付/下单)且 30 分钟未定位 → 升级到 ingress 负责人;证据链出现互相矛盾(如日志与抓包结论冲突)→ 停止操作、拉会商;任何"要不要现在改全局 ConfigMap"的问题 → 默认不改,升级到变更评审。502/504 类故障的升级成本远低于误操作全局配置的成本。
10. 风险提醒
每一条都是生产里真出过事的类型:
-
全局 ConfigMap 修改影响整个集群所有 Ingress。 改前必须 kubectl -n ingress-nginx get cm ingress-nginx-controller -o yaml > cm-backup-$(date +%F).yaml;先在测试集群 apply,再灰度到生产;apply 后立即观察 8.3 的 reload 成功指标。一个写错的键可能让整次 reload 失败,全部新配置不生效(controller 保留旧配置继续服务,但你以为改了什么)。
-
调大超时是借时间不是借容量。 proxy-read-timeout 从 60s 调到 300s,意味着慢请求洪峰时 nginx 与后端各自要多背 5 倍时长的连接占用;后端 Tomcat 默认 maxThreads 200,慢请求堆积会先于 ingress 打满应用线程池,把"504 故障"放大成"503/全站不可用故障"。调参必须同时确认连接与线程容量(7.4 与 13 节)。
-
收紧/放开重试直接影响资金安全。 proxy-next-upstream 保持默认时,POST 在请求发送前后有不同的重试约束,上线前必须实测(方法见 11.3),而不是依赖本文或任何文档的默认值描述。资金类接口按业务幂等能力评估重试;需关闭换后端时可设 nginx.ingress.kubernetes.io/proxy-next-upstream: "off"。
-
重启 ingress-controller 是生产破坏性操作。 rollout restart 期间该 controller 的全部域名有秒级抖动,多副本 + PodDisruptionBudget 也未必无感。它几乎不能修复 502/504 的根因(根因在应用/网络/参数),不要用重启代替排查。确需重启(如内存泄漏缓解):多副本逐台、低峰窗口、盯 5xx 曲线。
-
删除类命令的 namespace 陷阱。 kubectl -n ingress-nginx delete pod ... 只会杀 ingress 自身造成秒级抖动;但手滑 -n <业务ns> 的批量 label 删除就是事故。所有 delete 前先 get 同样的选择器确认命中列表;label 选择器宁可写死具体 name 也不要图省事用宽 label。
-
nginx -s reload 风暴。 批量变更 Ingress(如 CI 一次性 apply 几十个)会触发密集 reload,reload 期间旧 worker 要等服务完存量长连接才退出,大量 reload 会累积 worker 与内存。批量变更尽量合并窗口、错峰。
-
改内核参数(conntrack 等)三件套:备份现值、按节点灰度、持久化文件先写好。 持久化文件写错会让节点重启后网络行为异常且不自愈,改完在 /etc/sysctl.d/ 里 sysctl --system 验证一遍。
-
tcpdump 与压测都是生产流量压力源。 tcpdump 必须带 timeout 和 -c 上限;hey/ab 压测只在预发或低峰做,压测目标里包含写接口时先确认幂等。
-
日志清理不等于删日志文件。 truncate access log 在容器化 stdout 采集架构下基本不该由你手工做(轮转由 kubelet/containerd 管),真要清理节点上 /var/log 的历史轮转文件,用 find ... -name '*.log.*' -mtime +7 先 ls 列清单再删,不要直接 -delete。
-
修改防火墙/安全组以放行健康检查为高频踩坑点。 LB 健康检查网段被安全组拦截会导致 LB 判定 ingress 节点全部不健康,表现为整站 5xx。变更前导出当前规则备份,先加规则观察、确认无误再删旧规则。
11. 验证方式
改完之后"看起来好了"不算数,三类验证各自回答一个问题。
11.1 配置生效验证(改对了吗)
# 1) 配置面:nginx -T 里找目标 server 块的超时值
kubectl -n ingress-nginx exec deploy/ingress-nginx-controller -- \
sh -c "nginx -T 2>/dev/null" | grep -A40 'server_name report.example.com' \
| grep proxy_read_timeout
# 预期:proxy_read_timeout 120s;
# 2) controller 日志确认 reload 成功
kubectl -n ingress-nginx logs -l app.kubernetes.io/name=ingress-nginx --since=5m \
| grep -E 'reload|Backend status' | tail
11.2 行为验证(有效了吗)
504 类: 用一个实测耗时超过旧超时(60s)但小于新超时(120s)的导出请求验证:
time curl -sk -o /dev/null -w '%{http_code}\n' --max-time 130 \
-H 'Host: report.example.com' https://<ingress-lb-ip>/report/export?month=2026-01
# 预期:返回 200,real 时间 ≈ 75s;旧行为是 60s 整返回 504
发布竞态类: 执行一次完整滚动发布,同时跑 8.4 的探测脚本,预期非 200 次数为 0:
kubectl -n pay rollout restart deploy/payment-callback
kubectl -n pay rollout status deploy/payment-callback --timeout=300s
# 对照 /tmp/watch-502-*.log 修复前后的非 200 计数
11.3 重试与幂等实测(POST 类必做)
测试环境构造"后端收到请求但不返回"的场景验证 POST 是否会被重试。示例思路(不是生产可照抄的脚本):用一个 sleep 端点,请求会先落到后端 A 并在 A 上留日志,再延迟 65s 返回:
# 1) 通过 ingress 发一次 POST(read timeout 60s,A 后端 65s 才回)
curl -sk -X POST -d '{"order":"test-001"}' --max-time 70 \
-H 'Host: pay.example.com' https://<ingress-lb-ip>/api/sleep-delay
# 2) 去后端看 /api/sleep-delay 被调用了几次、来自哪些 Pod
# 调用次数 == 1 → 该版本 ingress 对 POST 未重试;> 1 → 重试生效,业务必须幂等
这个实验同时验证了"超时后用户侧的表现"和"重试次数",比任何文档都可靠。
11.4 指标回归
修复上线后 30 分钟内,Prometheus 上确认:该 service 的 5xx rate 回落到基线、p99 无异常抬升、ingress config_last_reload_successful 保持 1、节点 conntrack 水位(如动过)稳定。
12. 回滚方案
原则:每个变更动作都有对应的反向动作,且反向动作经过演练。
12.1 回滚映射表
| 变更 |
回滚动作 |
耗时 |
验证点 |
| Ingress annotation |
kubectl apply -f <变更前备份的 ingress yaml>(改前 kubectl get ingress -o yaml > ingress-bak.yaml) |
秒级 |
6.2 节 nginx -T 确认值回退 |
| 全局 ConfigMap |
kubectl apply -f cm-backup.yaml |
秒级 |
reload 成功 + 抽查多个域名 5xx |
| Deployment(探针/preStop) |
kubectl -n pay rollout undo deploy/payment-callback |
1~3 分钟(滚动) |
rollout status 完成 + 探测脚本无非 200 |
| 内核参数 |
sysctl -w 改回备份文件中各键的备份值 |
秒级 |
conntrack 计数恢复预期、dmesg 无新增异常 |
| LB 空闲超时 |
控制台/API 改回原值(变更前截图留档) |
分钟级 |
LB 健康检查恢复、5xx 回落 |
| ingress controller args |
kubectl rollout undo controller 的 Deployment |
秒级~分钟级 |
5xx、reload 指标 |
12.2 回滚决策线
提前和值班群约定死,避免临场争论:
- 变更后 30 分钟内,任一维度 5xx 比例较变更前基线上涨 ≥ 0.5 个百分点(阈值按业务基线定)且与变更时间吻合 → 立即回滚,不现场定根因;
- 全局 ConfigMap 变更后
nginx_ingress_controller_config_last_reload_successful 出现 0 → 立即回滚(说明配置有键不被支持或语法错误);
- 单 Ingress annotation 变更引发非目标接口异常 → 说明改动波及了全局,先回滚再分析。
12.3 回滚后的动作
回滚不是终点:保留回滚前的现场日志与 nginx -T 输出,回到第 5 节重新走排查线。一个变更回滚两次仍不通,升级为"我对该链路模型理解有误",找更熟悉 ingress-nginx 内部机制的同事,而不是继续盲试参数。
12.4 回滚要演练,不要首战即实战
"回滚文件在 /root/ 下应该还在"不是预案。每季度演练一次,全部在测试集群走完:
- ConfigMap 回滚:改一个无害键 → 确认 reload 生效 → apply 备份 → 再确认回退;
- annotation 回滚:加一个测试 Ingress 的 read timeout → 回滚备份 YAML →
nginx -T 确认;
- Deployment 回滚:
rollout undo 后 rollout status 观察完整滚动;
- sysctl 回滚:改一台演练节点的 conntrack 上限 → 按备份文件恢复 →
sysctl 复核。
演练脚本与备份文件放同一套 Git 仓库加对象存储,备份命名统一为 <对象>-<日期>-<工单号>.yaml。凌晨三点故障时,你要做的是执行一条验证过一百次的命令,而不是现场回忆。
13. 生产环境注意事项
-
变更窗口与灰度顺序。 超时/重试类参数变更放低峰窗口;顺序:测试集群全量 → 生产非核心业务 Ingress → 观察 24h → 核心业务。全局 ConfigMap 变更只在维护窗口做,并通知所有业务方。
-
per-Ingress 优先。 集群共享物尽量少动。一个接口要 120s 就给那个 Ingress 加 annotation,不要让全集群陪它等 120s。
-
容量评审。 调大超时前回答三个数:该接口峰值 QPS × 新增平均占用时长 = 新增并发连接数;ingress 副本数 × 每 worker 连接上限(worker_connections)是否覆盖;后端 maxThreads/worker 数是否覆盖。三个数算不出来,就不该调。
-
告警先行。 任何超时参数调大,等于把故障从"60 秒 504"改造成"120 秒 504",检测窗口同步拉长,对应接口的超时告警阈值要按新预算重新设计,否则监控会"瞎"。
-
文档化参数阶梯表。 集群维护一张"LB 空闲超时 / ingress upstream-keepalive-timeout / 各应用框架 keep-alive 默认值"的对照表并随版本更新,3.7 节那张表就是模板。90% 的 keepalive 竞态 502 源自这张表过期。
-
定期演练。 每季度在测试集群做一次故障演练:杀一个后端 Pod 同时打流量,看 502 数量是否在目标(0)以内;压测打满 accept 队列看告警是否在 5 分钟内触达。演练脚本进 Git,别留在个人终端。
-
镜像升级看 changelog。 ingress-nginx 的 release notes 里对 keepalive 行为、重试策略、默认值的变更都有明确条目,升级 controller 版本前后各跑一遍 11.2/11.3 的验证,把它当成一次配置变更来管理。
-
权限与审计。 ConfigMap、Deployment 的修改权限收敛到运维/平台组,开启审计日志(API audit);业务方只允许改自己 Ingress 的 annotation。
-
关注 ingress controller 自身的资源与探针。 controller 在 reload 和 TLS 握手密集时 CPU 会尖峰,requests/limits 压得过紧会让 reload 变慢、liveness 探针超时导致 controller 被反复重启,故障从业务转移到入口本身。controller 的 CPU、内存、reload 耗时要有自己的告警基线,liveness 的超时与失败阈值要给 reload 留余量。
-
域名切换/DNS 缓存会制造"修好了但用户还在报错"。 LB 或 ingress IP 变更后,客户端与各级 DNS 的旧解析可能持续数分钟到一个 TTL。判定"修复是否生效"优先用直连 IP + Host 头验证(5.6 第 3 层),而不是等用户口径恢复。
14. 复盘总结
14.1 故障复盘模板(两个故障的复盘归档)
故障编号:INC-2026-0918-01
级别:P3(零星数据延迟,无资损)
现象:pay.example.com /api/callback 每日三个窗口零星 502,占比 <0.1%
时间线:
09:05 监控告警 5xx 比例超阈值
09:20 按 5.1~5.3 定位到 error.log prematurely closed,与发布窗口重合
09:40 抓包实锤:旧 Pod 先发 FIN,ingress 复用死连接被 RST
10:10 修复:preStop sleep 10s + HTTP readiness + graceful shutdown
11:30 灰度发布验证,非 200 计数 0
根因:滚动更新时 Pod 终止与 Endpoint 摘除传播存在竞态,ingress 在空窗内
继续向已终止 Pod 发请求。
证据链:发布事件时间线 + error.log 失败串 + tcpdump FIN/RST 时序 + 修复后探测归零。
行动项:1) 全集群 Deployment 补齐 preStop(已完成)
2) readiness 禁用裸 tcpSocket 探端口的模式推广替换(进行中)
3) 发布平台内置"发布期探测脚本"(已排期)
14.2 全文知识清单
读完这篇文章,你应该能不看资料回答以下问题:
- 502 和 504 在 nginx 语义上的分界是什么?各自 error.log 里的标志性字符串是什么?
- 怎么结合
$status、$upstream_status、error.log 和后端日志判断 502 的来源?
proxy-read-timeout 为什么不是"总耗时上限"?对长任务接口意味着什么?
- ingress-nginx 的后端列表从哪里同步?Endpoint 为空时报什么码?
- keepalive 竞态的时序是怎样的?为什么 Gunicorn/Node 默认值下最容易触发?
- 后端空闲超时和代理池空闲超时的"阶梯"应该怎么排?
- preStop sleep 赌的是什么?它和 terminationGracePeriodSeconds 的大小关系?
- 为什么全局 ConfigMap 不能随便动?调大超时前必须算的三个数是什么?
- POST 接口的重试风险怎么实测,而不是靠背文档?
14.3 收尾
502/504 排查的难点从来不在命令,命令就那十几条;难在两件事。
第一,克制动手的冲动。 现场日志、endpoint 快照、nginx -T 先固定下来再谈修复,故障现场是不可再生资源,重启一次就没有了。
第二,把"错误码出现的位置"和"根因所在的位置"当成两件事。 Ingress 只是把后端的死亡方式如实转述给你,preStop 缺失、Gunicorn 两秒的 keep-alive、74 秒的导出 SQL、打满的 conntrack——它们都不会自己说话,它们的表现形式就是那行 502。你的工作是根据 error.log 的失败串、upstream 计时、抓包的 FIN/RST 时序、发布事件的时间线,把这些沉默的根因一个个还原出来,并用修复后的归零数据证明还原是对的。
超时与 keepalive 参数的配置心法只有一句:默认值不动、差异用 annotation 表达、空闲保活沿链路向内递增(被复用的一方永远比复用的一方活得更久)、每一次调大超时之前先算容量。 守住这句,这个领域 80% 的坑与你无关。