Nginx 二进制热升级实战:USR2 平滑换版本、WINCH 收尾与 QUIT 回滚窗口

升级 Nginx 为什么不能简单 restart

给网站升级 Nginx 版本,大多数人的第一反应是 systemctl restart nginx。这在个人小站上"看起来"没问题,但 restat 的本质是——把旧进程整个杀掉,再启动新进程。在这个瞬间:正在处理的请求被强制中断,用户的连接被重置,正在下载大文件的会话直接断掉,短则几百毫秒、长则几秒的 502。对流量不大的站,你可能"运气好"没被发现;但只要有人在传文件或页面正好在加载,他一定会看到错误。

更麻烦的是"升级到一半翻车"。如果新版 Nginx 配置有语法错误、或者和现有模块不兼容,restart 之后旧进程已经没了,新进程起不来,网站彻底挂掉,你连回滚的缓冲时间都没有。

Nginx 提供了两套更专业的做法来解决这个问题:

优雅重载(nginx -s reload / systemctl reload nginx)——只重新读取配置,旧 worker 进程会把手上的请求处理完再退出,新请求由新 worker 接管。它不中断连接,用于日常改配置。

二进制热升级(USR2 信号)——连 Nginx 程序本身都换了(换了可执行文件),但不中断服务。新老两代 master + worker 短暂共存,老 worker 把存量请求处理完再退出。用于真正升级 Nginx 版本。

两者的区别很关键:reload 换的是配置(可执行文件没变),USR2 热升级换的是二进制程序。很多人以为升级版本也能用 reload,结果发现版本号没变——因为 reload 只是重读配置,用的还是磁盘上原来那个进程镜像。本文把这两种操作都讲透,重点是二进制热升级这个大多数人没做过、但早晚要用到的操作。

先讲清楚信号:Nginx 的 master/worker 模型

要理解热升级,必须先理解信号怎么控制 Nginx。Nginx 启动后有一个 master 进程和若干 worker 进程。master 不处理请求,只负责管理 worker、监听端口、读配置。所有控制都是"向 master 发信号"。

# 查看 nginx 进程结构
ps -ef | grep nginx | grep -v grep
# 典型输出:
# root  1234     1  ...  nginx: master process /usr/sbin/nginx
# www   1235  1234  ...  nginx: worker process
# www   1236  1234  ...  nginx: worker process

Nginx master 支持的标准信号:

# 用 nginx -s 命令等价于发信号
nginx -s stop     # 立即停止(相当于 TERM,粗暴)
nginx -s quit     # 优雅停止(相当于 QUIT,处理完存量请求再退)
nginx -s reload   # 重载配置(相当于 HUP)
nginx -s reopen   # 重新打开日志文件(相当于 USR1)

# 热升级专用(nginx -s 没有封装,要 kill 直接发)
kill -USR2 $(cat /run/nginx.pid)   # 启动新版本进程
kill -WINCH 老master_pid           # 优雅关停老 worker
kill -QUIT 老master_pid            # 关掉老 master

记住这个模型:USR2 让老 master 拉起一套全新的 master+worker(用磁盘上的新二进制),两套并存;WINCH 让老 worker 优雅退出;QUIT 让老 master 彻底退出。热升级就是按顺序发这三个信号。

日常改配置:优雅重载的正确姿势

先把这个最常用的操作说清楚,因为它有个隐藏坑。nginx -s reload 在配置有语法错误时,不会让新的配置生效(正确),但它会向 master 发 HUP,master 重读配置失败后继续用旧配置运行——也就是说,你改的配置没生效,但网站也没挂。这看着安全,实际很坑:你以为改好了,其实没有。

# 正确流程:先测语法,再 reload
nginx -t
# 输出 nginx: configuration file /etc/nginx/nginx.conf test is successful 才能继续

systemctl reload nginx
# 或 nginx -s reload

# 验证:确认 worker 进程 PID 变了 = 重载生效
ps -ef | grep "nginx: worker" | grep -v grep

要特别小心 reload 之后"端口还在但访问异常"的情况。reload 时,新 worker 会继承监听的 socket,老 worker 处理完手上的请求后退出。如果某个 worker 卡在一个长连接上(比如一个大的下载或长轮询),它可能一直不退出,新旧 worker 并存。这时你就有了两批 worker,占用双倍资源。用 ps 看 worker 数量就能发现异常,必要时对卡死的老 worker 单独发 QUIT。

