zsh — anti@lateresonance:~ (微重力交互终端 v1.0)
按 `~` 或 `Esc` 展开/收起
🛸 欢迎接入 Late Resonance(迟到的共鸣 · 微重力前哨站) 交互式终端。输入 help 查看可用指令(支持 `ls`, `cat`, `status`, `speedtest`, `anti`, `jelly`)。
anti@lateresonance:~ %

[2026 Remastered · 06] 从 cd *.tar.gz 翻车到 USR2 零停机热升级:Nginx 二进制平滑演进与回滚实战(Debian 13 真机版)

📅 发布于: 2026-11-04 05:32:51 · 🔄 更新于: 2026-11-04 05:32:51 · 📖 6761 字 (约 23 分钟阅读)

🛸 站长重制导读(Anti Gravi's Remaster Note)

本文是 「来时路 · 2026 Debian 13 真机重制企划(6/12)」 的第 6 篇(半程里程碑!),对应本站 2020 年 3 月发布的 《nginx升级脚本》(CID: 32)。

2020 年的原稿是一段仅 18 行的源码升级草稿,当时大概是随手凭记忆敲进编辑器便点了发布,结果留下了整整 5 处如果在生产环境直接跑会当场翻车的 Bug:从把压缩包当目录 cd *.tar.gz、在花括号扩展 {, .bak} 逗号后多敲了一个空格,到漏掉历史编译参数、误执行 make install 以及暴力 stop && start 断流。

今天我们在 Proxmox VE 的 Debian 13 (Nginx 1.26.3) 真机上逐一复现这些经典反例,并完整走通生产级 Nginx USR2 ➔ WINCH ➔ QUIT 零停机二进制热升级。


一、 2020 原稿 (CID: 32) 五大翻车现场复盘

我们先看一眼 2020 年原稿里的原始片段:

# 2020 原稿片段(包含多处语法与运维逻辑错误,切勿在生产直接运行!):
tar -xf *.tar.gz
cd *.tar.gz
./configure --prefix=/usr/local/nginx
make
make install
cp /usr/local/nginx/sbin/nginx{, .bak}
cp ./objs/nginx /usr/local/nginx/sbin/
/usr/local/nginx/sbin/nginx -s stop
/usr/local/nginx/sbin/nginx

为什么这段短短 9 行的代码隐藏了 5 个坑?

编号 2020 原稿 (CID: 32) 写法 真实执行后果 2026 Remastered (Debian 13) 根治方案
1. cd *.tar.gz 解压后直接 cd *.tar.gz *.tar.gz 展开后依然是压缩包文件名而非解压出的目录,Bash 当场报错 Not a directory 退出 使用 tar -xzf "nginx-${VER}.tar.gz" && cd "nginx-${VER}" 精确锚定版本号
2. 花括号空格陷阱 cp .../nginx{, .bak}(逗号后多了一个空格) Bash 的花括号展开(Brace Expansion)严禁在逗号前后出现未引号包裹的空格!一旦有空格,Bash 会将其当作字面量 nginx{, 和 .bak},报错 cannot stat '.../nginx{,' 紧贴逗号不留空格:cp /usr/local/nginx/sbin/nginx{,.bak}
3. 丢失历史编译参数 裸写 ./configure --prefix=/usr/local/nginx 原先编译进 Nginx 的 --with-http_ssl_module、--with-http_v2_module、--with-stream 全部丢失,替换二进制后跑 nginx -t 直接因不认识 ssl 指令而崩溃 通过 nginx -V 2>&1 | sed -n 's/^configure arguments: //p' 自动提取并完整继承旧版编译参数
4. 误跑 make install make 之后紧接着跑了 make install 升级已有 Nginx 时严禁执行 make install,只需要编译到 make 生成 objs/nginx 二进制即可;而且原稿先 make install 覆盖完才去 cp ... .bak 备份,顺序完全颠倒 仅执行 make -j"$(nproc)",先备份原二进制,再通过 install -m 755 objs/nginx ... 原子替换
5. 暴力 stop 中断连接 nginx -s stop && nginx 线上正在传输的所有长连接、WebSocket 与大文件下载瞬间被 RST 掐断 使用 Nginx 原生 USR2 ➔ WINCH ➔ QUIT 信号状态机,双 Master 共享监听句柄,实现 100% 零丢包热升级与随时秒级回滚

Debian 13 真机复现:原稿语法 Bug 报错实录

我们在 Debian 13 (10.0.0.140) 真机上实测原稿的前两个 Bash 语法陷阱:

=== [1. REPRODUCE 2020 CID:32 BASH SYNTAX BUGS ON DEBIAN 13] ===
--- [Bug 1] Executing: cd *.tar.gz ---
bash: line 11: cd: nginx-1.26.3.tar.gz: Not a directory

--- [Bug 2] Executing: cp dummy_bin{, .bak} (Notice space after comma breaks Brace Expansion!) ---
cp: cannot stat 'dummy_bin{,': No such file or directory

--- [Fix 2] Executing: cp dummy_bin{,.bak} (No space inside braces) ---
-rw-rw-r-- 1 codex codex 0 Nov  4 12:26 dummy_bin
-rw-rw-r-- 1 codex codex 0 Nov  4 12:26 dummy_bin.bak

二、 核心原理解析:Nginx USR2 -> WINCH -> QUIT 零停机热升级是如何工作的?

很多同学知道重载配置用 nginx -s reload(本质是发 SIGHUP),但当 Nginx 二进制文件本身升级(如从 1.24 升到 1.26) 时,SIGHUP 只会让旧 Master 重新读取配置文件,并不会加载磁盘上的新二进制程序!

要想在不中断任何存量客户端连接的前提下更换二进制,Nginx 设计了一套极其精妙的 四步信号交接协议:

+-------------------------------------------------------------------------+
| [Step 1] 替换磁盘上的 /usr/sbin/nginx 二进制文件(此时内存里跑的仍是旧版) |
+------------------------------------+------------------------------------+
                                     |
                                     v  kill -USR2 <OLD_MASTER_PID>
+-------------------------------------------------------------------------+
| [Step 2] 旧 Master 将 /run/nginx.pid 重命名为 /run/nginx.pid.oldbin,    |
|          并 fork+execve 启动新二进制 Master(写入新的 /run/nginx.pid)。 |
|          此时【旧 Master + 旧 Worker】与【新 Master + 新 Worker】并存,   |
|          通过继承的 Socket 文件描述符共同接收新请求,0 毫秒断流!          |
+------------------------------------+------------------------------------+
                                     |
                                     v  kill -WINCH <OLD_MASTER_PID>
+-------------------------------------------------------------------------+
| [Step 3] 旧 Master 停止派生并通知旧 Worker 处理完存量长连接后优雅退出。  |
|          注意:此时旧 Master 自身仍留在内存中待命!                       |
|          - 若发现新版异常:向旧 Master 发 SIGHUP 即可 1 秒拉起旧 Worker 回滚|
|          - 若确认新版正常:进入 Step 4                                   |
+------------------------------------+------------------------------------+
                                     |
                                     v  kill -QUIT <OLD_MASTER_PID>
+-------------------------------------------------------------------------+
| [Step 4] 旧 Master 释放资源并删除 /run/nginx.pid.oldbin,新 Master 转正! |
+-------------------------------------------------------------------------+

三、 PVE Debian 13 真机演练:双 Master 并存与零停机交接实录

我们在 Debian 13 (10.0.0.140) 真机上,对正在运行的 Nginx (Master PID: 5260) 逐步发送 USR2、WINCH 与 QUIT 信号,用 ps -ef 和 ls -l /run/nginx.pid* 拍下每一步的真实内核进程树变化:

=== [3. ZERO-DOWNTIME BINARY HOT UPGRADE (USR2 -> WINCH -> QUIT) ON DEBIAN 13] ===
[Step 1] Initial Running Master PID: 5260
root        5260       1  0 12:15 ?        00:00:00 nginx: master process /usr/sbin/nginx -g daemon on; master_process on;
www-data    7309    5260  0 12:17 ?        00:00:00 nginx: worker process

[Step 2] Sending SIGUSR2 to Master (5260) to spawn New Master & rename pid to .oldbin...
-rw-r--r-- 1 root root 6 Nov  4 12:26 /run/nginx.pid
-rw-r--r-- 1 root root 5 Nov  4 12:15 /run/nginx.pid.oldbin
  -> Old Master (/run/nginx.pid.oldbin): 5260
  -> New Master (/run/nginx.pid):        11435
  -> Both Old & New Masters co-existing on port 80 without dropping a single packet:
root        5260       1  0 12:15 ?        00:00:00 nginx: master process /usr/sbin/nginx -g daemon on; master_process on;
www-data    7309    5260  0 12:17 ?        00:00:00 nginx: worker process
root       11435    5260  0 12:26 ?        00:00:00 nginx: master process /usr/sbin/nginx -g daemon on; master_process on;
www-data   11437   11435  0 12:26 ?        00:00:00 nginx: worker process

[Step 3] Sending SIGWINCH to Old Master (5260) to gracefully drain old workers...
root        5260       1  0 12:15 ?        00:00:00 nginx: master process /usr/sbin/nginx -g daemon on; master_process on;
root       11435    5260  0 12:26 ?        00:00:00 nginx: master process /usr/sbin/nginx -g daemon on; master_process on;
www-data   11437   11435  0 12:26 ?        00:00:00 nginx: worker process

[Step 4] Sending SIGQUIT to Old Master (5260) to retire it after verification...
-rw-r--r-- 1 root root 6 Nov  4 12:26 /run/nginx.pid
  -> Final Active Master PID: 11435
root       11435       1  0 12:26 ?        00:00:00 nginx: master process /usr/sbin/nginx -g daemon on; master_process on;
www-data   11437   11435  0 12:26 ?        00:00:00 nginx: worker process

看 [Step 2] 到 [Step 4] 的进程父子关系(PPID 列):

  • 在 [Step 2] 中,新 Master 11435 的父进程(PPID)是旧 Master 5260;
  • 在 [Step 3] 收到 SIGWINCH 后,旧 Worker 7309 优雅谢幕,只留下新 Worker 11437 干活;
  • 在 [Step 4] 收到 SIGQUIT 后,旧 Master 5260 退出并清理掉 .oldbin,新 Master 11435 的 PPID 自动由 1 (systemd) 接管收养!

四、 2026 生产级源码平滑热升级完整脚本 (upgrade_nginx_zero_downtime.sh)

如果你所在环境必须使用源码编译 Nginx(例如需要编译特定的第三方 C 模块),请使用下面这份经过严格语法校验、自动继承原编译参数并采用 USR2 -> WINCH -> QUIT 零停机切换的标准脚本:

#!/usr/bin/env bash
set -euo pipefail

NEW_VER="${1:-1.26.3}"
NGINX_BIN="/usr/local/nginx/sbin/nginx"
PID_FILE="/usr/local/nginx/logs/nginx.pid"
WORK_DIR="/usr/local/src/nginx-upgrade-${NEW_VER}"

mkdir -p "$WORK_DIR"
cd "$WORK_DIR"

# 1. 下载并校验源码包(拒绝 cd *.tar.gz 盲跳)
if [[ ! -f "nginx-${NEW_VER}.tar.gz" ]]; then
  curl -fSLO "https://nginx.org/download/nginx-${NEW_VER}.tar.gz"
fi
tar -xzf "nginx-${NEW_VER}.tar.gz"
cd "nginx-${NEW_VER}"

# 2. 自动提取旧二进制完整 configure 编译参数并执行多核编译(严禁跑 make install!)
ORIG_ARGS=$("$NGINX_BIN" -V 2>&1 | sed -n 's/^configure arguments: //p')
echo "[+] Configuring Nginx ${NEW_VER} with inherited args: ${ORIG_ARGS}"
eval "./configure ${ORIG_ARGS}"
make -j"$(nproc)"

# 3. 备份旧二进制(紧贴逗号无空格)并用新二进制测试当前配置语法
cp "${NGINX_BIN}"{,.bak."$(date +%Y%m%d_%H%M%S)"}
./objs/nginx -t

# 4. 原子替换二进制文件并触发 USR2 -> WINCH -> QUIT 零停机热升级
install -m 755 ./objs/nginx "$NGINX_BIN"
OLD_PID=$(cat "$PID_FILE")

echo "[+] Spawning new master via SIGUSR2 (Old Master: ${OLD_PID})..."
kill -USR2 "$OLD_PID"
sleep 2

if [[ ! -f "${PID_FILE}.oldbin" ]]; then
  echo "[✗] .oldbin not created! Aborting..." >&2
  exit 1
fi

echo "[+] Draining old workers via SIGWINCH..."
kill -WINCH "$OLD_PID"
sleep 3

echo "[+] Retiring old master via SIGQUIT..."
kill -QUIT "$OLD_PID"
echo "[✓] Nginx successfully hot-upgraded to: $("$NGINX_BIN" -v 2>&1)"
🛸 安缇 (Anti Gravi) · 云端伴读舱 微重力巡航中

*(小水母 Gravi-Jelly 在半空轻轻闪烁着荧光青触角,帮你盯死每一行配置与 1C512M 内存水位 🪼✨)*

前哨站内存: -- MB | 系统已用: -- / -- MB
守望灯塔: Zero-G Anchor · 在线值守
微重力前哨站 · 内存: --MB · 负载: --