建站问答(五):”Upstream” vs “变量 + Resolver” —— 两种完全不同的反向代理机制

4次阅读
没有评论

共计 7780 个字符,预计需要花费 20 分钟才能阅读完成。

一、先看两种配置长什么样

1.1 方案 A:upstream 块(本方案采用)

# ─── 定义后端池 ───
upstream wordpress_backend {
    server wordpress:80 max_fails=3 fail_timeout=10s;
    keepalive 32;
}

# ─── 反向代理 ───
location / {
    proxy_pass http://wordpress_backend;
    proxy_http_version 1.1;
    proxy_set_header Connection "";
    # ...
}

1.2 方案 B:变量 + resolver(另一种写法)

# ─── 指定 DNS 服务器 ───
resolver 127.0.0.11 valid=10s ipv6=off;
resolver_timeout 5s;

# ─── 反向代理 ───
location / {
    set $backend "wordpress";
    proxy_pass http://$backend;
    proxy_http_version 1.1;
    # ...
}

表面上看,两者都是”把请求转发到 wordpress:80“,但 Nginx 内部的处理路径完全不同


二、核心差异:什么时候知道”后端是谁”

这是理解一切差异的关键。

2.1 upstream:配置加载时就确定了后端

Nginx 启动 / reload
       │
       ▼
  解析 nginx.conf
       │
       ├── 遇到 upstream wordpress_backend { server wordpress:80; }
       │
       ▼
  调用系统 DNS 解析 "wordpress" → 172.18.0.3
       │
       ▼
  将 IP 地址存入 ngx_http_upstream_t 结构体
       │
       ▼
  创建连接池(keepalive 32)
       │
       ▼
  配置加载完成,开始接受请求
       │
       ▼
  请求到达 → 直接从连接池取连接 → 转发

后端地址在”编译时”(配置加载阶段)就固定了。 运行期间不再做 DNS 查询。

2.2 变量 + resolver:每次请求时才解析后端

Nginx 启动 / reload
       │
       ▼
  解析 nginx.conf
       │
       ├── 遇到 set $backend "wordpress"
       │   → 只记录"有一个变量叫 $backend,值是字符串 'wordpress'"
       │   → 不解析,不建立连接
       │
       ▼
  配置加载完成,开始接受请求
       │
       ▼
  请求到达
       │
       ▼
  进入 location / 处理
       │
       ├── 对 $backend 求值 → "wordpress"
       │
       ├── 发现是域名 → 查询 resolver 缓存
       │   ├── 缓存命中且未过期 → 使用缓存的 IP
       │   └── 缓存未命中或已过期 → 向 127.0.0.11 发起 DNS 查询
       │
       ├── 拿到 IP → 新建 TCP 连接
       │
       ├── 转发请求
       │
       ├── 接收响应
       │
       └── 关闭 TCP 连接 ← 不复用!

后端地址在”运行时”(每个请求处理阶段)才确定。


三、逐项对比

3.1 DNS 解析

维度 upstream 变量 + resolver
解析时机 启动/reload 时一次 每个请求(受 valid 缓存控制)
解析频率 一次 valid 秒至少一次
解析失败 Nginx 启动失败(快速暴露问题) 返回 502(用户看到错误页)
运行时开销 每次请求有缓存查找或 DNS 查询
# upstream 方式:启动时解析
upstream wordpress_backend {
    server wordpress:80;    # 启动时解析,失败则 nginx -t 报错
}

# 变量方式:运行时解析
resolver 127.0.0.11 valid=10s;
set $backend "wordpress";
proxy_pass http://$backend;  # 每个请求都要查缓存或做 DNS 查询

valid=10s 意味着 DNS 结果缓存 10 秒。第 11 秒的请求会重新查询。即使缓存命中,也有缓存查找的开销。

3.2 连接复用(最关键的差异)

upstream 方式:

upstream wordpress_backend {
    server wordpress:80;
    keepalive 32;          # ← 连接池:最多保持 32 个空闲长连接
}

请求处理流程:

请求 1 → 池中无空闲连接 → 新建连接 A → 处理 → 连接 A 归还池中
请求 2 → 池中有连接 A   → 复用连接 A → 处理 → 连接 A 归还池中
请求 3 → 池中有连接 A   → 复用连接 A → 处理 → 连接 A 归还池中
...
请求 100 → 复用连接 A → 处理 → 归还

100 个请求只用了 1 次 TCP 握手。

变量 + resolver 方式:

set $backend "wordpress";
proxy_pass http://$backend;
# 没有 keepalive,也无法配置

请求处理流程:

