Nginx 热升级:不停机替换二进制文件
Nginx 升级通常要重启服务,正在处理的连接会跟着断掉。热升级换了一种做法:USR2 信号让新旧两个 master 进程并行,新版本接新流量,旧版本把手头请求处理完再退场,全程不停机。OpenResty 的热升级流程与此完全相同。这篇把整条信号流在容器里实跑一遍——升级、验证、回退,外加一个让 USR2 静默失效的真实坑。
实验环境
- Docker 容器
nginx:1.28-alpine(2026-09 实测):容器里另起一个独立实例(-p指定前缀、独立端口),运行中的旧版本是 nginx/1.28.3。 - 新版本二进制是源码编译的 nginx/1.30.4(
./configure --with-http_ssl_module && make的产物objs/nginx)——这是一次真实的跨版本二进制替换,不是同版本复制,信号流与生产升级一字不差。 - 原文要求照旧成立:新旧二进制的 configure 参数、依赖库版本、安装路径必须一致,缺了库的”新版本”会在运行期才暴露问题。
核心机制一句话
USR2 的本质是:旧 master 把 nginx.pid 改名为 nginx.pid.oldbin,再 fork 并 exec 一份新二进制——新 master 继承旧 master 的监听 socket(不重新 bind),旧 master 继续处理已建立的连接。之后用 WINCH 让旧 worker 优雅退场,用 QUIT 让旧 master 退场;任何一步出问题,信号反向再走一遍就回到旧版本。
第一步:备份并替换二进制
1 | cp /usr/sbin/nginx /srv/nginx1/nginx.old # 备份旧二进制(1.28.3) |
注:源码默认安装的 Nginx 二进制在
/usr/local/nginx/sbin/nginx,路径不同、动作相同——备份旧文件、新文件顶到原路径。运行中的 master 不受覆盖影响,它用的是已加载进内存的代码。
验证点:nginx -v 应显示新版本号(实测 1.30.4);nginx.old -v 显示旧版本号(1.28.3)。两个文件都在,回退才有入口。
第二步:USR2,拉起新 master
先记下旧 master 的 PID(cat /srv/nginx1/logs/nginx.pid,实验环境为 2729),然后:
1 | kill -USR2 2729 |
验证点:ps 应看到两个 master 进程并存;ls /srv/nginx1/logs/ 应出现 nginx.pid.oldbin——旧 master 的 pid 文件被改名,新 master 占用 nginx.pid:

PID 2729(旧)与 PID 2752(新,ppid=2729)同时存在,.oldbin 后缀出现,二进制切换完成了一半。此时新旧 master 各带各的 worker,都在服务。
第三步:WINCH,旧 worker 优雅退场
1 | kill -WINCH 8 |
验证点:ps 里旧 master(8)名下的 worker 进程消失——它们处理完手头请求后正常退出(日志里是 worker process exited with code 0);旧 master 本体保留,它是回退的唯一入口。error.log 里对应的是旧 worker 逐个优雅退出,没有任何异常码。
第四步:验证新版本,QUIT 送走旧 master
新版本跑一会儿,确认业务正常(curl -s http://127.0.0.1/ 返回正常页面),然后:
1 | kill -QUIT 2729 |
验证点:旧 master 进程消失,新 master(2752)被 init 收养(ppid 变 1),nginx.pid.oldbin 消失,服务照常响应:

到这里升级闭环。整个过程中客户端的连接没有断过——旧 worker 是把手头请求处理完才退的。
升级失败回退:反向再走一遍
新版本有问题时的回退思路完全对称:把旧二进制换回来(cp nginx.old /usr/sbin/nginx),对新 master 再发一轮 USR2/WINCH/QUIT——用旧二进制拉起的 master 会接管流量:
1 | cp /srv/nginx1/nginx.old /usr/sbin/nginx # 换回 1.28.3 |
验证点:nginx -v 回到旧版本号、页面正常响应、/var/run/nginx.pid 只剩一个。实测回退后服务照常,pid 文件恢复正常命名:

注意:回退时若旧 master(带 .oldbin pid 的那个)还在,直接 kill -HUP 旧masterPID 重新拉起旧 worker、再关掉新 master 也可以——两条路都通,前提都是旧二进制备份还在。
错误写法对比:USR2 悄悄失效的形态
实测中踩到一个环境相关的坑,值得写进手册:master 若以相对路径启动(比如裸命令 nginx,或某些脚本里 cd 后再启动),USR2 时它 exec 新二进制用的也是相对路径,exec 找不到文件直接失败:

表现为 USR2 后新旧 master 并不会并存、pid 文件也没有 .oldbin,服务还在正常响应——不翻 error.log 根本看不出失败。规避方法:启动 Nginx 永远用绝对路径(systemd 的 ExecStart=/usr/sbin/nginx 天然满足),发 USR2 前先确认 nginx.pid.oldbin 是否如期出现。
注意事项
- configure 参数保持一致:新版本编译时的模块、依赖库、安装路径要和旧版本对齐,缺了模块的热升级等于把半成品换上线。
- PID 别认错:USR2 之后有两个 master,操作前
cat一下两个 pid 文件确认谁是谁;对错误的进程发 QUIT 会把新版本直接送走。 - 旧 master 是回退入口:WINCH 只关 worker 不关 master,确认无误前别急着 QUIT 它;二进制备份
nginx.old同理,回退流程全靠它。 - execve 前置检查:以绝对路径启动、发 USR2 前用
nginx -t验证新二进制能正确解析当前配置,两步都不做就发信号是事故之源。
小结
回到开头的问题:升级能不能不断连接——实测下来答案是能,靠的是三个信号的分工:USR2 起新 master 并继承监听,WINCH 让旧 worker 干完活退场,QUIT 送走旧 master 收尾;出问题时把二进制换回备份、信号反向再走一遍即可回退。记住那两处容易翻车的细节——绝对路径启动、.oldbin 出现才算切换成功——这套不停机升级就能放心进生产手册。
本文由作者 2020-2024 年间的 CSDN 博客文章重构而来,原发布于 CSDN。