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代理就是会失败。

原因 & 解决办法

这其实是因为,在nginxupstream中采用了容器的服务器做转发地址,这就类似于一个域名,需要做DNS解析,docker容器中默认给你我们一个DNS解析服务地址(127.0.0.11),当nginx发现要转发的地址是域名时,会进行DNS解析,并缓存解析的结果,这样一来,如果后端服务器重启后ip变了,nginx是不知道的,转发就会失败。

查了一些资料,优先,我们无法禁用nginx的这个DNS缓存,只能从其它方向入手,主要有两个办法:

  1. 使用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的一些功能了,比如负载均衡等

  1. 配置让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变了的那个服务器再也不会受到请求了

  1. 对容器固定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缓存。

posted @ 2026-07-12 17:59  没有星星的夏季  阅读(18)  评论(0)    收藏  举报