技术指南

Nginx 反向代理上线:预检、切换与回滚

把 Nginx 反向代理发布拆成上游预检、配置验证、原子切换、发布身份观察与可验收回滚。

反向代理上线不是把一段配置贴进 Nginx。先证明上游在回环地址可用,再验证候选配置、原子切换入口、等待新 worker 可观察、从真实域名复查,最后演练回滚。每一层都要有预期状态、内容身份和失败退出条件。

来源覆盖组件,本文收敛上线事务

归档页面完整罗列 Nginx、Apache、Caddy、PHP-FPM、反向代理、证书和常见故障,可保留的是配置检查、回环探测、日志与重载顺序。它同时给出固定并发参数、通用进程数、递归权限修改和必得 A+ 等过宽结论。VPScope 不复用这些保证,而把范围收敛为一条可回滚的 Nginx 反向代理发布路径。

六层证据不要合并成一个“能打开”

发布前问题验收证据
上游进程应用是否已经 ready回环请求的状态、内容身份与耗时
Nginx 配置语法、文件和组合是否有效切换后的完整 `nginx -t` 通过
监听与虚拟主机地址、端口、Host 是否命中预期 server`ss` 与显式 Host 请求
代理边界Host、客户端地址和 scheme 如何传递应用只信任来自已知代理的头
DNS 与 TLSA/AAAA、80/443、证书名和链是否一致外部解析、握手与真实域名请求
业务入口关键路径是否返回正确版本完整观察窗的状态、内容、延迟和错误率

先保存当前版本与回环基线

`nginx -T` 可能包含内部域名、路径和上游地址;证据目录为 0700,分享前逐项脱敏
stamp=$(date --utc +%Y%m%dT%H%M%SZ)
umask 077
mkdir "nginx-rollout-$stamp"

sudo nginx -T >"nginx-rollout-$stamp/nginx-T.txt" 2>&1
sudo systemctl show nginx -p ActiveState -p SubState -p MainPID \
  >"nginx-rollout-$stamp/service.txt"
ss -ltnp >"nginx-rollout-$stamp/listeners.txt"
curl --fail --silent --show-error --max-time 3 \
  http://127.0.0.1:3000/health \
  >"nginx-rollout-$stamp/upstream-health.txt"

先从应用发布物或响应头取得不可变发布身份,例如 Git commit、镜像摘要或构建 ID。仅验证 `/health` 会漏掉路由、静态资源和依赖错误;至少再选择一个代表性只读业务路径,并在切换前写下预期正文或响应头。

候选配置只声明需要的代理语义

上游只监听回环地址;超时来自应用契约,WebSocket、流式响应和大上传需要单独设计
server {
    listen 80;
    listen [::]:80;
    server_name app.example.com;

    location = /health {
        proxy_pass http://127.0.0.1:3000/health;
        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 2s;
        proxy_read_timeout 5s;
    }

    location / {
        proxy_pass http://127.0.0.1:3000;
        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 3s;
        proxy_read_timeout 30s;
    }
}

Nginx 默认会重定义代理请求的 Host 与 Connection;示例显式保留外部 Host,并追加客户端地址。应用不能无条件信任任意客户端提交的 `X-Forwarded-*`,应只在请求确实来自受控代理时使用这些字段。不要为了“通用”而复制 WebSocket、缓存、缓冲或限流片段。

用原子链接切换,完整测试后才 reload

发行版目录可能不同;先用 `nginx -T` 确认真实 include 路径,候选和链接必须位于同一受控文件系统
set -euo pipefail
available=/etc/nginx/sites-available
enabled=/etc/nginx/sites-enabled/app.conf
candidate=$available/app-v2.conf
old_target=$(readlink -f "$enabled")

test -r "$candidate"
test -r "$old_target"
sudo nginx -t

sudo ln -sfn "$candidate" "$enabled.next"
sudo mv -Tf "$enabled.next" "$enabled"
if ! sudo nginx -t; then
  sudo ln -sfn "$old_target" "$enabled.next"
  sudo mv -Tf "$enabled.next" "$enabled"
  sudo nginx -t
  exit 1
fi

sudo systemctl reload nginx

沿同一路径逐层验收

生产验收还要从目标用户网络请求;日志可能含 IP、查询串和内部地址,外发前脱敏
# 1. 上游:不经过 Nginx
curl --fail --silent --show-error --max-time 3 \
  http://127.0.0.1:3000/health

# 2. 本机入口:固定 Host,确认虚拟主机与代理
curl --fail --silent --show-error --max-time 5 \
  --header 'Host: app.example.com' http://127.0.0.1/health

