Kubernetes 中 Nginx 代理的“失忆症”:为何后端恢复了,它却不知道?

183 阅读5分钟

在 Kubernetes 中使用 Nginx 作为代理时的一个经典痛点:服务更新或故障恢复后,Nginx 却无法感知后端 Pod 的 IP 地址变化,导致流量中断。 这个问题的核心是:Nginx 的 DNS 缓存机制。 在 Kubernetes 环境中,Pod 的生命周期是动态的。当一个 Deployment 下的 Pod 因故障重启、版本更新或节点伸缩而重建时,它的 IP 地址大概率会发生变化。虽然 Kubernetes 的 Service 机制通过 kube-proxyCoreDNS 保证了服务名(如 my-backend-service)始终指向健康的 Pod,但这个过程对 Nginx 来说并非完全透明。

image.png 问题的根源

默认情况下,Nginx 在启动时或重载配置(reload)时,会对 proxy_passupstream 块中定义的域名进行一次 DNS 解析,并将解析到的 IP 地址缓存下来。之后的所有请求,Nginx 都会直接使用这个缓存的 IP 地址。

这个机制在传统的、IP 地址相对固定的环境中运行良好。但在 Kubernetes 这类动态环境中,这就成了一个“定时炸弹”。一旦后端 Pod IP 变更,Nginx 的缓存没有更新,它依然会将请求转发到旧的、已经失效的 IP 地址,从而引发 502 Bad Gateway 或连接超时等错误。除非手动执行 nginx -s reload 或重启 Nginx Pod,否则它将永远无法“恢复记忆”。

案例分析:一个典型的故障场景

一个常见的场景是使用一个自定义的 Nginx Pod 作为反向代理,去转发流量到后端的多个 Headless Service。

在一个技术论坛的讨论中,一位开发者就分享了他的经历。他将 Nginx 配置为代理一个名为 python-www 的后端服务。当他手动删除后端的 Pod 以模拟故障恢复时,新的 Pod 获得了新的 IP 地址。Kubernetes 的 DNS 服务(CoreDNS)正确地更新了 python-www 的解析记录,但在 Nginx Pod 内部,无论怎么请求,流量始终发往旧的 IP,导致持续性的请求超时。通过 tcpdump 抓包发现,在后端 Pod 重启后,Nginx 根本没有发起新的 DNS 查询请求。

这个案例生动地证明了前面的判断:问题不在 Kubernetes 的服务发现,而在 Nginx 自身的解析行为。

解决方案:唤醒 Nginx 的动态 DNS 解析能力

解决方案的关键是让 Nginx “抛弃”启动时缓存的旧思想,转而在运行时动态地、定期地重新解析域名。这需要两个关键指令的配合:resolverresolve

1. 配置 resolver 指令:告诉 Nginx 去问谁

首先,我们需要在 http 配置块中告诉 Nginx 应该向哪个 DNS 服务器发起查询。在 Kubernetes 集群内部,这个 DNS 服务器就是 kube-dnsCoreDNS 服务。

我们可以通过查看集群的 kube-dns Service 获取其 ClusterIP。通常,这个地址是 10.96.0.10(或其他预留地址)。

http {
    # 指定 Kubernetes 集群的 DNS 服务地址
    # valid=30s 表示 Nginx 会缓存解析结果 30 秒,30 秒后会重新查询
    resolver 10.96.0.10 valid=30s;

    # ... 其他配置
}

实用建议: 为了让配置更具可移植性,避免硬编码 IP 地址,我们可以使用 Kubernetes DNS 服务的 FQDN(完全限定域名)kube-dns.kube-system.svc.cluster.local。这样,无论我们的集群 DNS 服务 IP 是什么,Nginx 都能正确找到它。

http {
    # 推荐:使用服务名代替硬编码的 IP
    resolver kube-dns.kube-system.svc.cluster.local valid=30s;

    # ... 其他配置
}

2. 配置 upstreamresolve 参数:启动动态解析

接下来,在定义上游服务时,我们需要在 server 指令后添加 resolve 参数。这个参数告诉 Nginx,这个域名需要被动态解析,而不是在启动时一次性解析。

http {
    resolver kube-dns.kube-system.svc.cluster.local valid=30s;

    upstream my_backend {
        # 使用 resolve 参数启用动态解析
        # 注意:这里必须使用域名,不能是 IP
        server my-backend-service.default.svc.cluster.local resolve;
    }

    server {
        listen 80;

        location / {
            proxy_pass http://my_backend;
        }
    }
}

关键点:

  • 必须使用变量:如果我们不使用 upstream 块,而是直接在 proxy_pass 中使用域名,那么我们需要借助一个变量来强制 Nginx 在运行时解析。当 proxy_pass 的值包含变量时,Nginx 不会缓存 IP,而是会在每次请求时(或根据 resolvervalid 时间)重新解析。
http {
    resolver kube-dns.kube-system.svc.cluster.local valid=30s;

    server {
        listen 80;

        location / {
            # 使用变量强制 Nginx 运行时解析
            set $backend_host my-backend-service.default.svc.cluster.local;
            proxy_pass http://$backend_host:8080;
        }
    }
}
  • 使用完全限定域名(FQDN):当 Nginx 使用 resolver 指令时,它会忽略 Pod 本地的 /etc/resolv.conf 文件中的 search 域。这意味着我们不能再使用像 my-backend-service 这样的短名称,而必须提供完整的 FQDN,例如 my-backend-service.default.svc.cluster.local(服务名.命名空间.svc.cluster.local)。否则,Nginx 会报告 "host not found" 或 "server failure" 的错误。

总结与最终建议

在 Kubernetes 这种 IP 地址动态变化的环境中,依赖 Nginx 默认的 DNS 缓存行为是不可靠的。

最终推荐配置:

为了实现健壮、自动化的服务发现,请遵循以下最佳实践:

  1. http 块中定义 resolver,并指向 Kubernetes 内部的 DNS 服务,最好使用其服务名(kube-dns.kube-system.svc.cluster.local)。
  2. resolver 设置一个合理的 valid 时间,如 10s30s,以平衡 DNS 查询开销和地址更新的及时性。
  3. upstream 中使用 resolve 参数,并确保 server 指令后跟随的是后端服务的 FQDN

通过这样配置,Nginx 就能摆脱“失忆症”,在后端服务 IP 变化后自动、快速地恢复流量转发,让我们的系统更加稳定和富有弹性。 希望这篇文章能帮大家在未来的工作中避免踩坑!