DEBUG.WIKI
ZgoCloud:多地区部署,按需选节点;日本、美国、香港、德国
雨云;给项目一台云服务器
RackNerd;$21.99/年 · 1GB KVM

Nginx 多个站点重复 include 后报 duplicate upstream

多个 Nginx 站点重复 include 完整 upstream 块,会触发 duplicate upstream。核对加载入口,在 http 中只定义一次后端组,再由各站点引用。

把多个站点的后端服务器列表抽到一个文件后,每个站点都 include 这个文件,配置检查却报错:

nginx: [emerg] duplicate upstream "cluster_backend" in /etc/nginx/upstreams/cluster.conf:1
nginx: configuration file /etc/nginx/nginx.conf test failed

如果公共文件包含完整的 upstream cluster_backend { ... },而两个站点文件又都在同一个 http 配置中加载它,就会重复声明同名 HTTP 后端组。后端是否在线、两个站点是否使用不同域名,都不能消除这个配置错误。

这篇处理的是 HTTP 反向代理中重复定义 upstream 的场景。stream 配置、第三方模块或修改过源码的 Nginx 需要按各自上下文判断;不要只凭报错文件名就删除一个仍被其他站点使用的公共文件。

公共文件只放了一份,也可能被解析两次

例如,主配置加载两个站点:

http {
    include /etc/nginx/sites-enabled/a.conf;
    include /etc/nginx/sites-enabled/b.conf;
}

两个站点文件各自包含下面这一行,且这一行位于各自的 server 块之外:

include /etc/nginx/upstreams/cluster.conf;

公共文件的内容如下。地址和端口只是示例,需要换成实际 HTTP 后端:

upstream cluster_backend {
    server 192.0.2.10:8080;
    server 192.0.2.11:8080;
}

include 会把文件中的指令纳入当前位置的配置解析。它没有“同一文件只导入一次”的保护,也不会为不同站点建立独立的 upstream 名称空间。解析第二个站点时,整个 upstream cluster_backend 块又被声明了一遍,因此发生冲突。

报错所指的行是碰到重复声明的位置,不一定能直接告诉你第一次是从哪里加载的。两个文件分别写了同名组、公共文件同时被明确路径和通配符纳入,也需要沿加载关系核对。

upstream 块应放在 http 上下文中。把公共文件的 include 移进 server 或 location,会使完整 upstream 块出现在不允许的位置,并不能解决共用后端组的问题。

从实际主配置找出重复入口

先确认服务使用的 Nginx 可执行文件、主配置以及启动时是否指定了前缀。主机上有多套安装或容器时,终端里的默认 nginx 不一定就是运行中的那一套。

下面以普通主机安装为例,替换两项绝对路径后再检查;若服务原本带有 -p,检查和重载命令也应使用相同前缀:

NGINX_BIN=/usr/sbin/nginx
NGINX_CONF=/etc/nginx/nginx.conf
sudo "$NGINX_BIN" -V
sudo "$NGINX_BIN" -t -c "$NGINX_CONF"

-V 能查看版本及编译配置;-t 检查配置语法,并尝试打开配置引用的文件。先记下完整报错路径,再查看主配置中实际使用的 include 路径。不要把磁盘上所有 .conf 文件都当成正在生效的配置。

如果安装了 ripgrep,可以在实际配置目录内找声明和加载入口:

sudo rg -n -i '^[[:space:]]*upstream[[:space:]]+cluster_backend[[:space:]]*\{' /etc/nginx
sudo rg -n '^[[:space:]]*include[[:space:]]+' /etc/nginx

搜索结果需要结合加载关系判断。第一条只找到一份声明,也不能排除该文件被两个 include 入口重复读取;还要从主配置逐层核对,尤其注意站点目录中的链接,以及“明确 include 一个文件,同时又用通配符包含它所在目录”的组合。源码对 HTTP upstream 名称的比较不区分大小写,改成大小写不同的组名也不能用来区分两组。