# 看看是不是有 worker 一直不退(老进程启动时间明显更早)
ps -eo pid,lstart,cmd | grep "nginx: worker" | grep -v grep

正式升级:二进制热升级全流程

假设你编译或安装了一个新版本的 Nginx,要平滑替换正在运行的旧版。整个过程最忌讳的就是直接覆盖可执行文件——因为老进程正在用它。正确做法是先把二进制换到新文件,再发信号让 master 用新文件启动新一代。

以从源码编译为例,假设旧版在 /usr/sbin/nginx,新版编到了 /usr/local/nginx/sbin/nginx:

# 第 0 步:备份旧二进制、记录当前 master pid
cp /usr/sbin/nginx /usr/sbin/nginx.old
OLD_PID=$(cat /run/nginx.pid)
echo "old master pid = $OLD_PID"

# 第 1 步:用新二进制替换 /usr/sbin/nginx(老进程已加载到内存,覆盖文件不影响它)
# 确认新二进制和旧的是同类编译参数(模块要一致,尤其 --with-*)
/usr/local/nginx/sbin/nginx -V   # 新版的编译参数
nginx -V                          # 旧版的编译参数,对比模块差异

# 先用新二进制测配置,确认它认得现有配置
/usr/local/nginx/sbin/nginx -t

# 替换文件
cp /usr/local/nginx/sbin/nginx /usr/sbin/nginx
chmod +x /usr/sbin/nginx

# 第 2 步:发 USR2,老 master 会用新二进制拉起一套新的 master+worker
kill -USR2 $OLD_PID
# 稍等 1~2 秒,观察新老两代并存
sleep 2
ps -ef | grep nginx | grep -v grep
# 应看到两对 master+worker,新 master 的二进制指向新的

发完 USR2 后,Nginx 的行为是:老 master 把 pid 文件重命名为 nginx.pid.oldbin,然后以新二进制启动一个新 master,新 master 自己写 nginx.pid。此刻两套进程并存,端口是"共享"监听的(SO_REUSEPORT 或 fd 继承),所以新老 worker 都能接请求,但新连接主要由新 worker 处理。

这是唯一能回滚的窗口期。如果发现新版有问题,立刻发信号回退,而不是继续往下走:

# 回滚:让新 master 退出,老 master 重新接管
NEW_PID=$(cat /run/nginx.pid)
kill -QUIT $NEW_PID          # 新 master 优雅退出
# 老 master 会监听 HUP,重读配置并重新 spawn worker
kill -HUP $(cat /run/nginx.pid.oldbin)

# 确认只剩老一代在跑
ps -ef | grep nginx | grep -v grep

如果新版验证 OK、运行正常,就完成升级的最后两步——让老一代彻底退场:

# 第 3 步:让老 worker 优雅退出(处理完存量请求再退)
kill -WINCH $OLD_PID
# 此时老 master 还在,但不再有老 worker

# 观察一会儿,确认没有报错、流量正常
journalctl -u nginx -n 50 --no-pager
curl -sI https://example.com | head -1

# 第 4 步:确认无误后,彻底关掉老 master
kill -QUIT $OLD_PID

# 验证最终状态:只应剩一套进程,版本是新版
ps -ef | grep nginx | grep -v grep
nginx -v   # 看版本号

顺序记牢:USR2(起新)→ 验证 → WINCH(老 worker 退)→ 验证 → QUIT(老 master 退)。任何一步发现问题,都还来得及用 QUIT 新 master + HUP 老 master 回滚。

systemd 管理下的热升级

现在绝大多数发行版用 systemd 管理 Nginx。systemctl reload nginx 实际是向 master 发 HUP,没问题。但热升级用到的 USR2 信号,systemd 默认是不转发的,你需要用 kill 直接对进程发(进程还在,systemd 不会干预),或者用 systemd 的 KillSignal(较新版本支持)。最稳妥的方式还是直接 kill -USR2 $(cat /run/nginx.pid)。

# systemd 环境下进程 pid 也可能来自 systemctl show
systemctl show nginx -p MainPID
# 或者用 pid 文件
cat /run/nginx.pid

# 注意:USR2 之后 pid 文件会变,旧 master 的 pid 在 nginx.pid.oldbin 里
cat /run/nginx.pid.oldbin

