在 Kubernetes 中使用 Nginx 作为代理时的一个经典痛点:服务更新或故障恢复后,Nginx 却无法感知后端 Pod 的 IP 地址变化,导致流量中断。
这个问题的核心是:Nginx 的 DNS 缓存机制。
在 Kubernetes 环境中,Pod 的生命周期是动态的。当一个 Deployment 下的 Pod 因故障重启、版本更新或节点伸缩而重建时,它的 IP 地址大概率会发生变化。虽然 Kubernetes 的 Service 机制通过 kube-proxy 和 CoreDNS 保证了服务名(如 my-backend-service)始终指向健康的 Pod,但这个过程对 Nginx 来说并非完全透明。
问题的根源
默认情况下,Nginx 在启动时或重载配置(reload)时,会对 proxy_pass 或 upstream 块中定义的域名进行一次 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 “抛弃”启动时缓存的旧思想,转而在运行时动态地、定期地重新解析域名。这需要两个关键指令的配合:resolver 和 resolve。
1. 配置 resolver 指令:告诉 Nginx 去问谁
首先,我们需要在 http 配置块中告诉 Nginx 应该向哪个 DNS 服务器发起查询。在 Kubernetes 集群内部,这个 DNS 服务器就是 kube-dns 或 CoreDNS 服务。
我们可以通过查看集群的 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. 配置 upstream 与 resolve 参数:启动动态解析
接下来,在定义上游服务时,我们需要在 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,而是会在每次请求时(或根据resolver的valid时间)重新解析。
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 缓存行为是不可靠的。
最终推荐配置:
为了实现健壮、自动化的服务发现,请遵循以下最佳实践:
- 在
http块中定义resolver,并指向 Kubernetes 内部的 DNS 服务,最好使用其服务名(kube-dns.kube-system.svc.cluster.local)。 - 为
resolver设置一个合理的valid时间,如10s或30s,以平衡 DNS 查询开销和地址更新的及时性。 - 在
upstream中使用resolve参数,并确保server指令后跟随的是后端服务的 FQDN。
通过这样配置,Nginx 就能摆脱“失忆症”,在后端服务 IP 变化后自动、快速地恢复流量转发,让我们的系统更加稳定和富有弹性。 希望这篇文章能帮大家在未来的工作中避免踩坑!