运维踩坑:nginx 缓存上游容器旧 IP,请求转发到了错误的服务

💡 原文中文,约4300字,阅读约需11分钟。
📝

内容提要

nginx反代Spring Cloud Gateway时,因proxy_pass使用主机名导致DNS解析缓存,容器重建后IP变化而nginx仍连旧IP,引发500错误。解决方法是配置resolver和变量版proxy_pass,按TTL动态解析,并显式rewrite去除前缀。

🔎

延伸解读

为什么 docker exec 通,nginx 却不通?

docker exec 启动的新 shell 会使用当前 DNS 解析,得到容器的最新 IP;而 nginx worker 进程在启动时解析主机名并缓存,之后不再更新。因此,即使容器重建后 IP 变化,nginx 仍会连接旧 IP,导致请求转发到错误的服务。

变量版 proxy_pass 的隐藏坑

使用变量版 proxy_pass 后,nginx 不再自动剥离 URI 前缀。原配置中 proxy_pass http://project-gateway:8080/ 会自动去掉 /api/,但改为变量后,必须显式添加 rewrite ^/api/(.*)$ /$1 break,并确保 proxy_pass 不带 URI,否则后端会收到错误的路径。

upstream 块同样无法解决动态解析

在 upstream 块中写主机名,nginx 同样只在启动时解析一次,不会动态刷新。只有 Nginx Plus 的 resolve 参数支持动态解析。因此,社区版 nginx 中,使用 resolver 和变量版 proxy_pass 是唯一实现动态解析上游的方式。

Q&A

nginx反代Spring Cloud Gateway时,为什么容器重建后请求会返回500错误?

因为nginx的proxy_pass指令使用主机名时,只在启动时解析一次并永久缓存。当gateway容器重建后IP发生变化,nginx仍然连接旧IP,而旧IP可能已被分配给其他服务,导致请求被转发到错误的服务,从而返回500错误。

如何解决nginx缓存上游容器旧IP的问题?

配置resolver指令和变量版proxy_pass,让nginx按TTL动态解析上游主机名。具体步骤:在http块中添加resolver 127.0.0.11 valid=30s ipv6=off;,在location中使用set $upstream_gateway project-gateway;和proxy_pass http://$upstream_gateway:8080;,并配合rewrite ^/api/(.*)$ /$1 break;显式去除前缀。

nginx的proxy_pass使用变量和直接写主机名有什么区别?

直接写主机名时,nginx在启动时解析一次并缓存,之后不再更新;使用变量时,nginx会在运行时通过resolver动态解析,并按照resolver的valid参数指定的TTL定期刷新,从而适应容器IP的变化。

为什么在nginx配置中需要显式使用rewrite去除前缀?

因为当proxy_pass使用变量时,nginx不会自动剥离location匹配的前缀。例如,location /api/ { proxy_pass http://$upstream_gateway:8080; } 会将完整的URI(包括/api/)传递给后端,导致后端收到错误的路径。因此需要显式使用rewrite ^/api/(.*)$ /$1 break;来去除前缀,并确保proxy_pass不带URI部分。

nginx的upstream块中写主机名能否解决动态解析问题?

不能。upstream块中写主机名同样是在启动时解析一次,不会动态刷新。只有Nginx Plus的resolve参数才支持动态解析。因此,社区版nginx无法通过upstream块解决此问题,必须使用resolver和变量版proxy_pass。

在Docker环境中,nginx的resolver应该配置什么地址?

在Docker环境中,nginx的resolver应配置为Docker内置DNS服务器地址127.0.0.11,并可以设置valid参数指定缓存时间,例如valid=30s,同时可设置ipv6=off以禁用IPv6解析。

🏷️

标签

➡️

继续阅读