☰
Nginx平滑升级原理与实战:从USR2信号到无缝切换
2026/9/29 10:03:27 网站建设 项目流程

最近公司线上Nginx又收到了安全公告,版本太老,必须升。我打开服务器一看,nginx -V显示还是1.20.2,上面跑着上百个server块,还有一堆WebSocket长连接和TCP四层代理。这种环境你敢直接systemctl restart nginx吗?肯定不敢,一重启所有连接瞬间断开,健康检查直接失败,用户那边立刻就能感知到服务不可用。

所谓平滑升级,就是在不中断服务、不断开已有连接的前提下,把Nginx二进制从旧版本无缝切换到新版本。这不仅是运维必备技能,也是排查Nginx版本漏洞、性能瓶颈时的常规操作。这篇文章会把整个流程从原理到实操完整拆开,每一步都给出可直接复制的命令和判断标准,不管你用的是源码编译安装,还是接手了别人的历史包袱,都能照着做。全文默认你在Linux环境,有root权限,并且Nginx是通过源码编译方式安装的,这是平滑升级最可控的安装方式。

1. 平滑升级前必须搞懂:Nginx的master-worker进程模型

1.1 为什么不能直接替换二进制再重启

很多新手第一次升级Nginx时,习惯性操作是:下载新版源码,configure、make、make install,或者直接把新编译好的二进制覆盖到/usr/local/nginx/sbin/nginx,然后systemctl restart nginx。这在测试环境完全没问题,但在线上环境会踩两个大坑。

第一个坑是连接中断。Nginx重启时,正在处理的所有请求都会被直接掐断。普通的HTTP请求还好,浏览器刷新一下就能恢复,但如果是WebSocket长连接、HTTP/2多路复用、TCP四层代理(比如数据库端口代理、游戏服转发),一断就是一片报错,调用方可能直接熔断。

第二个坑是上游负载均衡器会把你摘掉。很多生产环境前面还挂着一层SLB、LVS或者云负载均衡,它们通过健康检查来判断后端节点是否存活。Nginx一重启,健康检查端口可能在短时间内无响应,负载均衡器会认为节点挂了,直接把流量切走。等Nginx恢复后,又要等下一次健康检查周期才能重新调度流量,这个过程中用户流量已经被分到其他机器,可能造成业务波动。

所以线上Nginx升级,必须走平滑升级,让新旧版本在内存里有一个交接期,连接不断、健康检查不掉。

1.2 master-worker架构是平滑升级的底气

Nginx采用经典的master-worker进程模型。master进程负责读取配置文件、创建监听socket、fork出worker进程、管理worker生命周期;worker进程负责accept新连接并处理请求,每个worker是独立进程,互不干扰。

关键点在于:worker进程的所有代码都来自master进程fork的那一刻。也就是说,当master启动后,不管是master还是worker,它们运行的都是当时加载进内存的那份二进制代码。就算你后来把磁盘上的nginx文件替换成了新版本,正在运行的老进程依然在内存里跑着旧代码,不会被影响。

平滑升级正是利用了这个特性。我们先在磁盘上把二进制换成新版,然后通过信号让旧master进程再fork出一个新的master进程。新master读取的是磁盘上的新版代码,继承监听socket,再fork出一批新worker。这时新旧两套进程同时在跑,旧worker处理旧连接,新worker处理新连接,两者互不打架。等到旧连接自然处理完,让旧worker优雅退出,整个Nginx就完全过渡到了新版本。全程没有停机,没有连接中断。

2. 升级前准备:把现状摸透,把后路留好

2.1 记录当前Nginx版本和编译参数

这是最容易被忽略的一步,也是翻车率最高的一步。很多人升级完才发现,原来旧版支持的HTTPS现在不支持了,原来跑得好好的TCP代理也不工作了,原因就是编译参数没保留全。

执行命令:

/usr/local/nginx/sbin/nginx -V

注意是小写的-v只显示版本号,大写的-V才会显示版本号和所有编译参数。输出大概是这样的:

nginx version: nginx/1.20.2 built by gcc 8.5.0 20210514 (Red Hat 8.5.0-4) (GCC) configure arguments: --prefix=/usr/local/nginx --with-http_ssl_module --with-http_v2_module --with-stream