# 3. 真实 TLS 入口:把 PUBLIC_IP 换成本机公网地址
curl --fail --silent --show-error --max-time 10 \
  --resolve app.example.com:443:PUBLIC_IP \
  --dump-header - https://app.example.com/health

# 4. 同时观察服务、状态码和上游错误
systemctl is-active nginx
journalctl -u nginx --since '-5 minutes' --no-pager
sudo tail -n 100 /var/log/nginx/error.log

`--resolve` 只覆盖本次 curl 的域名到地址映射,适合 DNS 切换前验证证书名和目标主机。它不能证明公共 DNS 已传播,也不能替代不同地区、IPv4/IPv6 和真实客户端的检查。

证书签发有独立的网络前提

HTTP-01 验证只在公网 80 端口进行。申请前确认域名的 A/AAAA 指向当前入口、80 端口从互联网可达、挑战路径没有被认证或错误重写,并保留 443 的应用验收。不能开放 80 时应评估 DNS-01 或支持 TLS-ALPN-01 的客户端,而不是把任意高端口写进 HTTP-01 命令。

回滚旧目标,也要重新验收

`old_target` 必须在发布开始时保存到事故记录;不要凭文件名猜测旧版本
set -euo pipefail
enabled=/etc/nginx/sites-enabled/app.conf
old_target=/etc/nginx/sites-available/app-v1.conf  # 换成发布前记录的绝对路径
test -r "$old_target"

sudo ln -sfn "$old_target" "$enabled.next"
sudo mv -Tf "$enabled.next" "$enabled"
sudo nginx -t
sudo systemctl reload nginx

# 在有界窗口内等待旧 release ID,再复查真实入口、日志和错误率
curl --fail --silent --show-error --max-time 5 \
  --header 'Host: app.example.com' http://127.0.0.1/health

回滚触发应在发布前量化,例如连续五分钟 5xx 超过门槛、p95 延迟越界、关键路径内容身份错误或依赖失败。回滚后仍需等旧 worker 可观察并复查业务;只看到 reload 成功不是恢复证据。

隔离演练验证了拒绝、切换与回滚

VPScope 用 Ubuntu Nginx 1.24.0 在一次性前缀中启动两个 loopback 上游。初始入口在 127.0.0.1:18190 返回 v1;语法错误候选以退出码 1 被 `nginx -t` 拒绝,运行中的 v1 继续响应。通过测试的候选经 reload 后返回 v2,再把旧配置恢复并 reload,入口重新返回 v1。

演练第一次用单次请求判断 reload,仍命中正在排空的旧 worker;改为固定 2.5 秒上限内轮询发布身份后通过。第一次发送 graceful quit 后立即探测,监听也仍短暂存在;加入有界关闭等待后,入口和两个上游端口全部关闭,一次性目录删除。这些结果验证事务顺序,不证明公网 DNS、TLS、systemd unit、生产负载或外部网络。

按失败层决定动作

观察优先核对不要先做
`nginx -t` 失败错误行、include、证书/日志文件与权限reload 或重启
回环上游失败应用进程、监听地址、就绪与依赖改 DNS 或证书
本机 Host 请求错站listen、server_name、default server扩大文件权限
入口 502/504上游地址、连接拒绝、超时和应用日志无限放大超时
本机通过、外部失败防火墙、云安全组、NAT、A/AAAA 与路径反复 reload
TLS 名称或链错误SNI、证书 SAN、链与实际目标 IP关闭证书验证

发布完成的可复查清单

  • 旧配置目标、发布身份、回滚命令和触发门槛在变更前记录。
  • 上游仅在预期地址监听,回环健康和代表性业务路径通过。
  • 当前基线和切换后的完整有效配置均通过测试,失败候选没有 reload。
  • 入口链接使用受控的原子替换,完整测试失败时已自动恢复旧目标。
  • reload 后在有界窗口内观察到新发布身份,不用一次请求判定完成。
  • 显式 Host、真实域名、目标用户网络和需要的 IPv4/IPv6 路径均通过。
  • 代理头的信任边界已在应用侧限制,未复制无关的缓存、WebSocket 或调优参数。
  • HTTP-01 的 DNS 与公网 80 前提已证明,证书名、链、续期演练和 443 入口通过。
  • 状态码、尾延迟、错误日志和依赖在完整观察窗内未越界。
  • 回滚恢复旧发布身份并重新验收;端口、进程和临时证据已按计划清理。

返回知识库