案例 Kubernetes ingress 六小时 502:default_server 挂在一个被健康检查清空的 upstream 上

2026-09-26 21:06:20

Kubernetes ingress 六小时 502:default_server 挂在一个被健康检查清空的 upstream 上

来源:Anatomy of a 6-hour Kubernetes ingress outage

一次后端 Deployment 失去全部健康 Pod。nginx 主动健康检查把 upstream 池标记为空。那个池是 80 和 443 的 default_server,于是所有未匹配到具体 server 块的域名返回 502,持续 6 小时 38 分钟。触发点在 Kubernetes 侧,爆炸半径来自多年前的一个配置选择,事后修复几乎都在 nginx 侧。

症状

受影响 ingress 对上所有域名的请求都返回 connection refused 或 502 Bad Gateway。nginx error log 以每小时数 GB 的速度增长,主要由两种模式构成:

no live upstreams while connecting to upstream
upstream timed out (110: Connection timed out) while connecting to upstream

实例 1 上大约 420 万条 no live upstreams、59.3 万条 timeout,实例 2 数量几乎一致。两个独立 ingress Pod 出现完全相同的模式,基本可以排除 ingress 侧自身是原因。

架构

Internet -> Cloud LB -> nginx ingress pods(2 个,配置一致)-> Kubernetes 节点上的 NodePort:

  • :31820 -> default-app,同时是 :80 和 :443 的 default_server
  • :32443 -> https-apps
  • :30100 -> static-apps
  • :31808 -> internal-services
  • :8080 -> dns-resolved-app(通过集群 DNS 解析)

关键点在于 :31820 后面的 upstream 同时承担 80 和 443 的 default_server,任何没有匹配到更具体 server 块的请求都会落到它上面。

根因

06:44 UTC,:31820 后端 Pod 失去响应。nginx 侧开了主动健康检查:

check interval=5000 rise=2 fall=5 timeout=3000 type=http;
check_http_send "GET / HTTP/1.0\r\nHost: example.com\r\n\r\n";
check_http_expect_alive http_2xx http_3xx;

连续 5 次检查失败(fall=5,25 秒窗口)后,upstream 池里的每一台 server 都被标记为 DOWN,池变成空池。由于这个池同时是两个端口的 default_server,所有未匹配具体 server 块的请求都打到空 upstream 上,返回 502。

级联过程:

  • 池清空 => 所有未匹配域名 502;
  • 健康检查失败按完整请求速率写日志,单个实例 error log 超过 1.5 GB;
  • 连接预算和 worker 时间被失效的 default 占满,其他 upstream 开始超时;
  • 另一个 upstream 用集群 DNS 配置(server some-app.namespace.svc.cluster.local:8080 resolve;),CoreDNS 受压后该解析也失败。

修复

大致按优先级排序。

1. 调超时,避免失败级联

2 秒的 connect timeout 对已经在承压的后端过于激进,每次重试都占一个 worker 槽位。

proxy_connect_timeout 5s;   # was 2s
proxy_send_timeout 15s;     # was 8s
proxy_read_timeout 15s;     # was 8s
send_timeout 5s;            # was 2s

在两个实例的 14 个 location 块上统一应用。

2. 调 max_fails 与 fail_timeout

原配置 max_fails=5 fail_timeout=10s:连续 5 次失败把 server 标记为 down,10 秒后重试;后端持续异常时会产生持续抖动。改为 max_fails=3 fail_timeout=30s——失败检测更快,恢复更慢。下一次降级期间日志刷屏量下降了一个数量级。

3. 用显式 NodePort 列表替换集群 DNS upstream

upstream some_app_upstream {
  least_conn;
  server 10.0.0.11:32100 max_fails=3 fail_timeout=30s;
  server 10.0.0.12:32100 max_fails=3 fail_timeout=30s;
  server 10.0.0.13:32100 max_fails=3 fail_timeout=30s;
  server 10.0.0.14:32100 max_fails=3 fail_timeout=30s;
  server 10.0.0.15:32100 max_fails=3 fail_timeout=30s;
  keepalive 32;
}

4. 收敛 upstream keepalive

default 池上每 worker keepalive 10240 是历史遗留值,从未做过基准测试。降到 256,延迟无可测量变化,内存占用和 FD 压力都更小。

5. 对 "no live upstreams" 告警

加一条基于日志的告警,no live upstreams 速率非零即触发,再对几个高价值域名加合成拨测。

6. 修掉一个潜伏的 SSL 配置问题

有个监听 443 ssl 的 server 块没有任何 ssl_certificate 指令,一直静默存活。修复后加了 CI lint,对渲染出的配置跑 nginx -t。

7. 修真正坏掉的后端

:31820 后面的 Deployment 处于 CrashLoopBackOff,另有一个节点已经静默死亡 19 天,容量随之减少。增加了一个 job,标记任何 NotReady 超过一小时的节点。

经验

  • Catchall 流量不应依赖单一的 upstream 池:可以跑两个并行的 default 后端并加权,或者在 default 池为空时返回一个静态致歉页。
  • 健康检查调参是系统的一半:检测快是好事,恢复慢是坏事。
  • 数据路径上的 DNS 服务发现是一种耦合;只要它在路径上,就把解析器当一级依赖对待。
  • error log 每小时超过 1 GB,本质上是"你没有对真正的症状告警"。

代价

更慢的失败检测:max_fails=3 加上更长的超时,意味着一个真正死掉的后端要多花几秒才被标记 down。静态节点列表增加了一步配置更新。降低 keepalive 会带来略多的连接建立开销。

复制全文 生成海报 运维 nginx Kubernetes 故障复盘 高可用

推荐文章

程序员茄子在线接单