请求 1 → 新建连接 A → 处理 → 关闭连接 A
请求 2 → 新建连接 B → 处理 → 关闭连接 B
请求 3 → 新建连接 C → 处理 → 关闭连接 C
...
请求 100 → 新建连接 XX → 处理 → 关闭连接 XX

100 个请求 = 100 次 TCP 握手 + 100 次 TCP 挥手。

3.3 为什么变量方式无法 keepalive?

这不是”忘了实现”,而是架构上不可能

Nginx 的连接池(keepalive)是这样工作的:

┌─────────────────────────────────────────────────┐
│              ngx_http_upstream_t                │
│                                                 │
│  peers: [ { addr: 172.18.0.3:80 } ]             │
│                                                 │
│  keepalive 连接池:                              │
│    ┌──────────────────────────────┐             │
│    │ 空闲连接列表                  │             │
│    │  conn_1 (fd=14, idle=2s)     │             │
│    │  conn_2 (fd=15, idle=5s)     │             │
│    │  ...                         │             │
│    └──────────────────────────────┘             │
│                                                 │
│  索引键: 目标地址 (172.18.0.3:80)                │
└─────────────────────────────────────────────────┘

连接池在配置加载阶段创建,按目标地址索引。请求完成后,连接归还到对应地址的池中。

当使用变量时:

请求到达 → $backend 求值 → "wordpress" → DNS 解析 → 172.18.0.3
                                                      │
                                                      ▼
                                              这个地址在配置加载时
                                              根本不存在,没有对应的
                                              连接池结构体
                                                      │
                                                      ▼
                                              只能新建连接
                                              用完后无处归还
                                                      │
                                                      ▼
                                                  关闭连接

变量方式走的是完全不同的代码路径:

代码路径 触发条件 函数入口
upstream 路径 proxy_pass http://upstream_name; ngx_http_upstream_init() → 连接池
变量路径 proxy_pass http://$variable; ngx_http_proxy_pass_variable() → 每次新建

在 Nginx 源码中,ngx_http_upstream_keepalive_module 只挂载在 upstream 块的 postconfiguration 阶段。变量方式的请求根本不经过这个模块。

3.4 健康检查

维度 upstream 变量 + resolver
被动健康检查 max_fails=3 fail_timeout=10s ❌ 无
故障摘除 ✅ 连续失败 3 次后 10 秒内不再转发 ❌ 每次都尝试
故障恢复 ✅ 10 秒后自动重新尝试
# upstream:WordPress 容器短暂不可用时
upstream wordpress_backend {
    server wordpress:80 max_fails=3 fail_timeout=10s;
    # 连续 3 次失败 → 标记为不可用 → 10 秒内跳过 → 快速返回 502
    # 而不是让每个请求都等待超时
}

# 变量方式:没有健康检查
# 每次请求都尝试连接 → 如果容器挂了 → 每个请求都等 connect_timeout 才报错

3.5 负载均衡

维度 upstream 变量 + resolver
多后端 ✅ 支持 ❌ 不支持
负载均衡算法 round-robin / least_conn / ip_hash
权重 weight=5
# upstream 支持多后端
upstream wordpress_backend {
    server wordpress_1:80 weight=3;
    server wordpress_2:80 weight=1;
    keepalive 32;
}

# 变量方式只能指向一个目标
set $backend "wordpress";
proxy_pass http://$backend;

四、性能量化对比

以本方案为例,假设:

  • 容器内网延迟:~0.1ms
  • TCP 三次握手:~0.3ms(容器内)
  • DNS 查询(缓存未命中):~1-5ms
  • WordPress 处理时间:~50ms

4.1 单个请求的延迟

阶段 upstream 变量 + resolver
DNS 解析 0(启动时已完成) 0~5ms(缓存命中 0,未命中需查询)
TCP 握手 0(复用连接) ~0.3ms
请求处理 50ms 50ms
TCP 关闭 0(连接保留) ~0.1ms
总延迟 ~50ms 50.455.4ms

单个请求差异不大。但看并发场景:

4.2 100 QPS 下的资源消耗

指标 upstream (keepalive 32) 变量 + resolver
TCP 握手次数/秒 ~0(连接复用) 100
TCP 关闭次数/秒 ~0 100
并发连接数 ≤ 32 100(每个请求一个)
socket 创建/销毁 ~0 200 次/秒
内核开销 极低 显著(fd 分配、TCP 状态机)
DNS 查询 0 ~6 次/分钟(valid=10s)

4.3 TIME_WAIT 堆积

变量方式下,每个请求关闭连接后,主动关闭方(Nginx)会进入 TIME_WAIT 状态(默认 60 秒):

100 QPS × 60 秒 = 6000 个 TIME_WAIT 连接

这些连接占用文件描述符和内核内存。在极端情况下:

