运维踩坑:nginx 缓存上游容器旧 IP,请求转发到了错误的服务
容器化微服务环境,前置 nginx 反代到 Spring Cloud Gateway。某次 gateway 容器重建后,所有经 nginx 的 /api/** 请求开始返回 500,而 gateway 自身、服务发现、各业务服务均正常。
排查绕了一大圈,根因是 nginx 的一个经典行为:proxy_pass 写主机名时只在启动时解析一次,之后永久缓存。上游容器重建换 IP 后,nginx 仍在连旧 IP——而旧 IP 已被 Docker 分配给了另一个服务。
现场
前置 nginx + Spring Cloud Gateway + Nacos 架构,全部跑在同一个 Docker 自定义网络(172.18.0.0/16)里:
1 | Docker 网络 172.18.0.0/16 |
请求链路本该是 客户端 → nginx → gateway → 业务服务,gateway 再按 Nacos 的实例列表把请求路由到 project-order / project-auth。但出问题时 nginx 根本没把请求交给 gateway——某个经 nginx 的接口请求报错:
1 | GET http://10.20.30.40:8000/api/xxx |
几个看似矛盾的现象,把排查方向带偏了数次:
- 服务发现正常:目标服务已注册到 Nacos,gateway 也订阅到了实例。
- gateway 容器内直连正常:
curl gateway:8080/xxx返回 200。 - 但经 nginx 的请求,gateway 一条日志都没有——请求根本没到 gateway。
- nginx 配置看不出问题:
location /api/ { proxy_pass http://project-gateway:8080/; },proxy_pass带尾部/会剥前缀,没毛病。 docker exec nginx curl project-gateway:8080也是通的。
配置对、直连通、exec 也通,唯独经 nginx 就 500 且 gateway 收不到——问题出在 nginx worker 实际把请求转发到了哪。抓 nginx worker 的真实 TCP 连接目标(/proc/net/tcp 解码):
1 | 38 -> 172.18.0.20:8080 ← nginx worker 在连这个 IP |
对照各容器当前 IP:
1 | project-gateway -> 172.18.0.21 |
nginx 把请求转发到了 172.18.0.20(业务服务 project-order),而非 gateway(.21)。该服务没有对应路由,抛异常被框架兜底成 500。
为什么
docker exec通、worker 却不通?exec启动的新 shell 用当前 DNS(解析到.21),而 nginx worker 用的是启动时缓存的旧 IP(.20)。同一容器、同一网络栈,DNS 解析的时机不同。
根因
1 | location /api/ { |
proxy_pass写的是主机名,且全配置没有resolver指令 → nginx 在启动时解析一次并永久缓存,之后不再重新解析。- gateway 容器重建后 IP 由
.20变.21,旧 IP.20被 Docker 重新分配给了project-order。 - nginx 仍在连旧 IP,于是所有
/api/**请求都打到了project-order。
一句话:proxy_pass 写主机名 = 启动期解析一次后缓存,容器 IP 一变就连错。
修复
resolver + 变量版 proxy_pass,让 nginx 按 TTL 动态解析:
1 | http { |
三个关键点:
resolver 127.0.0.11 valid=30s—— Docker 内置 DNS,解析结果 30 秒过期。set $upstream+ 变量版proxy_pass——proxy_pass一旦含变量,nginx 就不再启动期静态解析,而是运行时走resolver。这是社区版 nginx 动态解析上游的唯一方式。rewrite ^/api/(.*)$ /$1 break—— 必须显式剥前缀(原因见下)。
连环坑:变量版 proxy_pass 不剥前缀
从纯文本 proxy_pass http://x:8080/; 改成变量版后,原来自动剥 /api/ 前缀的能力消失了,后端会收到错误的 path(实测 gateway 收到的是 /)。所以必须配 rewrite ... break 显式剥,并且 proxy_pass 去掉尾部 /(配 rewrite break 时必须是纯地址,带 URI 会二次改写)。
别走这条路
upstream {} 块里写主机名同样是启动期一次性解析,不动态刷新(只有 Nginx Plus 的 resolve 参数才动态)。upstream 块解决不了这个问题。
改完 nginx -t && nginx -s reload,容器怎么重建、IP 怎么变,nginx 都会按 30 秒 TTL 自动跟上。