configure arguments这一行就是你在新版编译时必须全部保留的参数。Nginx是高度模块化的,很多功能都是通过编译参数开启的。比如HTTPS需要--with-http_ssl_module,四层代理需要--with-stream,获取客户端真实IP需要--with-http_realip_module,状态监控需要--with-http_stub_status_module。如果在configure时漏掉任何一个,对应模块就不会编译进新二进制,后续配置文件里用到相关指令时,nginx -t会直接报“unknown directive”。

建议把这行参数原样复制到文本文件里,后面configure时逐项对照。

2.2 备份配置文件和二进制

平滑升级不管多稳,都要先把后路留好。主要备份三样东西:配置目录、二进制文件、证书私钥。

cp -a /usr/local/nginx/conf /usr/local/nginx/conf.bak.$(date +%F) cp /usr/local/nginx/sbin/nginx /usr/local/nginx/sbin/nginx.old

如果证书和私钥文件放在conf目录外面(常见路径如/etc/ssl/private/、/data/certs/),也要一起备份。私钥文件在升级过程中如果出问题,恢复起来很难,而且会导致HTTPS站点直接挂掉。

顺便确认一下Nginx的pid文件位置。源码编译安装默认在/usr/local/nginx/logs/nginx.pid,如果是yum或apt安装,可能在/run/nginx.pid或/var/run/nginx.pid。后面信号操作要用到这个路径,建议先确认好,避免信号发错文件。

ls -l /usr/local/nginx/logs/nginx.pid

2.3 选择目标版本,下载源码

去Nginx官网下载页(nginx.org/en/download.html)选择版本。官方分Mainline版本(主线版,新功能上线快,但稳定性略差)和Stable版本(稳定版,推荐生产环境使用)。本文以“从1.20.2升级到1.26.3”为例,实际升级时请以官方当前稳定版为准,版本号不是重点,流程完全一致。

cd /usr/local/src wget https://nginx.org/download/nginx-1.26.3.tar.gz tar xzf nginx-1.26.3.tar.gz cd nginx-1.26.3

如果你的服务器不能访问外网,就需要先在一台能联网的机器上下载tar包,再传到服务器上离线解压。离线环境要额外注意,编译所需的依赖库(pcre、zlib、openssl的devel包)也要提前准备好。这一点在国产化系统(比如银河麒麟、统信UOS)上经常遇到,仓库源可能不完整,提前用yum install或apt install把依赖装齐,能省很多麻烦。

3. 平滑升级核心原理:四个信号撑起新旧交接

3.1 USR2:让旧master拉起新master

Nginx的平滑升级,核心靠的是master对几个信号的处理。第一个关键信号是USR2。

当旧master收到kill -USR2 $(cat nginx.pid)时,它会做两件事:一是把当前自己的PID写到nginx.pid.oldbin文件里,二是尝试从磁盘读取新的Nginx二进制文件,并fork出一个全新的master进程。这个新master会继承旧master的监听socket,所以不会出现“端口已被占用”的报错。这也是Nginx能平滑升级的根本原因:新老进程共享同一个监听端口,老连接由老worker处理,新连接由新worker处理。

这里有个细节,新master进程创建后会把自己PID写到nginx.pid文件里,所以nginx.pid指向新master,nginx.pid.oldbin指向旧master。后面所有操作都要分清这两个文件。

3.2 WINCH:让旧worker优雅退出

新master拉起后,新旧两套worker会同时存在,都在抢着处理新连接。这时需要给旧master发送WINCH信号,让旧worker退出。

kill -WINCH $(cat nginx.pid.oldbin),旧master收到WINCH信号后,会逐步通知自己的worker进程:不再接受新连接,处理完当前正在处理的请求后自动退出。这个过程就是“优雅退出”,不会中断已经在处理的请求,只是不再接新活了。

随着旧worker一个个退出,Nginx的所有新连接会全部由新master下面的worker接管。观察进程列表时,你会看到worker数量逐渐减少,最终只剩一个新master和它的一批worker。

