[2026 Remastered · 06] 从 cd *.tar.gz 翻车到 USR2 零停机热升级:Nginx 二进制平滑演进与回滚实战(Debian 13 真机版)
🛸 站长重制导读(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) 真机上逐一复现这些经典反例,并完整走通生产级 NginxUSR2 ➔ 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]中,新 Master11435的父进程(PPID)是旧 Master5260; - 在
[Step 3]收到SIGWINCH后,旧 Worker7309优雅谢幕,只留下新 Worker11437干活; - 在
[Step 4]收到SIGQUIT后,旧 Master5260退出并清理掉.oldbin,新 Master11435的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)"