☰
Nginx离线升级全攻略:选型、源码编译与平滑切换实操
2026/10/10 7:59:14 网站建设 项目流程

2. 升级方法选型:RPM本地仓库包升级与源码编译升级的取舍

一个运维老手拿到一批内网机器的升级清单,第一反应绝不是直接敲命令,而是问三个问题:这些机器是什么操作系统?Nginx是包管理装的还是源码编译的?业务上能不能接受短暂停服?这三个问题的答案,直接决定了离线升级走哪条路线。

2.1 走RPM/本地Yum源升级:适合标准环境,但别当万能药

如果目标机器是CentOS 7/8、Rocky、Alma这类RedHat系系统,且Nginx当初是用yum安装的,那么走本地RPM源升级是效率最高的一条路。操作思路也很简单:找一台能访问外网的“搬运机”,下载好目标版本Nginx的RPM包以及全部依赖包,传到内网,用createrepo生成本地仓库元数据,把仓库配置写进/etc/yum.repos.d/,最后在内网机器上执行升级。

具体流程大致是这样:

# 在搬运机上准备RPM包目录 mkdir -p /data/nginx-repo/Packages cd /data/nginx-repo/Packages # 下载Nginx主包及依赖(此处按实际系统版本选择) # 假设下载到了 nginx-1.24.0-1.el7.ngx.x86_64.rpm 及 openssl、pcre、zlib 等依赖rpm # 生成仓库元数据 cd /data/nginx-repo createrepo . # 将整个 /data/nginx-repo 目录传到内网机器上,例如 /opt/nginx-repo # 内网机器配置本地repo vim /etc/yum.repos.d/nginx-local.repo

repo文件内容:

[nginx-local] name=nginx-local baseurl=file:///opt/nginx-repo enabled=1 gpgcheck=0

配置好之后:

yum clean all yum makecache yum install nginx # 或者只升级已装的 yum update nginx

RPM路线最大的优点是省事:升级后systemd服务脚本、日志轮转配置、目录权限通常都不需要额外调整,systemctl restart nginx就能接上。但它有两个很明显的软肋:

  1. 第三方模块很难塞进去。如果你之前在编译时加过--add-module这种参数,比如加了Lua模块、专门的WAF模块、定制的header过滤模块,那RPM包是背不动这些私人装备的。强行用RPM覆盖,模块直接丢光。
  2. 官方RPM源不一定适合离线场景。Nginx官方仓库里的包名和系统自带的包之间有依赖关系,还可能跟系统已有的openssl版本打架。想靠一两个RPM文件搞定全部依赖,在内网环境里经常会被依赖解析卡住。

所以,仓库环境越标准、越干净,RPM路线的性价比越高;但凡动过模块、改过源码、或者系统库版本偏旧,老老实实走源码编译线。

2.2 走源码编译升级:可控性最强,但准备工作最重

源码编译这条路,适用于大多数真实生产环境。原因很简单:很多内网机器的Nginx本来就是当初用源码./configure && make && make install手工装出来的,编译参数五花八门:--prefix可能装到了/usr/local/nginx,也可能单独指定了sbin-path;模块也是按需加的,有stub_status、有http_v2、有stream,甚至还有自己写的filter模块。

对这些机器而言,离线升级的本质不是“换一个安装包”,而是“用和以前一样的配方,把二进制换成新版本”。所以源码编译是唯一能完整继承原配置参数的方式。

源码编译升级的完整工作链是:准备源码包 → 准备编译依赖(gcc、make、PCRE、zlib、OpenSSL的开发库或源码) → 用原编译参数加上需要的更新重新configure → make → 替换二进制 → 平滑重载。

这条线的代价是前期准备多、对环境要求高。内网机器如果没有gcc和make,要么在搬运机上把编译环境一并准备成“可移植包”,要么直接在搬运机上完成交叉编译(如果是相同CPU架构和操作系统版本,可以提前编好二进制,把成品带进内网)。我在实际项目中比较推荐的也是后者:在搬运机上一次性把目标版本编译好,带上二进制和相关动态模块进内网替换,因为内网机器上临时装编译工具链往往比升级Nginx本身还要麻烦。

