找回密码
立即注册
搜索
热搜: Java Python Linux Go
发回帖 发新帖
Claude、GPT 海外模型 API 接入Claude skills 从入门到精通 吴恩达亲授 AI Agent 核心技能2026 瞪哥公务员考试全攻略 行测申论一站式系统备考
Agent 文心智能蒸馏模型实战 90G 课程智泊 AI 大模型训练营 基于 LangChain 的 RAG 与提示工程实战构建企业级 AI 大脑:大模型微调与 RAG / Agent 全栈实战

5778

积分

0

好友

729

主题
发表于 前天 01:26 | 查看: 4| 回复: 0

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 尝试后端的次数上限 以所用版本文档和生成配置为准

两个容易踩的细节:

  1. proxy-read-timeout 是"两次读之间的间隔"。一个流式接口每次隔 30 秒吐一块数据,只要间隔不超过 60 秒,即使总耗时 10 分钟也不会 504;反过来,一个接口 70 秒不出任何字节,哪怕它第 65 秒就要返回结果,也会在第 60 秒被 nginx 掐掉并返回 504。这决定了长任务接口要"定期有心跳输出"或者单独调大超时。

  2. 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 默认不在列表中,若要重试须显式配置。重试带来两个后果:

好处是掩盖了大部分瞬时故障,用户无感知。坏处有两个:

  1. 耗时放大。 后端超时 60 秒 + 重试到另一后端再 60 秒,用户侧最坏要等 120 秒以上才看到 504,客户端可能早就断了。排查时如果发现"用户说等了很久"而单条 upstream_response_time 只有 60 秒,就要意识到日志里的一条记录可能对应了两跳后端。

  2. 非幂等请求需单独核验。 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 上,把它展开成七个必须回答的问题,按顺序回答,不许跳:

  1. 错误码到底是 502 还是 504? (用户口述不可信,以 ingress access log 的 status 为准)
  2. 是 Ingress 产生的还是上游透传的? (3.4 节的 upstream_status 对账)
  3. 发生频率和影响面? (全量持续 / 周期性窗口 / 随机零星 / 发布时必现 / 特定路径独有——每种形态对应完全不同的根因方向)
  4. error.log 里的失败串是哪一类? (3.2 节的对照表直接给出方向)
  5. Endpoint 和 Pod 生命周期有没有问题? (502 主线)
  6. 耗时和容量有没有问题? (504 主线)
  7. 各层超时/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])))

故障二的对账过程:

  1. Prometheus 显示 /report/export p99 约 74s,p50 约 40s,全部在 40s 以上;
  2. ingress error.log:upstream timed out (110);
  3. 应用日志:导出任务实际处理 74s,期间无异常;
  4. 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)修复组合:

  1. 业务 Deployment 加 preStop sleep(给 Endpoint 摘除传播留时间)+ 适当 grace 期(7.5 节 YAML);
  2. readiness 从 tcpSocket 改为 HTTP 探针打真实健康端点,Spring Boot 开启优雅停机(server.shutdown=graceful);
  3. ingress-nginx 侧维持默认重试(幂等 GET 类接口靠重试自愈)。注意:/api/callback 是 POST,重试有重复回调风险——这正是 3.6 节说的风险。实施前确认了支付回调按业务单号做了幂等去重,因此保留默认重试;若业务不具备幂等,需对该 Ingress 显式收紧 proxy-next-upstream,宁可牺牲自愈能力。

故障二(导出接口 504)修复组合:

  1. 仅对报表 Ingress 加 annotation 把 proxy-read-timeout/proxy-send-timeout 提到 120s(7.1 节),不动全局;
  2. 应用侧异步化导出(提交任务 + 轮询取结果),从根上消灭长同步请求——这是长期方案,调参只是止血;
  3. 给导出接口在应用层配独立线程池隔离,防止慢导出拖垮快接口;
  4. 若报表域名走独立 LB,LB 空闲超时同步 ≥ 120s 对齐(7.6 节),否则 LB 层先掐连接。

实施顺序纪律:先上 preStop/探针(纯增强、无副作用),观察一个发布窗口;再动超时参数;一次一个变量,每步都有第 11 节的验证和第 12 节的回滚预案兜底。

5.12 案例三:upstream sent too big header,"只有一部分用户 502"

