线上业务正跑着,安全公告突然说要升级nginx版本,不然有漏洞风险。这时候要是直接nginx -s stop再启动,所有在线的连接瞬间全断,正在上传文件的用户、正在走的支付请求,全都得重来一遍。我遇到这种情况不止一次,所以热升级(也叫平滑升级)是我在CentOS 7上处理nginx版本更新时最常用的方式。整个过程不中断服务、不丢连接,几分钟内把二进制从旧版本换到新版本,老连接处理完自然退出,新连接直接由新进程接管。这篇内容就是完整记录我在CentOS 7上做nginx热升级的步骤、背后逻辑和踩过的坑,给要做同样操作的运维、后端开发一个可以直接照着走的参考。
1. 什么情况下该用热升级,为什么“先停再启”行不通
先说清楚一个概念:热升级解决的是“nginx版本要变,但业务不能断”的问题。注意,这里说的是二进制版本要变,不是配置要变。很多人会把nginx -s reload当成升级手段,但实际上reload只是重新加载配置文件,运行中的nginx代码版本根本不会变化。要换版本,常规做法是停掉服务、替换二进制、再启动,但这个操作在线上场景里往往不现实。
1.1 需要热升级的典型场景
- 安全漏洞修复:nginx官方或发行版仓库发布安全公告后,需要尽快升级到修复版本。这种需求通常很急,但业务又不能停。
- 新增功能或模块:比如要启用HTTP/2、gRPC支持、新的负载均衡策略,或者要加入公司自研的第三方模块,这些往往要求nginx主版本升级。
- 性能优化或老版本兼容问题:某些版本在高并发下存在性能瓶颈,或者和上游组件有兼容性问题,需要升级解决。
- 第三方模块适配:自研模块或商业模块要求nginx版本必须大于某个版本。
CentOS 7上这个问题尤其明显。系统自带的nginx版本往往比较老,yum源里的版本也未必是最新的,很多情况下需要手动编译新版。而线上服务挂着大量的长连接、keepalive连接、WebSocket连接,一旦重启nginx,这些连接全部断裂。客户端重连是一回事,更麻烦的是依赖nginx做反向代理的内部系统,上游连接一断,链路抖动会传导到整个业务。
1.2 热升级和“先停再启”的本质差异
普通重启的过程是:停止进程→释放端口→启动新进程→重新监听端口。在这段间隙里,端口没有进程监听,所有请求直接失败。
热升级的过程则是:旧master进程收到信号后,用磁盘上的新版本二进制再启动一个master进程,新master重新绑定监听端口(因为存在SO_REUSEPORT或者监听socket由master继承,实际上新老进程可以同时监听同一组端口),新连接由新worker处理,旧worker继续处理已经建立的连接,等旧worker处理完手里的请求后优雅退出,最后旧master退出。整个过程没有端口空窗期,没有连接被强制断开。
| 升级方式 | 二进制版本是否变化 | 已有连接是否中断 | 是否需要停机窗口 | 操作复杂度 |
|---|---|---|---|---|
| nginx -s reload | 否 | 不中断 | 不需要 | 低 |
| stop + start | 是 | 全部中断 | 需要 | 低 |
| kill -TERM + start | 是 | 全部中断 | 需要 | 低 |
| 热升级(USR2信号流程) | 是 | 不中断 | 不需要 | 中 |
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 为什么能不断线:master-worker进程模型和信号交接逻辑
热升级能成立,底层靠的是nginx的进程模型和Unix信号机制。不理解这部分,操作起来就是背命令,一旦命令执行结果和预期不符,完全不知道从哪里排查。
2.1 master-worker架构
nginx启动后默认有两个类型的进程:
- master进程:负责读取配置、绑定监听端口、管理worker进程的生命周期。不直接处理请求。
- worker进程:实际处理请求的进程,每个worker是一个单线程事件循环。连接接受、HTTP解析、日志写入都由worker完成。
在热升级场景里,master进程最关键的能力是:它可以在运行期间,根据信号指令,用磁盘上当前存在的nginx二进制文件再启动一个全新的master进程。这就是为什么我们可以先用新二进制覆盖旧二进制,然后通过信号让旧master“生出”一个新master来。
2.2 热升级用到的几个信号
nginx的信号处理机制非常成熟,以下是热升级流程中涉及的关键信号:
| 信号 | 作用 | 使用场景 |
|---|---|---|
| USR2 | 旧master启动新的master进程,新旧master共存 | 热升级第一步 |
| WINCH | 告知master让其worker进程优雅退出(处理完当前请求后退出),master本身不退出 | 热升级第二步:让旧worker退场 |
| QUIT | 优雅关闭master进程(会先等它的worker全部退出) | 热升级最后一步:关掉旧master |
| HUP | 重载配置,重新生成worker进程 | 回滚时让旧master拉起新worker |
| TERM/INT | 快速关闭(不等待) | 一般不建议在升级流程使用 |
2.3 整个交接过程是怎样的
一次标准的nginx热升级流程,信号发送顺序是这样的:
- 向旧master发送
USR2信号,旧master用新二进制启动新master和新worker。 - 新master和新worker开始接管新连接。
- 向旧master发送
WINCH信号,旧master让旧worker处理完当前请求后逐个退出。 - 确认旧worker全部退出后,向旧master发送
QUIT信号,旧master优雅退出。
用个生活化的类比:热升级就像餐厅换厨师。旧厨师手里有已经下单的菜在做,不能把菜扔了直接赶人。新厨师站在旁边,开始接新的客人。等旧厨师把手上的菜全部做完,再让他下班。整个过程客人没有一刻是没人服务的。
这里的关键点在于:连接不是master持有的,而是worker持有的。master只是管理者和监听socket的所有者,真正接收和处理连接的是worker。旧worker不退出,它手里的连接就一直存活。所以只要我们不强制杀掉旧worker,连接就不会断。这也是为什么热升级过程中,新老worker可以同时处理请求,互不干扰。
2.4 一个很多人忽略的细节:pid文件的变化
热升级过程中,pid文件会发生变化。旧master收到USR2信号后,会把原来的pid文件重命名为nginx.pid.oldbin,然后新master会写入一个新的pid文件。所以升级后你会在pid目录下看到两个文件:nginx.pid(新master的pid)和nginx.pid.oldbin(旧master的pid)。
这个特性在做后续操作时非常重要。回滚或者给旧master发WINCH信号时,需要找到旧master的pid,如果你没有提前保存,就要靠查看nginx.pid.oldbin文件来获取。很多人在这里卡住,就是因为不知道这个细节。
3. 动手前必须做好的三件事:编译参数迁移、依赖检查、旧二进制留底
热升级的风险控制,一半以上在升级前完成。仓促上手,后面出问题只能干瞪眼。我每次升级前都会按固定流程做三件准备。
3.1 先把当前版本和编译参数完整记下来
运行nginx -V,把输出的完整信息保存起来,尤其是configure arguments部分。
bash复制nginx -V
输出类似这样的内容:
text复制nginx version: nginx/1.16.1
built by gcc 4.8.5 20150623 (Red Hat 4.8.5-44) (GCC)
built with OpenSSL 1.0.2k-fips 26 Jan 2017
TLS SNI support enabled
configure arguments: --prefix=/usr/local/nginx --with-http_ssl_module --with-http_v2_module --with-stream
configure arguments就是你当初编译nginx时使用的参数。升级时,新版本必须使用相同的configure参数,否则升级后某些模块可能消失。比如旧版本有--with-http_ssl_module,新编译时漏掉了,升级后HTTPS服务直接起不来。最稳妥的做法是把这个参数复制到一个文本文件里,后面configure时直接引用。
3.2 编译依赖检查与安装
在CentOS 7上源码编译nginx,需要的依赖包包括:gcc(编译器)、make(构建工具)、pcre-devel(正则表达式库)、zlib-devel(gzip压缩库)、openssl-devel(HTTPS/TLS支持)。如果没有安装,configure阶段会报错。
bash复制yum install -y gcc make pcre-devel zlib-devel openssl-devel
有些场景还会用到libxml2-devel、libxslt-devel、gd-devel等,取决于你要编译的模块。如果没有特殊模块需求,上面这四个基础包基本够用。
顺便提一句:如果configure阶段报错说pcre.h找不到,就是pcre-devel没装;如果报openssl/ssl.h找不到,就是openssl-devel没装。这类报错信息非常明确,对症装包即可。
3.3 下载新版本源码并用原参数编译
从nginx官网下载新版源码包(根据你的需求选择版本):
bash复制wget https://nginx.org/download/nginx-1.24.0.tar.gz
tar -zxvf nginx-1.24.0.tar.gz
cd nginx-1.24.0
然后使用之前从nginx -V里拿到的configure参数进行配置:
bash复制./configure --prefix=/usr/local/nginx --with-http_ssl_module --with-http_v2_module --with-stream
make -j4
注意这里不要执行make install。我们只需要编译出来的二进制文件,不需要它覆盖现有的安装目录。编译完成后,二进制文件在objs/nginx路径下。
为什么要这样做?因为make install会按照configure时指定的--prefix路径,把二进制、配置、日志目录等文件统一安装到指定位置,这样会覆盖现有nginx的目录结构,等于把回退的路给堵死了。只编译不安装,我们就能拿到一个干净的二进制文件,手工控制覆盖操作。
3.4 备份旧二进制与配置文件
这一步是回滚的基础,千万别跳过。把当前正在使用的nginx二进制文件复制到备份目录:
bash复制mkdir -p /opt/nginx_backup
cp /usr/sbin/nginx /opt/nginx_backup/nginx.1.16.1.bak
cp /etc/nginx/nginx.conf /opt/nginx_backup/nginx.conf.1.16.1.bak
这里要特别说一下路径差异。CentOS 7上如果通过yum安装(包括nginx官方yum源),nginx二进制通常在/usr/sbin/nginx;如果是源码编译安装且--prefix=/usr/local/nginx,那二进制在/usr/local/nginx/sbin/nginx。做备份前先用which nginx或者ps -ef | grep nginx确认一下运行中的二进制路径。
备份这一步的成本就是一条cp命令的时间,但没备份的代价可能是一次无法回滚的生产事故。我见过有人升级后发现新版本有问题想回退,结果旧二进制已经被覆盖了,只能重装,中间业务中断了很久。不要犯这种低级错误。
3.5 确认当前运行状态和配置无误
在动手升级前,还应该先做一个nginx -t,确认当前配置文件没有语法错误:
bash复制nginx -t
这个操作可以提前暴露配置问题。如果这一步都不过,升级后同样会失败,但至少能确认问题出在配置上而不是升级操作上。
4. 正式热升级:从USR2信号到旧进程退出
准备做完,进入正式操作。以下命令我按实际执行顺序列出,每一步都说明验证方法,避免走到一半不知道下一步怎么办。
4.1 记录旧master的PID
升级前先把旧master的PID保存到一个变量里。热升级之后pid文件会被新master接管,到时候再找旧PID会比较绕。用nginx.pid.oldbin也能找到,但现在顺手存一下更省事。
bash复制OLD_PID=$(cat /var/run/nginx.pid)
echo $OLD_PID
注意pid文件的路径:yum安装的nginx,pid文件在/var/run/nginx.pid(CentOS 7上/var/run是/run的软链接,两者等效);源码编译安装的可能在/usr/local/nginx/logs/nginx.pid。如果cat不到,用ps -ef | grep "nginx: master"查看master的PID也可以。
4.2 用新二进制替换旧二进制
把编译好的新二进制复制到nginx的二进制路径,直接覆盖:
bash复制cp /path/to/nginx-1.24.0/objs/nginx /usr/sbin/nginx
这时候可能有人会疑惑:替换了运行中的二进制,会不会影响当前进程?不会。运行中的进程在启动时已经把可执行文件映射到内存,之后运行的代码一直是内存里的旧版镜像。磁盘上的文件只是“下一次启动时用来加载的新版本”。这正是热升级能操作的前提——我们先准备好新二进制,然后通过信号让运行中的master去启动新master。
替换完后执行一遍nginx -t,确认新二进制能正常解析配置。这一步能提前发现新版本的配置兼容性问题。
bash复制nginx -t
4.3 发送USR2信号,启动新master
现在向旧master发送USR2信号:
bash复制kill -USR2 $OLD_PID
发送后,旧master会用磁盘上的新二进制启动一个新的master进程,新master再启动新的worker进程。这时候执行ps -ef | grep nginx,会看到类似这样的输出:
text复制root 12345 1 0 10:00 ? 00:00:00 nginx: master process /usr/sbin/nginx
nginx 12346 12345 0 10:00 ? 00:00:00 nginx: worker process
root 23456 1 0 10:01 ? 00:00:00 nginx: master process /usr/sbin/nginx
nginx 23457 23456 0 10:01 ? 00:00:00 nginx: worker process
注意输出里有两个master进程,一个是旧master(12345),一个是新master(23456)。这是热升级过程中的正常中间态,不是异常。新master对应的worker是新的,旧master的worker还在。
此时查看pid文件的变化:
bash复制cat /var/run/nginx.pid # 新master的pid
cat /var/run/nginx.pid.oldbin # 旧master的pid
4.4 验证新worker是否正常接管
这一步不能省。升级不是“信号发出去就完事”,必须确认新进程能正常处理请求,才能进入下一步。
首先用curl验证本机服务是否正常:
bash复制curl -I http://localhost
如果服务正常返回HTTP响应头,说明新worker已经能正常处理连接。然后看error.log有没有异常输出:
bash复制tail -f /var/log/nginx/error.log
接入层服务的验证更要谨慎,可以请求几个线上真实接口,确认反向代理、负载均衡、SSL证书这些能力在新版本下都正常。
如果验证发现新版本有问题,这时候就要立即回滚,流程见第5节。如果一切正常,继续往下走。
4.5 发送WINCH信号,让旧worker优雅退出
确认新worker工作正常后,向旧master发送WINCH信号:
bash复制kill -WINCH $OLD_PID
WINCH的作用是让旧master优雅关闭它的worker进程。旧worker会把已经建立的连接处理完再退出,但不再接受新的连接。新连接全部由新worker处理。
执行命令后再看进程列表,正常情况下旧worker会逐渐消失,最终只剩新master、新worker和旧master:
text复制root 23456 1 0 10:01 ? 00:00:00 nginx: master process /usr/sbin/nginx
nginx 23457 23456 0 10:01 ? 00:00:00 nginx: worker process
root 12345 1 0 10:00 ? 00:00:00 nginx: master process /usr/sbin/nginx
如果旧worker迟迟不退,通常是有长连接存在(比如WebSocket、大文件下载、keepalive连接)。这时候需要等待,或者检查业务里是否有长时间挂着的连接。不要强行杀掉旧worker,否则连接会断开,热升级就失去意义了。等旧worker全部退完后,再进入下一步。
4.6 发送QUIT信号,彻底结束旧master
旧worker全部退出后,旧master已经没有任何worker。这时候向旧master发送QUIT信号:
bash复制kill -QUIT $OLD_PID
QUIT是优雅退出,master在确认所有worker都退出后才会结束。执行后再看进程列表:
bash复制ps -ef | grep nginx
正常情况只剩一个新的master和它的worker进程,热升级到此完成。
最后确认一下版本:
bash复制nginx -V
此时输出应该是新版版本号。
4.7 关于systemd管理的特殊情况
如果nginx是通过systemd管理的(CentOS 7上yum安装后通常有nginx.service),热升级用信号操作不会经过systemd。如果unit文件里配置了PIDFile=/run/nginx.pid,systemd会自动跟踪新的master PID,systemctl status nginx会显示active。如果unit没配PIDFile,systemd可能还记着旧PID,升级后systemctl status可能显示异常,但服务本身是正常的。这种情况可以systemctl daemon-reload,或者重启一下nginx服务(如果业务允许)。
实际上我建议,如果能通过systemctl管理,最好检查一下unit配置,确认PIDFile是否存在。这属于细节问题,但生产环境操作时,这些细节经常会变成“怎么systemctl status和我想的不一样”的疑团。
5. 升级后的验证清单和失败回滚流程
热升级成功不代表就万事大吉了。上线后的观察和回滚预案同样重要。我建议按以下清单做一次完整验证。
5.1 升级完成的验证清单
| 检查项 | 命令 | 预期结果 |
|---|---|---|
| 进程状态 | ps -ef | grep nginx |
只有一个master,worker数量正常 |
| 配置有效性 | nginx -t |
syntax is ok,test is successful |
| 版本确认 | nginx -V |
显示新版本号和编译参数 |
| 服务可访问 | curl -I http://localhost |
返回HTTP响应 |
| 业务接口 | 请求实际业务接口 | 返回正常业务数据 |
| 错误日志 | tail -f /var/log/nginx/error.log |
无新增error级别日志 |
| 端口监听 | netstat -tlnp | grep 80 |
监听端口正常,PID为新master |
建议在升级完成后观察一个业务周期(比如10到30分钟),确认没有隐藏问题再宣布升级完成。
5.2 如果升级后发现问题,怎么快速回滚
回滚的核心:旧二进制还在备份位置,就能一键还原。
回滚步骤如下:
- 把备份的旧二进制复制回去:
bash复制cp /opt/nginx_backup/nginx.1.16.1.bak /usr/sbin/nginx
- 找到旧master和新master的PID。新master的PID在
/var/run/nginx.pid,旧master的PID在/var/run/nginx.pid.oldbin。如果PID文件已经被覆盖或丢失,用ps -ef | grep "nginx: master process"手工识别。
bash复制NEW_PID=$(cat /var/run/nginx.pid)
OLD_PID=$(cat /var/run/nginx.pid.oldbin)
- 向旧master发送HUP信号,让旧master用旧二进制重新拉起worker进程:
bash复制kill -HUP $OLD_PID
- 验证旧worker正常接收请求:
bash复制curl -I http://localhost
- 确认旧master拉起worker并正常服务后,向新master发送QUIT信号,让新master退出:
bash复制kill -QUIT $NEW_PID
为什么先HUP旧master再QUIT新master?因为回滚也要保持连接不断。先用旧版本接管连接,再撤掉新版本。顺序不能反,否则新master一退,旧worker还没ready,就出现服务空窗期。
5.3 升级失败的常见原因
- 配置不兼容:新版本对某些配置项的语义有变化,升级前
nginx -t能提前发现大部分问题。 - 编译参数缺失:configure时漏了模块,导致升级后功能缺失,比如HTTPS、HTTP/2、stream模块不可用。
- 新版本自身bug:极少数情况,新版在某些场景下表现异常,比如内存泄漏、worker崩溃。需要回滚并排查原因。
- 第三方模块不兼容:自研模块或商业模块没有适配新版,升级后模块加载失败。这种风险在编译前就要评估清楚。
6. 我在CentOS 7上实测踩过的坑,以及建议的固定操作习惯
热升级流程本身不复杂,我在生产环境做过很多次,真正出问题的地方都是些“看起来很小”的细节。在这里把坑和习惯做法都列出来。
6.1 坑1:pid文件路径搞错,信号发错对象
CentOS 7上yum安装的nginx,pid文件在/run/nginx.pid或/var/run/nginx.pid(两者是同一个,因为/var/run是指向/run的软链接)。但源码编译安装默认路径是/usr/local/nginx/logs/nginx.pid。如果抄网上的命令时没注意路径,cat /var/run/nginx.pid可能cat不到,或者明明cat到了却不是当前运行进程的PID。
我的习惯是:升级前先用ps -ef | grep "nginx: master"确认当前master进程的真实PID,再和pid文件内容对比。这样即使pid文件路径不对,也不会信号发错对象。
6.2 坑2:configure参数没对齐,升级完某个模块直接消失
这是我见过最多的翻车现场。旧版本编译时带了--with-http_ssl_module,新版本configure时只写了--prefix=/usr/local/nginx,升级完HTTPS网站直接无法访问,因为ssl模块压根没编译进去。
解决办法就是升级前完整记录nginx -V的configure参数,configure时原样复制。可以用nginx -V 2>&1 | grep "configure"提取参数保存。
6.3 坑3:没做二进制备份,回滚无从谈起
这个前面反复强调过。备份成本极低,就是一条cp命令。没有备份的回滚方案形同虚设。我建议把/opt/nginx_backup这个目录固定下来,每次升级都把旧二进制、旧配置文件、nginx -V输出一起放进去,按版本号命名,将来排查问题也有据可查。
6.4 坑4:升级后看到两个master以为失败了
热升级中间态就是两个master并存,很多人第一次操作时看到ps里有多个master,以为命令执行错误,甚至直接重启服务。这个心理关要过:只要流程没走完,两个master是正常现象;只有走到最后QUIT旧master后再看到多个master,才需要排查。
6.5 坑5:长连接导致旧worker一直不退
升级过程中最常遇到的卡点就是:发了WINCH信号,旧worker就是不退。原因是worker手里有长连接,比如WebSocket、SSE、大文件下载、或者upstream keepalive连接。nginx的worker不会强行断开连接,它会等连接处理完才退出,这是设计行为,不是故障。
遇到这种情况,先判断业务能否接受这个等待时间。如果长连接业务本身就是常态化的(比如流媒体服务),可以等业务低峰期再操作,或者在确认新版本无误后,评估是否可以接受短时间连接中断,强制结束旧worker。强制结束旧worker的命令是:
bash复制kill -QUIT 旧worker的pid
这会让单个worker处理完当前请求后退出,但如果连接一直不断,它依然不退。你还可以直接kill 旧worker的pid,但这样连接会断,和热升级的初衷违背,要谨慎。
6.6 坑6:忽略第三方模块的版本适配
如果旧版本编译时添加了自研模块或商业模块,新版本升级前必须确认这些模块是否支持新版nginx。有些模块基于特定版本开发,nginx版本一变就编译不过,或者运行时报错。这一步在下载新版源码前就要确认,别等到编译时才发现问题。
6.7 建议的固定操作习惯
升级nginx这件事,我一个人操作过很多次,也协助团队处理过线上事故。现在我的固定流程是:
- 升级前:保存
nginx -V输出、记录旧版本号、备份二进制和配置、确认业务低峰期、在预发环境先演练一遍。 - 升级中:用变量保存旧PID,每执行一步都验证进程和服务状态,不赶进度。
- 升级后:做完整验证清单,观察一个业务周期,把升级记录(新旧版本号、日期、操作人、编译参数)写进变更文档。
热升级看起来只有一个USR2信号的事,但真正让它安全落地的,是升级前的备份、编译参数对齐和升级后的验证。把这些准备做到位,整个操作就是一条命令一条命令地走完流程,基本不会出意外。我在实际操作中的一个体会是:热升级最大的风险不是信号用错,而是准备工作没做全。备份好旧二进制、记录好编译参数、确认新版本能正常服务,这三点做到,热升级就是一件很常规的运维操作。将来有新人问起nginx怎么升级,我会直接告诉他:先备份,再编译,然后USR2、WINCH、QUIT三个信号走一遍,最后别忘了验证和留档。