# 检查 TIME_WAIT 数量
ss -s | grep TIME-WAIT
# 或
netstat -n | grep TIME_WAIT | wc -l

upstream + keepalive 方式下,连接不关闭,TIME_WAIT 几乎为零


五、变量 + resolver 方式真正的适用场景

说了这么多缺点,那这种方式完全没有用武之地吗?不是。它有特定的适用场景:

场景 为什么用变量 + resolver
后端地址是动态的 如根据请求参数、Header、Cookie 决定转发目标
K8s / 容器编排中 Pod 频繁重建 IP 随时变化,不能用启动时固定的地址
利用 DNS 做服务发现 后端通过 DNS 注册,TTL 控制更新频率
后端数量极多且动态变化 无法在 upstream 中静态列举

但在 Docker Compose 场景中:

  • 容器名(wordpressdbweb)是固定的
  • 容器重建后,Docker DNS 会更新解析结果,但容器名不变
  • 后端数量少且确定(就 2 个)
  • 不需要动态路由

所以 upstream 是更优选择。


六、如果非要用变量方式,配置长什么样?

为了对比,这里给出完整的变量方式配置:

http {
    # DNS 解析器(Docker 内置)
    resolver 127.0.0.11 valid=10s ipv6=off;
    resolver_timeout 5s;

    # 注意:没有 upstream 块
    # 注意:没有 keepalive
    # 注意:没有健康检查

    server {
        listen 443 ssl;
        listen 443 quic reuseport;
        http2 on;
        server_name cnix.win;

        # ... TLS、安全头等配置 ...

        location / {
            # 必须用变量,不能直接写域名
            # 直接写 proxy_pass http://wordpress:80;
            # 效果等同于 upstream(启动时解析),但没有连接池
            set $backend "wordpress";
            proxy_pass http://$backend;

            proxy_http_version 1.1;
            proxy_set_header Host $host;
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_set_header X-Forwarded-Proto $scheme;

            # 这些配置仍然有效
            proxy_connect_timeout 10s;
            proxy_send_timeout 60s;
            proxy_read_timeout 60s;
        }
    }
}

注意一个常见误解:

# 这不是"变量方式",这是"直接域名方式"
proxy_pass http://wordpress:80;

# 这才是"变量方式"
set $backend "wordpress";
proxy_pass http://$backend;