3.3 QUIT和HUP:正常收尾和失败回滚

还有两个信号要掌握。QUIT信号用于优雅退出master进程,升级验证通过后,用QUIT让旧master退出。HUP信号用于让master重新加载配置文件,但在回滚场景下,HUP有特殊用途:如果旧master的worker都已经退完了,给它发HUP,它会按照自己内存里的旧代码重新fork出旧worker,这就等于把流量切回旧版本。

四个信号在工作中的用途如下表:

信号作用对象典型用途
USR2当前master拉起新master,启动平滑升级流程
WINCH旧master让旧worker优雅退出,停止接收新连接
QUITmaster优雅退出master,升级成功后清理旧进程
HUP旧master让旧master重新拉起worker,升级失败时回滚

另外USR1信号是重新打开日志文件,用于日志切割,和升级没关系,别混淆了。

4. 实操完整步骤:从下载编译到信号切换

4.1 安装编译依赖

Nginx编译安装需要gcc、make以及几个核心依赖库:PCRE(正则表达式支持)、zlib(gzip压缩支持)、OpenSSL(HTTPS支持)。不同Linux发行版安装命令不一样。

Debian/Ubuntu系:

apt update apt install -y build-essential libpcre3-dev zlib1g-dev openssl libssl-dev

CentOS/RHEL系:

yum install -y gcc gcc-c++ make pcre-devel zlib-devel openssl-devel

如果configure时提示缺某个库,按报错信息补齐再重来。依赖问题一般都很直观,缺什么装什么就行。

4.2 配置编译参数,保留旧模块并补充新特性

进入新版源码目录,执行configure。关键原则是:旧版的configure arguments全都要带上,缺一不可;同时可以根据需要新增模块。假设旧版参数是:

--prefix=/usr/local/nginx --with-http_ssl_module --with-http_v2_module --with-stream

那么新版可以这样配置:

cd /usr/local/src/nginx-1.26.3 ./configure \ --prefix=/usr/local/nginx \ --with-http_ssl_module \ --with-http_v2_module \ --with-http_realip_module \ --with-http_stub_status_module \ --with-stream \ --with-stream_ssl_module \ --with-pcre

逐项说明一下这些参数的作用:--prefix指定安装路径,必须和旧版一致,否则后续找配置、找日志都会出问题;--with-http_ssl_module是HTTPS支持,没有它配置里写listen 443 ssl会报错;--with-http_v2_module是HTTP/2支持,现在很多站点都在用;--with-http_realip_module用于在Nginx反代场景下获取客户端真实IP,尤其是在多层代理后面;--with-http_stub_status_module提供/nginx_status页面,用于监控连接数;--with-stream开启四层TCP/UDP代理,比如转发MySQL、Redis流量;--with-stream_ssl_module是四层代理的SSL支持;--with-pcre启用PCRE正则库。

如果旧版配置里通过--add-module附带了第三方模块(比如--add-module=/path/to/nginx-module),也要原样保留。第三方模块对新版Nginx的兼容性不确定,如果configure时报错,优先去模块仓库看有没有新版补丁,不要硬着头皮编。

4.3 编译并替换二进制:只make,不make install

configure成功后会生成Makefile,接下来执行编译:

make -j$(nproc)

-j$(nproc)是利用服务器全部CPU核心并行编译,速度更快。编译完成后,新二进制文件在objs/nginx。

注意,这里不要执行make install。因为make install会把新版的所有文件按照prefix路径重新安装一遍,包括默认配置文件、示例文件等,可能会覆盖你原有的修改。我们只需要新的可执行文件,其他东西保持原样不动。

替换前先用新二进制测试一下当前配置文件,提前发现兼容性问题:

./objs/nginx -t -c /usr/local/nginx/conf/nginx.conf -p /usr/local/nginx/

-t表示测试配置,-c指定配置文件路径,-p指定prefix路径。如果输出syntax is ok和test is successful,说明新版Nginx能正常解析现有配置,可以放心替换。如果报错,会明确提示是哪一行哪个指令出问题,常见原因包括新版移除了某些旧指令,或者某个指令对应的模块没编译进去。