2.3 环境评估与路线决策表:动手前先花十分钟做判断

我把决策逻辑整理成一张表,你照着对号入座就行:

条件推荐路线理由
系统是RedHat系,Nginx由yum安装,没有自定义模块RPM/本地Yum源服务管理与升级路径最平滑
系统是RedHat系,Nginx由yum安装,但需要补第三方模块源码编译(不推荐RPM)RPM无法引入自定义模块
Nginx是源码编译安装,且nginx -V有自定义参数源码编译必须保留原始configure参数
内网机器没有完整编译工具链搬运机预编译二进制避免在内网装工具链的连锁依赖
不确定原Nginx怎么装的先查nginx -V和安装路径再决定无法判断路线时,先恢复原状再升级

这张表的价值在于逼你先回答“我到底要什么”。离线的环境里试错成本比联网环境高一截,与其事后折腾回滚,不如把判断放在动手前。

3. 源码编译离线升级的核心实操:平滑切换不丢连接

前面讲完了选型,这一节直接进入重头戏:基于源码编译的离线平滑升级。很多文章会把make install当成升级的终点,但真正的生产环境升级,关键在于如何让新旧版本衔接时不丢用户请求。Nginx信号机制里的USR2、WINCH、QUIT这几个信号,就是为这个场景设计的。

3.1 先搞懂平滑升级的进程切换原理,操作才不容易出错

Nginx在运行起来后,实际有两个层级的进程:一个master主进程负责管理、监控、加载配置;多个worker进程负责真正的请求处理。升级时最朴素的做法是停了旧的、启动新的,代价是停服窗口里所有连接断掉。

平滑升级的思路则是:旧master先不退场,新master在旧master基础上再启动一份。具体过程如下:

  1. 向旧master进程发送USR2信号,旧master会重新以新二进制启动一个新的master进程。此时新旧两个master和各自的worker进程同时存在,新master的worker开始接管新的请求,旧master的worker继续处理存量连接。
  2. 向旧master发送WINCH信号,旧master会优雅地逐步关闭自己的worker进程,存量连接处理完一个关一个,不处理完不强制杀。
  3. 等旧master的worker全部退出后,再向旧master发送QUIT信号,让旧master进程自己退出。这时候整个进程树里就只剩下新master和新worker了。

这个机制说白了就是“新旧两代人交接班”:老的等手头活干完再走,新的已经上岗接客。窗口期业务几乎没有感知,只有日志里能看到某段时间内新旧worker交替存在的痕迹。

注意,网上有人把升级命令写成nginx -s upgrade,这是个不存在的选项。nginx -s后面真正支持的是stop、quit、reopen、reload,平滑升级要靠前面说的信号配合来触发。你在源码包的Makefile里看到的make upgrade目标,本质上也还是发送USR2等信号,不是调什么特殊命令。

3.2 完整实操流程:从内网拿到二进制到平滑切换落地

先说准备工作。在搬运机上,假设我们已经把Nginx新版本源码包、OpenSSL源码、PCRE源码、zlib源码都准备好了,并且确认目标内网机器的CPU架构和操作系统与搬运机一致(通常都是x86_64 + 同一大版本系统)。

第一步,把原始Nginx的编译参数完整抄出来。在内网机器上执行:

nginx -V

输出里configure arguments:后面那一长串,是原版编译时用的全部参数,比如:

--prefix=/usr/local/nginx --with-http_ssl_module --with-http_v2_module --with-stream --with-http_stub_status_module --with-pcre --with-zlib=../zlib-1.2.13 --with-openssl=../openssl-1.1.1w

这些参数一个都不能丢,升级版本不等于升级参数,缺模块的后果在下文会专门讲。

第二步,在搬运机解压源码并构造一致的configure参数:

tar -zxvf nginx-1.24.0.tar.gz cd nginx-1.24.0