直接写域名(proxy_pass http://wordpress:80)时,Nginx 在启动时解析一次,行为接近 upstream(但没有连接池和健康检查)。用变量后,才触发运行时解析。


七、三种方式的完整对比

实际上存在三种方式:

方式 配置写法 DNS 时机 连接池 健康检查
① upstream 块 proxy_pass http://upstream_name 启动时
② 直接域名 proxy_pass http://wordpress:80 启动时
③ 变量 + resolver set $b "wordpress"; proxy_pass http://$b 每次请求

方式 ② 是一个容易被忽略的中间态:启动时解析(和 ① 一样),但没有连接池(和 ③ 一样)。

本方案选择 ① 的理由:

① upstream:启动时解析 ✅ + 连接池 ✅ + 健康检查 ✅ + 负载均衡 ✅
② 直接域名:启动时解析 ✅ + 连接池 ❌ + 健康检查 ❌
③ 变量:    运行时解析 ❌ + 连接池 ❌ + 健康检查 ❌

八、验证方法

8.1 确认 upstream keepalive 生效

# 查看 Nginx 到 WordPress 的 TCP 连接
docker exec nginx ss -tnp | grep :80

# 如果 keepalive 生效,你会看到少量(≤32)ESTABLISHED 连接
# 如果每次请求都新建连接,你会看到大量连接或 TIME_WAIT

8.2 对比观察

# 终端 1:持续监控连接数
watch -n 1 "docker exec nginx ss -tn | grep :80 | wc -l"

# 终端 2:发送 100 个请求
for i in $(seq 1 100); do
  curl -s -o /dev/null https://cnix.win/
done

# upstream + keepalive:连接数稳定在 1~5
# 变量方式:连接数飙升到 100,然后慢慢下降(TIME_WAIT)

九、为什么本方案里仍然写上了 resolver 指令

9.1 Nginx 中两套独立的 DNS 解析体系

体系一:系统解析器(给 upstream 用)

  • 解析时机: 配置加载阶段(nginx -s reload 或启动时)
  • 解析方式: 系统调用 getaddrinfo()
  • DNS 来源: 容器内 /etc/resolv.conf(Docker 容器中通常已指向 127.0.0.11
  • resolver 指令是否参与:不参与
  • 解析失败后果: Nginx 启动失败,nginx -t 报错

体系二:resolver 指令(给变量用)

  • 解析时机: 每个请求处理阶段(运行时)
  • 解析方式: Nginx 内置的异步 DNS 解析器
  • DNS 来源: resolver 指令指定的地址
  • resolver 指令是否参与:这是它唯一的工作
  • 解析失败后果: 返回 502

对比表

维度 系统解析器(upstream) resolver 指令(变量)
触发场景 upstream { server domain; } proxy_pass http://$var
解析时机 启动 / reload 每个请求
DNS 来源 /etc/resolv.conf resolver 指令
是否受 resolver 指令控制
解析频率 一次 valid
失败后果 启动失败 运行时 502

在 Docker 容器中,/etc/resolv.conf 的内容通常是:

nameserver 127.0.0.11

所以 getaddrinfo("wordpress") 自然就能通过 Docker 内置 DNS 解析到容器 IP。

9.2 完整解析流程图

Nginx 启动
    │
    ├── 解析 upstream 块
    │   └── server wordpress:80
    │       └── 调用 getaddrinfo("wordpress")
    │           └── 读取 /etc/resolv.conf → 127.0.0.11
    │               └── Docker DNS 返回 172.18.0.3
    │                   └── 存入 upstream 结构体 ✅
    │
    ├── 解析 resolver 指令
    │   └── 记录:运行时 DNS = 127.0.0.11,缓存 10s
    │       └── 等待变量方式使用(本方案中未触发)
    │
    └── 开始接受请求
        │
        ├── proxy_pass http://wordpress_backend
        │   └── 直接使用启动时解析好的 IP
        │       └── 不经过 resolver ✅
        │
        └── (假设)proxy_pass http://$variable
            └── 运行时求值 → 查 resolver 缓存 → 可能触发 DNS
                └── 这里才用到 resolver 指令

9.3 本方案中 resolver 指令到底有什么用?

严格来说,在纯 upstream 方案中,resolver 指令不是必需的。

保留它的理由:

理由 说明
防御性配置 如果将来某个 location 需要用变量方式(如动态路由),不需要临时加
某些第三方模块依赖 部分 Nginx 模块在运行时需要 resolver 做 DNS 查询
配置习惯 在 Docker 环境中声明 resolver 127.0.0.11 是常见惯例
无害 不使用变量时,这条指令不会产生任何额外开销

十、总结

┌─────────────────────────────────────────────────────────────────┐
│                     请求到达 Nginx                               │
│                         │                                       │
│              ┌──────────┴──────────┐                            │
│              ▼                     ▼                            │
│     proxy_pass http://       proxy_pass http://$var             │
│     upstream_name                  │                            │
│              │                     ▼                            │
│              │              运行时求值变量                        │
│              │                     │                            │
│              │                     ▼                            │
│              │              查询 resolver 缓存                   │
│              │              (可能触发 DNS)                       │
│              │                     │                            │
│              ▼                     ▼                            │
│     从连接池取连接           新建 TCP 连接                        │
│     (可能 0 次握手)         (必须 3 次握手)                      │
│              │                     │                            │
│              ▼                     ▼                            │
│         转发请求              转发请求                            │
│              │                     │                            │
│              ▼                     ▼                            │
│         接收响应              接收响应                            │
│              │                     │                            │
│              ▼                     ▼                            │
│     连接归还池中             关闭连接                             │
│     (等待复用)              (TIME_WAIT)                         │
│                                                                 │
│  ┌─────────────────┐    ┌─────────────────┐                    │
│  │   upstream 路径 │    │   变量路径       │                    │
│  │  编译时确定后端  │    │  运行时确定后端  │                    │
│  │  连接池复用      │    │  每次新建连接    │                    │
│  │  健康检查        │    │  无健康检查      │                    │
│  │  负载均衡        │    │  无负载均衡      │                    │
│  └─────────────────┘    └─────────────────┘                    │
└─────────────────────────────────────────────────────────────────┘
维度 upstream 变量 + resolver
DNS 解析 启动时一次 每次请求
TCP 连接 复用(0 次握手) 每次新建(3 次握手)
keepalive ❌(架构上不可能)
健康检查
负载均衡
TIME_WAIT 极少 大量
配置复杂度 略高(多一个 upstream 块) 略低
适用场景 后端固定、需要高性能 后端动态、需要运行时路由

一句话:

upstream 是”先建好高速公路,车辆直接通行”;变量 + resolver 是”每辆车到了才临时修一条路,用完就拆”。对于 WordPress 博客这种后端固定的场景,没有理由选择后者。

本文内容在完整版教程 Docker WordPress 建站指南:从零到生产级部署 基础上进行讨论

正文完
 0
评论(没有评论)