Nginx缓存DNS解析结果导致代理失败,报错404(NotFound)
这是一个不常见的问题。
有一天,我使用docker-compose部署了一个nginx节点和3个后端节点,nginx对3个后端节点做负载均衡的反向代理,当我把其中一个后端节点重启之后,我发现它再也收不到任何请求了,同时nginx也返回404,这就让我奇怪了,我只是重启了一下,无论是进入到nginx容器还是在宿主机都可以访问到重启之后的后端节点,就是通过nginx不行,重启nginx之后恢复正常,我多尝试了几次,发现时偶发的。查了很多资料,最终确定是Nginx缓存了DNS解析结果导致的。
为了简单演示,我简单做了一个dotnet后端的镜像
首先有一个控制器TestController
using Microsoft.AspNetCore.Mvc;
namespace test.Controllers;
[ApiController]
[Route("[controller]")]
public class TestController : ControllerBase
{
[HttpGet]
public string Get()
{
return $"data from {GetLocalIp()}";
}
private string GetLocalIp()
{
var addressList = System.Net.Dns.GetHostEntry(System.Net.Dns.GetHostName()).AddressList;
var ips = addressList.Where(address => address.AddressFamily == System.Net.Sockets.AddressFamily.InterNetwork)
.Select(address => address.ToString()).ToArray();
if (ips.Length == 1)
{
return ips.First();
}
return ips.Where(address => !address.EndsWith(".1")).FirstOrDefault() ?? ips.FirstOrDefault();
}
}
然后写一个Dockerfile来构建镜像:vi Dockerfile
FROM mcr.microsoft.com/dotnet/sdk:8.0
RUN dotnet new webapi -o DemoApi --no-https & \
cp TestController.cs /DemoApi/Controllers/
CMD ["dotnet", "run", "--urls=http://0.0.0.0:5000", "--project=DemoApi"]
然后构建镜像:sudo docker build -t demoapi:v1 .
接着,我们添加nginx反向代理配置demoapi.conf,这里只有两个后端应用做负载均衡,而且用的是容器服务器,因为后面准备采用docker-compose进行编排,所以这个service名称可以理解为dns的概念。
server {
listen 5001 ;
location / {
proxy_pass http://demoapi/;
}
}
upstream demoapi {
least_conn;
server demoapi1:5000;
server demoapi2:5000;
}
接着,我写了docker-compose.yml来编码容器
services:
nginx:
image: nginx:1.23.1
volumes:
- ./demoapi.conf:/etc/nginx/conf.d/demoapi.conf
demoapi1:
image: demoapi:v1
container_name: demoapi1
demoapi2:
image: demoapi:v1
container_name: demoapi2
networks:
default:
driver: bridge
然后构建容器:sudo docker compose up -d
容器起来之后,我们可以访问接口:curl http://localhost:5001/Test
可以看到,接口轮流的在两个后端节点的结果
接着我们按照下面的操作验证
# 查看一个后端节点的ip
sudo docker inspect demoapi1 | grep IPAddress
# 再停掉这个后端节点
sudo docker stop demoapi1
# 在同网络下随便开一个新的容器,需要自己确认网络名
sudo docker run -id --network [网络名] nginx:1.23.1
# 启动停掉的后端节点
sudo docker stop demoapi1
再次查看这个后端节点的IP,如果IP发生变化,那么接着调用上面的后端接口,会发现一个请求成功,一个返回404。
哪怕我们进入nginx容器里面,也是可以访问到的,但是nginx代理就是会失败。
原因 & 解决办法
这其实是因为,在nginx的upstream中采用了容器的服务器做转发地址,这就类似于一个域名,需要做DNS解析,docker容器中默认给你我们一个DNS解析服务地址(127.0.0.11),当nginx发现要转发的地址是域名时,会进行DNS解析,并缓存解析的结果,这样一来,如果后端服务器重启后ip变了,nginx是不知道的,转发就会失败。
查了一些资料,优先,我们无法禁用nginx的这个DNS缓存,只能从其它方向入手,主要有两个办法:
- 使用
resolver + 变量实现动态解析,
我们修改nginx的配置:
server {
listen 5001 ;
# 1. 配置 resolver
# 127.0.0.11 是 DNS 服务器
# valid=30s 表示 DNS 解析结果缓存 30 秒(默认是 5 分钟,建议设短一点以快速感知变化)
# ipv6=off 可选,如果后端不支持 IPv6 可关闭以加快解析
resolver 127.0.0.11 valid=30s ipv6=off;
# 可选:设置 DNS 查询超时时间
resolver_timeout 5s;
location / {
# 2. 定义变量
# 注意:必须使用变量,否则 resolver 不会生效
set $demoapi "demoapi1";
# 3. proxy_pass 中使用变量
proxy_pass http://$demoapi;
}
}
这样一来,我们就无法使用nginx的一些功能了,比如负载均衡等
- 配置让
nginx将请求转发到下一台上游服务器
server {
listen 5001 ;
location / {
proxy_pass http://demoapi/;
proxy_send_timeout 300s;
proxy_read_timeout 300s;
proxy_connect_timeout 5s;
# 当上游服务器返回配置的http状态时,将请求转发到下一台上游服务器
proxy_next_upstream error timeout http_500 http_502 http_503 http_504 http_404;
# 重试次数
proxy_next_upstream_tries 3;
}
}
这么做只是掩盖了问题,也就是说,ip变了的那个服务器再也不会受到请求了
- 对容器固定IP,这样容器之后就不会变了
修改docker-compose.yml
services:
nginx:
image: nginx:1.23.1
volumes:
- ./demoapi.conf:/etc/nginx/conf.d/demoapi.conf
demoapi1:
image: demoapi:v1
container_name: demoapi1
networks:
default:
ipv4_address: 172.127.0.10
demoapi2:
image: demoapi:v1
container_name: demoapi2
networks:
default:
ipv4_address: 172.127.0.10
networks:
default:
driver: bridge
ipam:
config:
- subnet: 172.127.0.0/16
一般不推荐这么做,因为容器创建于销毁时很常用的操作,如果指定的ip被占用,可能导致容器无法启动
总结
虽然我们一般使用nginx做反向代理时,基本都是采用ip来配置,但是在容器化环境下,我们一般不推荐采用ip,特别是k8s集群下,其实也能理解,nginx为了性能考虑才会缓存DNS的结果,但是还是希望能有相关的配置,可以采用一些策略来清理或者禁用这个DNS缓存。

浙公网安备 33010602011771号