确认配置没问题后,正式替换:

cp /usr/local/nginx/sbin/nginx /usr/local/nginx/sbin/nginx.old cp objs/nginx /usr/local/nginx/sbin/nginx

先备份旧二进制,再用新二进制覆盖。这里我用的是cp而不是mv,这样可以确保文件权限和原有属性保持一致。

4.4 发送USR2信号,拉起新master

二进制换好之后,马上给当前运行的Nginx发送USR2信号:

kill -USR2 $(cat /usr/local/nginx/logs/nginx.pid)

执行后立刻检查进程状态:

ps -ef | grep nginx

正常情况下你会看到两个master进程。通过pid文件可以区分谁是新的、谁是旧的:

cat /usr/local/nginx/logs/nginx.pid cat /usr/local/nginx/logs/nginx.pid.oldbin

nginx.pid里是新master的PID,nginx.pid.oldbin里是旧master的PID。此时新旧worker会并存,进程数翻倍,这是正常现象,说明升级交接已经开始。

4.5 发送WINCH信号,让旧worker优雅退出

新master起来后,发送WINCH信号给旧master,让旧worker优雅退出:

kill -WINCH $(cat /usr/local/nginx/logs/nginx.pid.oldbin)

随后再次观察进程列表:

ps -ef | grep nginx

你会看到旧master下的worker进程数量逐渐减少,最终全部退出,只剩旧master进程本身还挂着,以及一个新master带着一队新worker在正常工作。到这里,新的Nginx二进制已经接管了所有新连接。

4.6 验证新版本运行状态

进程切换只是第一步,真正要紧的是业务是否正常。验证清单如下:

# 查看当前实际生效的版本 /usr/local/nginx/sbin/nginx -V # 检查端口监听是否正常 ss -lntp | grep 80 # 测试HTTP访问 curl -I http://127.0.0.1/ # 测试HTTPS访问 curl -kI https://127.0.0.1/ # 查看错误日志是否有异常 tail -f /usr/local/nginx/logs/error.log

如果你是反代架构,还要确认后端服务的访问日志里能看到新的请求打过来;如果Nginx前面还有一层负载均衡,要确认健康检查依然正常,没有节点被摘除。如果Nginx配置了WebSocket代理或TCP/UDP四层转发,也建议找一台测试机连一下,确保功能正常。

升级后最好观察一段时间,不要急着清理旧进程。我个人的习惯是至少观察一个业务周期,比如一个高峰期过去,确认各种业务请求都没有报错,才进入下一步。

4.7 确认无误后,让旧master优雅退出

验证没问题后,把旧master也结束掉,整个升级流程就完成了:

kill -QUIT $(cat /usr/local/nginx/logs/nginx.pid.oldbin)

执行后再次查看进程:

ps -ef | grep nginx

此时应该只剩一个master和它下面的worker。nginx.pid指向的就是新master的PID。

需要提醒的是,kill -QUIT是优雅退出,它会等worker处理完当前请求再退出。如果有些旧worker因为长连接迟迟不退,可以先观察一段时间;如果确认长连接已经不需要了,可以在维护窗口直接kill -QUIT 旧worker的PID,让它们尽快结束。特别是WebSocket、HTTP/2这类连接,在升级期间如果一直有活跃连接,旧worker可能长时间不退出,这是正常的,不用慌。

5. 升级失败怎么办:回滚流程与常见问题排查

5.1 回滚流程:HUP旧master加恢复旧二进制

平滑升级最大的优势是随时可以回滚。但前提是你没有提前QUIT掉旧master。如果升级后发现新版有问题,比如某个模块不兼容、配置解析报错、worker频繁崩溃,执行以下回滚步骤。

第一步,让旧master重新拉起worker:

kill -HUP $(cat /usr/local/nginx/logs/nginx.pid.oldbin)

旧master会按照内存里的旧代码重新fork出worker,开始接收新连接。注意这里HUP的意义和平时“重新加载配置”不太一样,在升级回滚场景下,HUP会直接让旧版本的worker重新接管流量。此时新旧两套进程又同时在跑。

第二步,等确认流量已经切到旧版本后,关闭新master:

kill -QUIT $(cat /usr/local/nginx/logs/nginx.pid)

第三步,把旧二进制恢复回去:

cp /usr/local/nginx/sbin/nginx.old /usr/local/nginx/sbin/nginx

然后检查进程状态,确认Nginx已经恢复到升级前的版本和状态。整个回滚过程同样不需要重启服务器,不需要停服务,这是平滑升级最让人安心的地方。

这里强调一点:升级过程中,千万不要一慌就执行systemctl restart nginx或者reboot。那样会把新旧进程全部干掉,回滚的余地都没有了,反而会酿成更大的事故。

5.2 常见问题速查表

把升级过程中最常见的几个问题整理成速查表:

现象可能原因解决办法
configure报缺少PCRE/zlib/OpenSSL依赖库未安装安装对应devel包后重新configure
nginx -t报unknown directive新版移除了指令,或对应模块未编译补充编译参数重新编译,或修改配置文件
升级后HTTPS站点打不开新二进制没编入ssl_module必须加上--with-http_ssl_module重新编译
USR2后没有新masterpid文件路径错误,或权限不够确认cat的是实际nginx.pid,用root执行
WINCH后旧worker迟迟不退存在长连接/WebSocket/HTTP2连接等连接自然断开,或维护窗口强杀旧worker
worker频繁重启配置或模块不兼容查看error.log定位原因,必要时回滚
systemctl status nginx异常PID指向新master,systemd状态判断错乱确认进程健康,不要盲目restart

5.3 几个容易栽的坑(个人经验)

这些坑我在不同环境里都踩过,写出来帮大家省时间。

第一个坑:编译参数记不全。有次升级觉得麻烦,直接configure没带--with-stream,结果升级完所有TCP四层代理全部失效,排查了好久才发现是模块没编进去。升级前一定要把旧版nginx -V的configure arguments完整抄出来。

第二个坑:不备份直接覆盖。有人图省事,新二进制直接mv到sbin目录,结果新版一启动就崩,想回滚发现旧文件已经没了。所以备份和替换一定要分成两步走,先把旧文件复制成nginx.old再覆盖。

第三个坑:用make install来代替手动替换。make install会重新生成整个安装目录,如果配置文件里有你后来改过的东西,可能被覆盖得面目全非。平滑升级的标准动作就是make之后手动cp objs/nginx到sbin目录,其他文件一概不动。

第四个坑:升级完立刻QUIT旧master。虽然新版本验证通过,但有些配置在特定请求路径下才会触发问题,比如某些uri返回异常、某个接口鉴权失败。建议观察一段时间再清理旧master,回滚的余地越大,心里越有底。

第五个坑:用yum或apt安装的Nginx不要用源码二进制覆盖。包管理器自带Nginx的动态模块路径和配置文件路径可能和源码编译不一样,直接覆盖容易出问题。这种情况建议直接用官方nginx源执行yum update nginx或apt upgrade nginx,也能达到升级效果,只是流程不同。

第六个坑:如果Nginx是通过systemd托管的,升级后检查一下systemctl status nginx。因为平滑升级后master进程的PID变了,有些systemd配置会导致状态显示异常。确认实际进程正常后,按需重新配置unit文件即可,不要一看到failed状态就盲目的restart,那会把连接全掐断。

6. 写在最后的一点习惯

我个人在实际升级中一直是“先备、后测、再切、慢收”这八个字。先备份配置和二进制,测新版对现有配置的兼容性,确认无恙再切信号,最后不要急于收掉旧master。按这个流程做过几十次Nginx升级,从1.16到1.20再到1.26,几乎没有翻过车。如果你也经常和Nginx打交道,建议把整套操作写成一个带set -e的bash脚本,每次升级前自动执行nginx -t,失败就中止,成功再继续,这样能把人为失误降到最低。

最后再分享一个小技巧:升级过程中如果看到worker进程长时间不退出,可以用ps -ef --forest看进程树,直观确认新旧两套master的从属关系。还有,所有信号操作前,先cat nginx.pid确认文件里确实是当前master的PID,别在维护多个Nginx实例的机器上误杀了别的进程。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询