然后按原参数执行configure。建议再加一个--with-http_v3_module吗?先不要,新增模块是一个单独的评审动作,离线升级最忌讳顺手夹带私货。升级时configure参数原则上和原参数保持完全一致,除非业务明确要求新增模块。这是降低回归风险最有效的做法。

随后编译:

make -j4

编译完成后,不建议在内网机器上原地make install。原因前面提过:内网机器不一定有完整的编译工具链,而搬运机上已经产出了完整的二进制。我们只需要把编译产物带进内网即可。这里要拿走的产物有两类:

  • 主二进制:objs/nginx
  • 动态模块:objs/*.so,如果编译时有--add-dynamic-module并且配置了load_module指令,那么这些.so文件是必须一并带上的

第三步,进入内网机器执行升级。假设我们用了scp、U盘或者内网文件服务器把搬运物传到了/tmp/nginx-upgrade/目录,接下来按这个顺序操作:

# 1. 备份现有二进制与动态模块 cp -a /usr/local/nginx/sbin/nginx /usr/local/nginx/sbin/nginx.bak cp -a /usr/local/nginx/modules /usr/local/nginx/modules.bak # 2. 保留旧master的PID,后面的信号要发给它 OLD_MASTER_PID=$(cat /usr/local/nginx/logs/nginx.pid) echo "旧master进程PID: $OLD_MASTER_PID" # 3. 检查新配置是否正确(用新二进制测试) /usr/local/nginx/sbin/nginx.bak -t # 4. 替换新二进制(先不要重启任何进程) cp -a /tmp/nginx-upgrade/nginx /usr/local/nginx/sbin/nginx chmod 755 /usr/local/nginx/sbin/nginx # 5. 动态模块同步替换(如果原配置load_module了的话) cp -a /tmp/nginx-upgrade/*.so /usr/local/nginx/modules/

这里有一个关键点:替换二进制只是文件层面的改变,正在运行的进程还不会受影响。Linux下对于访问中的二进制文件,旧进程依然持有已删除inode的句柄,所以把文件覆盖了之后,其实不需要担心正在跑的旧进程突然崩掉。这也是为什么要先备份,因为覆盖是即时的。

第四步,开始平滑切换:

# 6. 向旧master发送USR2信号,拉起新master kill -USR2 $OLD_MASTER_PID # 7. 等一两秒,让新master成功启动,然后让旧master退出worker kill -WINCH $OLD_MASTER_PID # 8. 观察旧worker是否逐步退出,确认旧worker全部消失后,结束旧master kill -QUIT $OLD_MASTER_PID

整个过程中,我一般会在旁边开一个监控窗口反复看进程状态:

ps -eo pid,ppid,stat,cmd | grep nginx

重点看旧master的PID还在不在、旧worker是不是慢慢变少了、新master和新worker活得好不好。如果WINCH之后旧worker一直没有退出,说明有请求长连接没有被释放,这时候不要急,更不要随手kill -9,等一下长连接自然超时或业务方主动断开即可。如果确认业务已经迁移完,也可以手动让旧worker退出,但那属于特殊情况。

第五步,切换完成后做一次状态确认:

nginx -v ps -eo pid,ppid,stat,cmd | grep nginx curl -I http://127.0.0.1/

看到版本号是目标版本,master和worker进程数正常,首页能正常返回状态码,升级的“硬切换”就算完成了。

3.3 实操现场容易翻车的几个细节,提前避开

细节一:configure参数里--prefix千万别改。有朋友为了让“路径更规范”,升级时顺手把--prefix改成别的路径,结果新二进制被装到了新目录,原目录的conf和logs全没动,升级后旧的nginx -t是拿新二进制测的,测的是新路径下的配置,幸好发现了,不然业务流量全打到一个找不到配置文件的孤儿进程上。任何与路径相关的参数,包括--prefix、--sbin-path、--conf-path、--pid-path,必须原封不动保留。

细节二:动态模块的主版本号必须和Nginx主版本完全一致。比如你在内网用的是Nginx 1.22.1的第三方模块,现在升级到1.24.0,如果直接把编译好的新Nginx换上去,但没有同步编译匹配1.24.0的模块,Nginx启动时大概率会在日志里报module ... is not binary compatible之类的错误,然后拒绝加载对应模块,甚至直接启动失败。所以升级时一定把load_module指向的.so文件一并替换成objs/目录下新版本编译出来的。

细节三:flush掉HTTP/2的旧连接。如果业务里大量使用HTTP/2,且旧worker一直不退,很可能是连接上还挂着HTTP/2的长连接。这类连接如果业务本身不主动断开,旧worker会一直占着。这种情况可以考虑在升级前先让网关或负载均衡把权重摘掉,等存量连接自然减少后再发信号,也可以接受新老worker共存一小段时间,等业务低峰期再收尾。

细节四:升级前先把新配置检查一遍。用旧二进制测试配置没问题,不代表新二进制下也绝对没问题。新版本对某些指令的默认值、废弃状态可能有变化,虽然通常只是warning,但nginx -t如果返回非零,我绝对不会继续往下走。用新二进制执行:

/usr/local/nginx/sbin/nginx -t

一切正常再发USR2信号,这是底线。

4. 升级后的验证、回滚与常见故障排查

平滑切换成功不代表整体升级完成。生产上我见过太多次“升级完开始报错”的案例了,所以升级后的验证环节必须走一套固定流程,不能只看版本号就收工。这一节把验证清单、回滚方案和常见问题整理成可直接参考的表格和命令序列。

4.1 升级后健康检查清单:别只看版本号

先看基础三层:进程、版本、配置。

# 版本是否正确 nginx -v # 编译参数是否完整保留 nginx -V # 配置是否无告警 nginx -t # 进程结构是否正常(1个master + N个worker) ps -eo pid,ppid,stat,cmd | grep nginx

再往上走,验证业务功能:

# 本地HTTP状态 curl -I http://127.0.0.1/ # 如果开了HTTPS,测一下证书链和握手 curl -kI https://127.0.0.1/ # 如果有反代,访问一个具体的后端接口,确认转发正常 curl -I http://127.0.0.1/api/healthcheck

然后查日志:

tail -n 100 /usr/local/nginx/logs/error.log tail -n 100 /usr/local/nginx/logs/access.log

这里有两个信号需要重点关注:error.log里如果出现[alert]、[emerg]级别的记录,需要立即处理;access.log里如果出现大量5xx,说明反代链路出问题了。还有一种不那么显眼但很重要的迹象:日志里出现[warn]级别的指令废弃提示,比如某些老指令在新版本里被标记为deprecated,这时候虽然业务不受影响,但要记录到升级文档里,计划后续沟通优化。

另外一个我每次都会做的检查是观察一定周期的连接数和内存。升级后第一波流量打进来,连接数曲线如果和升级前明显不在一个量级,要么是配置里的worker_processes、worker_connections参数因为版本行为差异导致发生变化,要么是TCP/HTTP层行为有变化。可以用ss -s看系统层socket统计,也可以用nginx -V确认编译参数没变、nginx -t确认配置没变,那大部分问题就出在业务方连接方式上。

4.2 升级失败后的回滚方案:绝对不要硬扛

平滑升级最令人安心的地方在于,它天然适合回滚。因为旧master在升级流程中一直留到了最后一步才退出,只要你有旧二进制备份,整个流程是可以完全倒回去的。

回滚分两种情况:

情况一:新的master已经启动,但旧master还没退出。这时候最简单,直接杀掉新master,让旧master继续工作。杀新master可以这样:

# 找到新master的PID(通常是原来的PID文件被覆盖后的新值) # 之前如果保存了旧master PID,可以直接定位进程树 # 直接对新master发QUIT信号,让它优雅退出 kill -QUIT $(cat /usr/local/nginx/logs/nginx.pid)

但要注意,此时PID文件内容已经被新master写上了,所以要确认你真正操作的是新master。稳妥的做法是先用ps -eo pid,ppid,cmd | grep nginx看清楚进程关系。新master退出后,旧master因为没有收到QUIT信号,它会继续管理worker,业务不会中断。

情况二:新旧已经彻底切换完成,旧master退出了。这时候回滚就相当于再做一次“反向平滑升级”。把备份的旧二进制恢复回去,重新加载动态模块,然后走一遍信号流程:

# 恢复旧二进制 cp -a /usr/local/nginx/sbin/nginx.bak /usr/local/nginx/sbin/nginx # 恢复旧动态模块 rm -rf /usr/local/nginx/modules cp -a /usr/local/nginx/modules.bak /usr/local/nginx/modules # 找到当前新master PID CUR_PID=$(cat /usr/local/nginx/logs/nginx.pid) # 用USR2拉起旧二进制的新master,再WINCH、QUIT收尾 kill -USR2 $CUR_PID kill -WINCH $CUR_PID kill -QUIT $CUR_PID

既然走到回滚这一步,说明升级效果与预期有较大偏差。回滚完成后要在升级文档里专门留一段“复盘记录”,把失败的原因、现象、回滚用时记清楚,不然下一次遇到同样的坑还是要重新踩。

4.3 常见问题速查表:离线环境踩坑实录

我把这些年做内网升级时碰到的高频问题整理成一个速查表,遇到对应报错直接查行为即可:

现象可能原因解决办法
configure时提示缺少PCRE/zlib/OpenSSL头文件搬运机上没有对应开发库下载源码包并在configure参数里用--with-pcre=...、--with-zlib=...、--with-openssl=...指定路径
make时报错openssl/opensslv.h: No such file or directoryOpenSSL开发头文件与Nginx编译参数不匹配检查--with-openssl路径是否正确,确保解压完整
新二进制启动时error.log报module is not binary compatible动态模块与Nginx主版本不匹配用新版本源码重新编译所有第三方模块,替换.so文件
nginx -t报指令不存在新版本废弃或移除了某个指令到官方变更日志查该指令的替代方案,改配置后再执行信号切换
USR2信号后新master没有启动新二进制无执行权限或动态模块缺失先chmod +x,然后用nginx -t和手动启动方式逐一排查
升级后curl访问静态文件405新版本对location匹配或try_files默认行为有差异检查配置上下文中root/alias路径是否完整,必要时逐行对比新旧配置
升级后HTTPS握手失败OpenSSL版本或SSL协议配置差异检查ssl_protocols和ssl_ciphers是否被新版本默认值覆盖,显式声明安全策略
内网机器没有gcc/make编译环境未准备不在此机器编译,搬运机预编译后拷入
升级后内存占用比之前高很多worker_processes和worker_connections配置未随新版本合理调整按实际业务规模和机器规格重算worker数量并reload
现象可能原因解决办法
升级后某个URL出现404location配置中正则或前缀匹配优先级在新版本有变化恢复配置并对比新旧版本对location的解析差异,常见于老版本配置写法较为模糊的情况
旧worker一直退不掉长连接或WebSocket连接未断开等待连接自然释放,或摘掉负载均衡权重后再次WINCH处理

5. 离线升级这件事,最后再交代几句

说实话,Nginx在线升级只要网络通,很多人五分钟就搞定了。而离线升级最磨人的地方反而不是Nginx本身,而是“物资准备”。我在实际项目中见过因为少带了一个zlib源码包导致现场编译失败的,也见过因为搬运机上Linux发行版小版本不同导致新二进制在内网机器上segfault的。所以我的习惯是:所有依赖包在搬运机上先做一次完整升级演练,全部通过后,把搬运产物(二进制、.so、依赖源码包、校验值清单)打成一个tar包,随升级文档一起进内网。

另外一个小建议:升级完成后把nginx -V输出、新二进制MD5值、升级前后配置文件的diff结果,统一存档到运维文档里。这样下次再有人接手,或者半年后又要做一次升级,翻文档就能知道这台机器的完整历史。反正离线环境里出一次事故的成本,往往比这些准备工作高得多。

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

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

立即咨询