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 会带来略多的连接建立开销。