补一个与前两个形态不同的 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. 风险提醒

    每一条都是生产里真出过事的类型:

    1. 全局 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 保留旧配置继续服务,但你以为改了什么)。

    2. 调大超时是借时间不是借容量。 proxy-read-timeout 从 60s 调到 300s,意味着慢请求洪峰时 nginx 与后端各自要多背 5 倍时长的连接占用;后端 Tomcat 默认 maxThreads 200,慢请求堆积会先于 ingress 打满应用线程池,把"504 故障"放大成"503/全站不可用故障"。调参必须同时确认连接与线程容量(7.4 与 13 节)。

    3. 收紧/放开重试直接影响资金安全。 proxy-next-upstream 保持默认时,POST 在请求发送前后有不同的重试约束,上线前必须实测(方法见 11.3),而不是依赖本文或任何文档的默认值描述。资金类接口按业务幂等能力评估重试;需关闭换后端时可设 nginx.ingress.kubernetes.io/proxy-next-upstream: "off"。

    4. 重启 ingress-controller 是生产破坏性操作。 rollout restart 期间该 controller 的全部域名有秒级抖动,多副本 + PodDisruptionBudget 也未必无感。它几乎不能修复 502/504 的根因(根因在应用/网络/参数),不要用重启代替排查。确需重启(如内存泄漏缓解):多副本逐台、低峰窗口、盯 5xx 曲线。

    5. 删除类命令的 namespace 陷阱。 kubectl -n ingress-nginx delete pod ... 只会杀 ingress 自身造成秒级抖动;但手滑 -n <业务ns> 的批量 label 删除就是事故。所有 delete 前先 get 同样的选择器确认命中列表;label 选择器宁可写死具体 name 也不要图省事用宽 label。

    6. nginx -s reload 风暴。 批量变更 Ingress(如 CI 一次性 apply 几十个)会触发密集 reload,reload 期间旧 worker 要等服务完存量长连接才退出,大量 reload 会累积 worker 与内存。批量变更尽量合并窗口、错峰。

    7. 改内核参数(conntrack 等)三件套:备份现值、按节点灰度、持久化文件先写好。 持久化文件写错会让节点重启后网络行为异常且不自愈,改完在 /etc/sysctl.d/ 里 sysctl --system 验证一遍。

    8. tcpdump 与压测都是生产流量压力源。 tcpdump 必须带 timeout 和 -c 上限;hey/ab 压测只在预发或低峰做,压测目标里包含写接口时先确认幂等。

    9. 日志清理不等于删日志文件。 truncate access log 在容器化 stdout 采集架构下基本不该由你手工做(轮转由 kubelet/containerd 管),真要清理节点上 /var/log 的历史轮转文件,用 find ... -name '*.log.*' -mtime +7 先 ls 列清单再删,不要直接 -delete。

    10. 修改防火墙/安全组以放行健康检查为高频踩坑点。 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. 生产环境注意事项

    1. 变更窗口与灰度顺序。 超时/重试类参数变更放低峰窗口;顺序:测试集群全量 → 生产非核心业务 Ingress → 观察 24h → 核心业务。全局 ConfigMap 变更只在维护窗口做,并通知所有业务方。

    2. per-Ingress 优先。 集群共享物尽量少动。一个接口要 120s 就给那个 Ingress 加 annotation,不要让全集群陪它等 120s。

    3. 容量评审。 调大超时前回答三个数:该接口峰值 QPS × 新增平均占用时长 = 新增并发连接数;ingress 副本数 × 每 worker 连接上限(worker_connections)是否覆盖;后端 maxThreads/worker 数是否覆盖。三个数算不出来,就不该调。

    4. 告警先行。 任何超时参数调大,等于把故障从"60 秒 504"改造成"120 秒 504",检测窗口同步拉长,对应接口的超时告警阈值要按新预算重新设计,否则监控会"瞎"。

    5. 文档化参数阶梯表。 集群维护一张"LB 空闲超时 / ingress upstream-keepalive-timeout / 各应用框架 keep-alive 默认值"的对照表并随版本更新,3.7 节那张表就是模板。90% 的 keepalive 竞态 502 源自这张表过期。

    6. 定期演练。 每季度在测试集群做一次故障演练:杀一个后端 Pod 同时打流量,看 502 数量是否在目标(0)以内;压测打满 accept 队列看告警是否在 5 分钟内触达。演练脚本进 Git,别留在个人终端。

    7. 镜像升级看 changelog。 ingress-nginx 的 release notes 里对 keepalive 行为、重试策略、默认值的变更都有明确条目,升级 controller 版本前后各跑一遍 11.2/11.3 的验证,把它当成一次配置变更来管理。

    8. 权限与审计。 ConfigMap、Deployment 的修改权限收敛到运维/平台组,开启审计日志(API audit);业务方只允许改自己 Ingress 的 annotation。

    9. 关注 ingress controller 自身的资源与探针。 controller 在 reload 和 TLS 握手密集时 CPU 会尖峰,requests/limits 压得过紧会让 reload 变慢、liveness 探针超时导致 controller 被反复重启,故障从业务转移到入口本身。controller 的 CPU、内存、reload 耗时要有自己的告警基线,liveness 的超时与失败阈值要给 reload 留余量。

    10. 域名切换/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% 的坑与你无关。




    上一篇:PhET 免费物理仿真平台:诺奖得主做的,浏览器打开就能做实验
    下一篇:内存泄漏是什么?成因、后果与避免方法,面试官听完都点头
    您需要登录后才可以回帖 登录 | 立即注册

    手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

    GMT+8, 2026-9-27 04:26 , Processed in 0.669349 second(s), 42 queries , Gzip On.

    Powered by Discuz! X3.5

    © 2025-2026 云栈社区.

    快速回复 返回顶部 返回列表