还有一个 systemd 特有的坑:如果你的 unit 里设了 PIDFile=/run/nginx.pid,USR2 把 pid 文件重命名成 .oldbin 再让新 master 写回 nginx.pid,可能出现一段时间 pid 文件指向新 master、而 systemd 记录的是老 master 的 MainPID。等你 QUIT 掉老 master 后,systemd 可能误以为服务停了而触发重启。因此做热升级时建议临时停用 unit 的自动重启(Restart=no),或升级完立即 systemctl daemon-reload 让 systemd 重新对齐状态。

预防性检查清单:升级前必须确认的事

热升级翻车,八成是因为"新版和旧版的编译模块不一致",或者"配置里有新版本不认的指令"。发 USR2 之前,务必做这几项检查,全部通过再动手:

# 1. 模块对齐:把两版的 --with-xxx / --add-module 列表对比
nginx -V 2>&1 | tr ' ' '\n' | grep -E 'with|add-module' | sort > /tmp/old.mods
/usr/local/nginx/sbin/nginx -V 2>&1 | tr ' ' '\n' | grep -E 'with|add-module' | sort > /tmp/new.mods
diff /tmp/old.mods /tmp/new.mods
# 有差异要评估:新版少了某个模块,会导致相关配置失效

# 2. 配置兼容:用新二进制测当前配置
/usr/local/nginx/sbin/nginx -t

# 3. 证书与依赖路径:新二进制是否指向同样的库、同样的日志路径
ldd /usr/local/nginx/sbin/nginx    # 看动态库依赖是否都找得到

# 4. 磁盘空间:热升级期间新老日志/临时文件会并存,留足空间
df -h /var/log /var/cache/nginx

另外强烈建议:先在测试机上完整走一遍,把命令抄下来对一遍,再去生产。低配 VPS 上"进程起不来"往往不是配置问题,而是内存不够——新老两套 worker 并存时瞬时内存翻倍,1GB 的机器可能直接 OOM。所以热升级最好在业务低峰、内存有余量时做。

常见故障速查

发了 USR2 没有新进程出现:新二进制不存在或不可执行、路径写错,或配置测不过。USR2 之后立刻看 error.log,会写明失败原因。
nginx -v 还是旧版本号:你发的是 reload(HUP)而不是 USR2,reload 只重读配置不换二进制。确认发的是 kill -USR2。
新老 worker 并存很久不退出:老 worker 卡在长连接上。用 ps -eo pid,lstart 找出老进程,确认无流量后 kill -QUIT 单独处理,或先 WINCH 老 master 强制让老 worker 退。
QUIT 老 master 后 systemd 把服务也停了:MainPID 错乱,systemctl reset-failed nginx 并 daemon-reload 重新对齐,必要时临时关掉 Restart=。
升级后内存暴涨甚至 OOM:新老两套 worker 并存导致瞬时翻倍,属于预期现象但低配机扛不住。升级应选在低峰,或先缩 worker 数量再升级。

FAQ

Q:reload 和热升级到底该用哪个?
A:改配置用 reload(HUP);换 Nginx 程序版本用热升级(USR2)。日常 99% 的操作都是 reload,热升级只在升级版本时才用。

Q:直接 systemctl restart 真的不行吗?
A:能"完成"升级,但会中断所有进行中的连接,且失败时没有回滚窗口。个人小站半夜重启可能没事,但只要有过用户在传文件或正在下单,就是事故。养成用 reload/热升级的习惯,成本很低。

Q:热升级期间用户完全无感吗?
A:理论上存量连接由老 worker 处理完,新连接给新 worker,切换无感知。实际上极少量长连接(WebSocket、大文件下载)在新老交替时可能有抖动,建议低峰操作。

Q:nginx.pid.oldbin 是什么?能删吗?
A:热升级时老 master 的 pid 会被存到这里,是回滚的依据。热升级完成、确认新版稳定前不要删。彻底升级完之后它会被清理,也可以手动删。

Q:Docker 里的 Nginx 怎么热升级?
A:容器里热升级很别扭(进程模型和镜像绑定),通常直接换镜像重建容器,靠"滚动/双容器切换"来做到不中断,而不是容器内发信号。这点要注意,别在容器里硬套热升级流程。

Last modification:October 2nd, 2026 at 01:26 pm

Leave a Comment