很多人在 k8s 集群里习惯这样做:
前端用一个 Nginx Deployment,当成“自建 Ingress / 前端网关”,然后在 nginx.conf 里直接用 Service 名称 去反向代理后端服务。
一开始一切正常,但当你 更新后端服务 之后,诡异的问题来了:
- Nginx 容器里
ping 这个 Service 完全没问题
- 但通过 Nginx 访问接口,就是 502 / 连接不上
- 重启一下 Nginx Pod,又好了……
这其实就是:Nginx 的 DNS 缓存在 k8s 场景下坑了你。
下面就用一个真实案例,完整说明:
- 问题现象
- 背后原因(Nginx + k8s Service + DNS 的联动)
- 两套可落地的解决方案(原生 Nginx + Tengine 动态解析),附完整配置
一、问题:后端升级后,Nginx 无法再访问 Service
场景简化一下:
- 前端:一个 Nginx Pod,做反向代理
- 后端:若干微服务,通过 k8s Service 暴露,如
demo-dlp-service.huanfa
- Nginx 里直接写:
1
| proxy_pass http://demo-dlp-service.huanfa:80;
|
现象:
这类问题常让人误以为是:
- k8s 网络有问题?
- Service 没更新?
- Pod 探针 / readiness 有坑?
实际上,非也——核心问题在于:Nginx 的 DNS 解析结果被缓存住了。
二、原因:Nginx 只在“第一次”解析域名,之后就不再更新
在传统物理机 / 虚拟机环境中,后端服务器 IP 很少变,所以 Nginx 默认的 DNS 行为一直没啥问题:
- 配置里写了
proxy_pass http://some-domain:80;
- Nginx 在启动或第一次处理请求时做一次 DNS 解析
- 把解析出来的 IP 缓存在内存里,之后就一直用这个 IP
但是在 k8s 环境里,Service 对应的后端 Pod IP 是会变的:
- 滚动升级 / 重启 Pod
- HPA 扩容缩容
- 节点故障迁移
都会导致 Service 背后的 Endpoints 变化。
而如果你 只是简单地在 proxy_pass 里写了 Service 名称,又没有额外配置 DNS 相关指令,那么:
Nginx 仍然只会在“第一次”解析 Service 的域名,后面就一直用旧 IP。
这就是为什么你在 Nginx 容器里 ping 没问题(系统 DNS 是新的),
但 Nginx 自己还是连旧的 Pod(甚至已经不存在的 IP)——自然就访问失败了。
三、解决方案一:在原生 Nginx 中启用动态 DNS 解析(resolver + set)
在 nginx 1.18.0 版本中测试通过。
核心思路:
- 告诉 Nginx:DNS 服务器是谁(比如 k8s 里的
kube-dns)
- 将 Service 名称赋值给一个变量
- 在
proxy_pass 中使用这个变量
- 这样 Nginx 就会在访问时按
valid=… 设定,周期性重新解析域名
完整配置如下(保持原样,不做任何修改):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40
| user root; worker_processes auto; events { worker_connections 1024; } http { include mime.types; default_type application/octet-stream; access_log /var/log/nginx/nginx-access.log; error_log /var/log/nginx/nginx-error.log; log_format main '$remote_addr - $remote_user [$time_local] "$request" ' '$status $body_bytes_sent "$http_referer" ' '"$http_user_agent" "$http_x_forwarded_for"'; sendfile on; keepalive_timeout 65; gzip on; gzip_static on; proxy_buffer_size 128k; proxy_buffers 32 128k; proxy_busy_buffers_size 128k; fastcgi_buffers 8 128k; send_timeout 60; server { listen 80; server_name demo-dlp-service.huanfa; resolver kube-dns.kube-system.svc.cluster.local valid=5s; set $endpoint_service demo-dlp-service.huanfa.svc.cluster.local; proxy_http_version 1.1; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; location / { proxy_pass http://$endpoint_service:80; } } }
|
关键配置说明
resolver kube-dns.kube-system.svc.cluster.local valid=5s;
- 指定 Nginx 使用 k8s 的 DNS 服务(
kube-dns)进行解析
valid=5s 表示解析结果最多缓存 5 秒,过期后会重新查
set $endpoint_service demo-dlp-service.huanfa.svc.cluster.local;
- 把 Service 的完整域名(一定要写全 FQDN)赋值给变量
$endpoint_service
proxy_pass http://$endpoint_service:80;
- 使用变量做
proxy_pass,配合上面的 resolver,就能让 Nginx 定期重新解析域名
这种方案适用场景
- 已经在用 原生 Nginx(官方镜像等),不方便切到 Tengine
- 服务数量不算特别多,Nginx 做简单前端网关
- 希望配置简单、变更小,只是补上 DNS 动态解析能力
四、解决方案二:使用 Tengine 的 dynamic_resolve 功能
如果你愿意使用淘宝开源的 Tengine 3.1,可以直接用其内置的 dynamic_resolve 动态解析能力。
重点注意:Service 地址一定要写完整 FQDN,例如:
imes-system-service.demowms.svc.cluster.local。
下面是完整配置(保持原样,不做任何修改):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160
|
user nginx; pid /usr/share/nginx/temp/nginx.pid; worker_processes auto; worker_rlimit_nofile 65535;
include /usr/share/nginx/conf/modules-enabled/*.conf; events { multi_accept on; worker_connections 65535; } http { charset utf-8; sendfile on; tcp_nopush on; tcp_nodelay on; server_tokens off; log_not_found off; types_hash_max_size 2048; types_hash_bucket_size 64; client_max_body_size 16M; include mime.types; default_type application/octet-stream; access_log /usr/share/nginx/logs/access.log; error_log /usr/share/nginx/logs/error.log warn; gzip on; gzip_disable "msie6"; gzip_comp_level 6; gzip_min_length 1100; gzip_buffers 16 8k; gzip_proxied any; gzip_types text/plain text/css text/js text/xml text/javascript application/javascript application/x-javascript application/json application/xml application/rss+xml image/svg+xml/javascript; proxy_buffer_size 128k; proxy_buffers 32 128k; proxy_busy_buffers_size 128k; fastcgi_buffers 8 128k; send_timeout 60; resolver_timeout 5s; resolver kube-dns.kube-system.svc.cluster.local ipv6=off; upstream sys { dynamic_resolve fallback=stale fail_timeout=15s; server imes-system-service.demowms.svc.cluster.local:8150; } upstream ppc { dynamic_resolve fallback=stale fail_timeout=15s; server imes-ppc-service.demowms.svc.cluster.local:1100; } upstream dev { dynamic_resolve fallback=stale fail_timeout=15s; server imes-dev-service.demowms.svc.cluster.local:2100; } upstream wms { dynamic_resolve fallback=stale fail_timeout=15s; server imes-wms-service.demowms.svc.cluster.local:8155; } upstream fts { dynamic_resolve fallback=stale fail_timeout=15s; server imes-fts-service.demowms.svc.cluster.local:8190; } upstream flowable { dynamic_resolve fallback=stale fail_timeout=15s; server imes-flowable-service.demowms.svc.cluster.local:1587; } upstream po { dynamic_resolve fallback=stale fail_timeout=15s; server imes-po-service.demowms.svc.cluster.local:1200; } server { listen 443 ssl; server_name webServer; ssl_certificate /home/cert/server.crt; ssl_certificate_key /home/cert/server.key; ssl_session_cache shared:SSL:1m; ssl_session_timeout 5m; ssl_ciphers HIGH:!aNULL:!MD5; ssl_prefer_server_ciphers on; autoindex_localtime on; client_max_body_size 1200M; root /usr/share/nginx/html; index index.html; location ^~/api/ppc/ { proxy_read_timeout 300s; proxy_pass http://ppc/api/ppc/; } location ^~/api/dev/ { proxy_read_timeout 300s; proxy_pass http://dev/api/dev/; } location ^~/api/wms/ { proxy_read_timeout 300s; proxy_pass http://wms/api/wms/; } location ^~/api/sys/ { proxy_read_timeout 300s; proxy_pass http://sys/api/sys/; } location ^~/api/fts/ { proxy_read_timeout 300s; proxy_pass http://fts/api/fts/; } location ^~/static/ { proxy_pass http://fts/static/; } location ^~/api/fluent/ { proxy_read_timeout 1s; proxy_pass http://69.139.185.178:28081/api/fluent/; } location ^~/api/flowable/ { proxy_read_timeout 300s; proxy_pass http://flowable/api/flowable/; } location ^~/api/im/ { proxy_read_timeout 300s; proxy_pass http://192.168.0.117:30010/api/im/; } location ^~/api/po/ { proxy_read_timeout 300s; proxy_pass http://po/api/po/; } location / { root /usr/share/nginx/html/; add_header Cache-Control no-store; rewrite ^.+(?<!js|css|png|ico|jpg|jpeg|woff|ttf|map|otf)$ /index.html break; index index.html; } location ~* \.(?:js|css|png|jpe?g|gif|ico)$ { gzip_static on; } error_page 500 502 503 504 /50x.html; location = /50x.html { root /usr/share/nginx/html/; } } }
|
关键点解析
resolver kube-dns.kube-system.svc.cluster.local ipv6=off;
upstream sys { dynamic_resolve fallback=stale fail_timeout=15s; … }
以及其它一系列 upstream:
dynamic_resolve:开启动态解析
fallback=stale:当解析失败时,继续使用旧的 IP,避免瞬时解析失败导致大面积 502
fail_timeout=15s:失败后多长时间内视为不可用
server imes-xxx-service.demowms.svc.cluster.local:端口;
- 一定要写完整的 FQDN(
*.svc.cluster.local),否则可能无法正确解析
这种方案适用场景
- 你愿意使用 Tengine 作为前端 Web 服务器
- 有大量后端服务 / 复杂路由,需要更强大的 upstream 能力
- 希望在解析出问题时有更优雅的降级(例如
fallback=stale 继续使用旧 IP)
五、实践中的几个小建议
无论你选用哪种方案,建议注意以下几点:
永远使用 Service 的全限定域名(FQDN)
比如:my-service.my-namespace.svc.cluster.local
避免依赖 search domain,减少“在某些环境下突然解析失败”的坑。
适当调小 DNS 缓存时间
- 原生 Nginx:
valid=5s 是比较折中的设置
- Tengine:搭配
dynamic_resolve 使用
时间太短会增加 DNS 压力,太长又起不到动态更新的效果,一般 5–30 秒比较合适。
区分“系统 DNS 能 ping 通”和“Nginx 内部解析是否更新”
ping service 成功,只说明 容器的 /etc/resolv.conf + CoreDNS 正常
- Nginx 使用的是 自己的 DNS 缓存机制,必须通过 resolver / dynamic_resolve 主动接入。
升级 / 重启策略
- 即便有了动态解析,生产环境里还是建议对 Nginx 做滚动升级 / 定期 reload
- 避免因为历史配置 / 旧连接导致的边角问题
六、总结
在 k8s 中使用 Nginx 作为前端服务器时,DNS 缓存问题是一个非常容易被忽略的坑:
- 表面上看是“后端升级后,Nginx 偶尔 502 / 访问失败”
- 实际上是:Nginx 只在第一次解析域名,后面 Service 或 Pod IP 变了,它却仍然握着旧 IP 不放
本文基于实际案例,给出了两种实测可行的解决方案:
原生 Nginx:
使用 resolver + set + 变量 proxy_pass,在 nginx 1.18.0 上验证通过
Tengine:
使用 dynamic_resolve,支持更丰富的动态解析和降级策略
如果你也在 k8s 里用 Nginx 直连 Service,建议尽快检查自己的 nginx.conf,
看看是不是还停留在“写死一个 Service 名称”的阶段——
早点补上 DNS 动态解析配置,可以少踩很多坑。