技术指南
Nginx 反向代理上线:预检、切换与回滚
把 Nginx 反向代理发布拆成上游预检、配置验证、原子切换、发布身份观察与可验收回滚。
反向代理上线不是把一段配置贴进 Nginx。先证明上游在回环地址可用,再验证候选配置、原子切换入口、等待新 worker 可观察、从真实域名复查,最后演练回滚。每一层都要有预期状态、内容身份和失败退出条件。
来源覆盖组件,本文收敛上线事务
归档页面完整罗列 Nginx、Apache、Caddy、PHP-FPM、反向代理、证书和常见故障,可保留的是配置检查、回环探测、日志与重载顺序。它同时给出固定并发参数、通用进程数、递归权限修改和必得 A+ 等过宽结论。VPScope 不复用这些保证,而把范围收敛为一条可回滚的 Nginx 反向代理发布路径。
六层证据不要合并成一个“能打开”
| 层 | 发布前问题 | 验收证据 |
|---|---|---|
| 上游进程 | 应用是否已经 ready | 回环请求的状态、内容身份与耗时 |
| Nginx 配置 | 语法、文件和组合是否有效 | 切换后的完整 `nginx -t` 通过 |
| 监听与虚拟主机 | 地址、端口、Host 是否命中预期 server | `ss` 与显式 Host 请求 |
| 代理边界 | Host、客户端地址和 scheme 如何传递 | 应用只信任来自已知代理的头 |
| DNS 与 TLS | A/AAAA、80/443、证书名和链是否一致 | 外部解析、握手与真实域名请求 |
| 业务入口 | 关键路径是否返回正确版本 | 完整观察窗的状态、内容、延迟和错误率 |
先保存当前版本与回环基线
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` 会漏掉路由、静态资源和依赖错误;至少再选择一个代表性只读业务路径,并在切换前写下预期正文或响应头。
候选配置只声明需要的代理语义
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
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沿同一路径逐层验收
# 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 命令。
回滚旧目标,也要重新验收
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 入口通过。
- 状态码、尾延迟、错误日志和依赖在完整观察窗内未越界。
- 回滚恢复旧发布身份并重新验收;端口、进程和临时证据已按计划清理。