Nginx 1.9.2 起支持 -T,在配置检查之外输出配置文件内容。修正到能够通过检查后,可用它核对加载结果:

sudo "$NGINX_BIN" -T -c "$NGINX_CONF"

配置仍有致命错误时,不要指望这个输出给出完整加载清单;它也不是运行中旧配置的快照。输出可能包含认证或业务配置,不要未经脱敏直接粘贴到公开渠道。

一个后端组只定义一次,多个站点引用它

若两个站点确实要共用相同的后端组及组内策略,把公共文件放到主配置的 http 下加载一次,删除各站点文件中对这个公共文件的重复 include。两个站点仍然保留各自的 server_name 和路由。

下面是展示位置关系的完整最小配置。不要用它直接覆盖已有生产主配置;生产中的 TLS、日志、认证、超时和其他站点配置都需要保留。示例的回环监听地址用于本机验证,后端地址是占位值:

events {
    worker_connections 1024;
}

http {
    upstream cluster_backend {
        server 192.0.2.10:8080;
        server 192.0.2.11:8080;
    }

    server {
        listen 127.0.0.1:8080;
        server_name a.example.test;

        location / {
            proxy_pass http://cluster_backend;
            proxy_set_header Host $host;
        }
    }

    server {
        listen 127.0.0.1:8080;
        server_name b.example.test;

        location / {
            proxy_pass http://cluster_backend;
            proxy_set_header Host $host;
        }
    }
}

拆分文件时,只需把示例中的 upstream 块放进公共文件,并在原位置用一次 include /etc/nginx/upstreams/cluster.conf; 替换。若该公共文件已经由 http 下的通配符加载,就不要再添加明确路径的 include。站点中的 proxy_pass http://cluster_backend; 是引用,不会再次创建这个组。

示例用 proxy_set_header Host $host; 将站点主机名传给后端,适用于后端需要按域名区分请求的情况。现有应用若要求固定 Host,应保留原先约定,不能因为抽取后端组就顺便改变请求头或代理路径。

如果两个站点需要不同的后端成员、权重或负载均衡方式,就应使用不同名称的组,并让各自的 proxy_pass 指向对应名称。两个不同名称的组包含相同服务器地址,本身不会触发 duplicate upstream。强行合并可能让一次后端调整同时影响多个站点。

配置通过后再重载,分别验证两个站点

改动前,把主配置、两个站点文件和公共文件一起备份到不会被现有 include 匹配的位置,并保存链接关系。只备份公共文件不够:本次修改涉及的是它与各个加载入口之间的关系。

在上述路径变量仍有效的同一终端中,先检查,通过后才发送重载信号:

sudo "$NGINX_BIN" -t -c "$NGINX_CONF" &&
    sudo "$NGINX_BIN" -s reload -c "$NGINX_CONF"

配置检查通过意味着这次解析没有再遇到同名组冲突,不代表后端服务已可用。重载后查看实际错误日志,分别访问两个站点的已知正常路径,确认返回的是各自预期的应用内容;同时检查后端收到的 Host 和请求路径是否保持预期。

如果使用上面的回环监听示例,可以从同一主机发起两个请求。/ 应替换为应用明确支持且无副作用的检查路径:

curl -i --noproxy '*' -H 'Host: a.example.test' http://127.0.0.1:8080/
curl -i --noproxy '*' -H 'Host: b.example.test' http://127.0.0.1:8080/

若此时出现 502 或应用内容不对,应继续检查后端连通性、协议和站点路由,不要再复制一份同名 upstream 试图补救。

Nginx 在重载时若无法应用新配置,会继续使用旧配置。若新配置能够加载但业务行为有误,则需要恢复这次改动涉及的整组配置文件,通过检查后再次重载。恢复重复 include 的故障版本不会成为有效回退,应使用修改前保存的、已知可工作的配置版本。