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
2
cp /usr/sbin/nginx /srv/nginx1/nginx.old           # 备份旧二进制(1.28.3)
cp /tmp/nginx-1.30.4/objs/nginx /usr/sbin/nginx # 新版编译产物就位(1.30.4)

注:源码默认安装的 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:

配图1

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 消失,服务照常响应:

配图2

到这里升级闭环。整个过程中客户端的连接没有断过——旧 worker 是把手头请求处理完才退的。

升级失败回退:反向再走一遍

新版本有问题时的回退思路完全对称:把旧二进制换回来(cp nginx.old /usr/sbin/nginx),对新 master 再发一轮 USR2/WINCH/QUIT——用旧二进制拉起的 master 会接管流量:

1
2
3
4
cp /srv/nginx1/nginx.old /usr/sbin/nginx   # 换回 1.28.3
kill -USR2 2752 # 用旧二进制拉起旧版本 master
kill -WINCH 2752 # 关掉"新版"的 worker
kill -QUIT 2752 # 送走"新版" master

验证点:nginx -v 回到旧版本号、页面正常响应、/var/run/nginx.pid 只剩一个。实测回退后服务照常,pid 文件恢复正常命名:

配图3

注意:回退时若旧 master(带 .oldbin pid 的那个)还在,直接 kill -HUP 旧masterPID 重新拉起旧 worker、再关掉新 master 也可以——两条路都通,前提都是旧二进制备份还在。

错误写法对比:USR2 悄悄失效的形态

实测中踩到一个环境相关的坑,值得写进手册:master 若以相对路径启动(比如裸命令 nginx,或某些脚本里 cd 后再启动),USR2 时它 exec 新二进制用的也是相对路径,exec 找不到文件直接失败:

配图4

表现为 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。