1. 动手升级前先摸清三件事:当前状态、版本差异和依赖环境
1.1 用一条命令摸清家底:nginx -V 的正确解读
先说一个很多教程不强调、但实战中特别容易出事的点:升级nginx之前,第一步不是去官网下源码,而是先把线上正在运行的版本和编译参数完整捞出来。
在服务器上执行:
nginx -V注意这里是大写的 V。它会输出类似这样的信息:
nginx version: nginx/1.24.0 built by gcc 4.8.5 20150623 (Red Hat 4.8.5-44) 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-http_realip_module --with-stream --with-stream_ssl_module --with-http_stub_status_module这一段configure arguments就是你后续编译新版本时必须保留的核心参数。我见过不止一次这样的场景:同事升级nginx时图省事,直接./configure --prefix=/usr/local/nginx一把梭,结果升级完以后一堆业务异常——WebSocket连不上、真实验IP变成了nginx内网IP、stream代理直接没了。最后查下来,就是编译参数丢了几个module导致的。
所以拿到nginx -V的输出后,建议复制到记事本里,后面编译新版本时逐项对照。另外顺手看一下nginx的版本号:nginx -v(小写v)和nginx -V | head -1效果一样,都可以确认当前版本。如果线上是1.18.0这种比较旧的版本,升级时跳的步子就更大,后面章节我会专门说版本跳级的事。
1.2 目标版本怎么选:稳定版优先,不要盲目追新
选目标版本这件事,很多人随手下载一个最新mainline版本就上了,这个习惯我劝你改一改。nginx的官方版本线分成两类:
| 类型 | 版本特征 | 适用场景 |
|---|---|---|
| Mainline版 | 版本号中间是奇数,如1.27.x | 新功能尝鲜、测试环境、短期项目 |
| 稳定版 | 版本号中间是偶数,如1.24.x、1.26.x | 生产环境、长期运维、依赖较多场景 |
我这次升级选的是稳定版的1.26.x系列。选择一个版本之后,还要做一件很重要的事:打开官方CHANGES文件,从头到尾扫一遍从你当前版本到目标版本之间的变更记录。
这个文件里会列出每一个小版本的功能变更、bug修复和配置行为变化。举几个我实际遇到的例子:某些版本调整了日志格式默认字段,有的版本对HTTP/2的某些指令做了废弃标记,还有的版本修改了ssl_protocols的默认值。这些变更如果不提前知道,升级完可能会发现日志解析脚本挂了、或者TLS配置行为和以前不一样。
另外,如果你用的是操作系统自带的nginx包(比如yum或apt装的),升级路径不太一样,优先考虑通过包管理器升级到同系列最新版,而不是混用源码编译。混用容易造成二进制位置、配置文件路径、pid文件路径不对齐的问题,排查起来很痛苦。本文后面说的升级方式,主要针对源码编译安装的nginx。
1.3 依赖库与第三方模块的连带风险
nginx编译依赖几个基础库:PCRE(正则支持)、zlib(gzip压缩)、OpenSSL(HTTPS和TLS)。升级时如果环境里这些库的版本太老,新版本nginx可能编译失败或者在运行时出现诡异行为。比如要使用HTTP/3或某些新TLS特性时,对OpenSSL版本有明确要求,编译前最好确认一下:
gcc --version pcre-config --version openssl version zlibconf --version # 或者 zlib.h 宏如果你依赖的是--with-http_ssl_module这种静态方式,那OpenSSL跟随nginx一起编译,问题不大;但如果你的环境中有些库是通过动态链接方式引用的,升级前就要评估库版本兼容性。
还有一类风险最容易忽略:第三方模块。比如你用了Lua扩展、防火墙类模块、某些控制面板自带的nginx插件,这些模块往往是针对特定nginx版本编译的。升级nginx主版本后,第三方模块如果没有同步更新,轻则编译失败,重则运行期段错误。检查方法也简单,nginx -V的输出里如果带--add-module=或者--add-dynamic-module=参数,就说明挂了第三方模块,升级前需要逐一确认它们对目标版本的兼容性。
提示:建议在最开始就给线上nginx做好编译参数和模块清单的文档备份。没有文档的话,每次升级都要靠回忆和猜,风险完全不可控。
2. 直接升级:编译新版本并替换二进制,全程可控可回滚
2.1 下载源码与编译参数还原
直接升级,也叫停机升级或者二进制替换升级,思路很简单:新版本编译好后,把旧的nginx二进制文件换掉,重启进程。这种方式有一个短暂的中断窗口(业务会断开几秒到几十秒),但胜在流程简单、可控性好,适合允许短暂停服的业务场景。
先说下载源码。到nginx官网下载页找到目标版本的源码包,比如nginx-1.26.x.tar.gz,用wget拉下来后解压:
wget https://nginx.org/download/nginx-1.26.x.tar.gz tar zxvf nginx-1.26.x.tar.gz cd nginx-1.26.x然后开始configure,这一步必须把之前nginx -V里看到的参数完完整整带回来。我这次线上环境的完整命令是这样的:
./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-http_gzip_static_module \ --with-pcre \ --with-cc-opt='-O2 -g -pipe -Wp,-D_FORTIFY_SOURCE=2 -fexceptions -fstack-protector-strong --param=ssp-buffer-size=4 -grecord-switches -m64 -mtune=generic' \ --with-ld-opt='-Wl,-z,relro -Wl,-z,now -pie'这里的--with-cc-opt和--with-ld-opt一般是从系统自带的nginx配置里抄过来的安全编译选项,如果你原来的nginx没有这些参数,不加也行,不影响功能,只是编译出的二进制加固程度有差别。
configure跑完之后,检查一下输出的summary,重点看有没有你期望的模块。如果缺少某个模块,大概率是依赖库没装全,回头补齐依赖再重新configure。
2.2 只编译不安装,手动替换关键文件
Configure完成后执行make,但注意:不要直接make install。
原因是make install会把新版文件铺满整个--prefix目录,包括覆盖你的配置文件、日志目录结构、甚至清掉一些你手动添加的辅助脚本。而这些目录里往往存着线上真正的数据。所以推荐的做法是:
make编译完成后,新版nginx二进制位于objs/nginx。接下来按顺序做这几件事:
# 1. 备份当前二进制 cp /usr/local/nginx/sbin/nginx /usr/local/nginx/sbin/nginx.old # 2. 复制新二进制 cp objs/nginx /usr/local/nginx/sbin/nginx # 3. 检查新二进制是否能正确加载配置 /usr/local/nginx/sbin/nginx -tnginx -t这一步非常关键。如果配置文件中使用了新版已经废弃的指令,或者语法上有变化,-t会把错误直接报出来。我当时就遇到过一件事:旧配置里写了某个老版本才认识的指令,新版本已经移除了,-t立刻给出unknown directive的提示,才避免了一次上线事故。
2.3 停旧启新,验证端口和进程状态
配置检查通过后,执行摇身一变。先停旧进程再启新进程:
# 优雅停止旧进程 /usr/local/nginx/sbin/nginx -s quit # 启动新进程 /usr/local/nginx/sbin/nginx注意这里的顺序必须严格:先停旧、再启新。如果不停旧直接启动新进程,新的master进程会绑定失败,报address already in use,因为旧进程还占着端口。另外-s quit是优雅退出,会等worker处理完当前请求后再退出;如果用-s stop强制停,正在进行的请求会被直接掐断。能接受几秒等待的话,尽量用quit。
启动后立刻检查几个东西:
ps -ef | grep nginx ss -lntp | grep 80确认master和worker进程都正常起来、端口正常监听后,再做一轮简单的功能验证(curl首页、本地带Host请求、检查error.log有没有新增报错)。
直接升级的优点是真出问题方便回滚:把nginx.old改回nginx,重启进程就还原了。缺点是那个中断窗口对某些业务来说可能无法接受。比如跑着大量WebSocket长连接的在线业务,哪怕断开两秒都会造成大量用户掉线,这种场景就必须用接下来要说的在线升级。
3. 在线升级:利用信号机制实现平滑热切换
3.1 热升级原理:master进程如何交接监听端口
在线升级,业内也叫热升级或平滑升级,核心目标就是一句话:新老进程交替的过程中,监听端口不断开,已有连接不断开,新连接无缝交给新进程。
要做到这个效果,靠的是nginx master-worker架构和一组进程信号。这里先把原理讲透:
nginx启动后,master进程(一号进程)负责管理整个生命周期,worker进程实际处理连接。master进程在启动时创建监听socket,然后fork出来的worker进程会继承这个socket。正常运行时,大家共享同一个监听socket,新连接进来后由内核负载均衡分给某个空闲worker。
在线升级的过程是这样的:
- 新二进制已经就位,但旧进程还在跑。
- 给旧master发送
USR2信号。旧master收到后,会fork出一个新的master进程(新master),新master读取新配置、加载新模块,然后fork出自己的worker进程。 - 关键的机制来了:新master启动时,复用旧master已经创建好的监听socket文件描述符。也就是说新worker和旧worker实际上监听在同一个socket上,新连接不会因为端口被占用而失败。
- 新worker开始接收新连接,旧worker继续处理它手上已有的连接。
- 给旧master发送
WINCH信号,旧master会优雅关闭自己的worker,也就是等它们处理完手头的请求后退出。 - 最后给旧master发送
QUIT信号,旧master退出,升级完成。
这个过程中,nginx把新老两个master进程之间通过特殊的进程通信机制传递了监听socket的继承关系,所以端口始终没有被释放过,连接也没有被掐断。
3.2 标准操作流程与命令解读
理论说完,上实操。准备工作还是和直接升级一样,先编译好新版本,然后把objs/nginx复制到/usr/local/nginx/sbin/nginx(同样先备份旧的)。实际上线操作命令如下:
# 第一步:查看当前master进程pid cat /usr/local/nginx/logs/nginx.pid # 第二步:给旧master发送USR2信号,启动新master kill -USR2 $(cat /usr/local/nginx/logs/nginx.pid) # 第三步:确认新master和worker已经启动 ps -ef | grep nginx执行kill -USR2后几秒内,进程列表里会出现两套master进程和两套worker进程。注意此时nginx.pid文件已经被新master写入自己的pid,而旧master的pid被保存在nginx.pid.oldbin里:
# 旧master的pid在新文件里 cat /usr/local/nginx/logs/nginx.pid cat /usr/local/nginx/logs/nginx.pid.oldbin继续执行优雅退出旧worker:
# 第四步:给旧master发送WINCH信号,关闭旧worker kill -WINCH $(cat /usr/local/nginx/logs/nginx.pid.oldbin)发送WINCH后,旧worker会陆续退出。你可以用ps -ef | grep nginx观察,旧的worker数量逐步减少到0,但旧master进程还在。此时新旧连接已经完全由新版本接管。
最后收尾:
# 第五步:确认新版本运行正常后,退出旧master kill -QUIT $(cat /usr/local/nginx/logs/nginx.pid.oldbin)执行完这五步,在线升级就完成了。整个过程中业务连接没有中断,对外服务的端口一直处于listen状态。
3.3 新老进程共存期的行为与风险控制
在线升级虽然不中断连接,但新老进程共存的窗口期有一些微妙的行为,不搞清楚容易出问题。
第一,这个窗口期内新老worker同时监听同一个端口,内核会把新建连接随机分给新老worker。换句话说,在发WINCH之前,一部分新连接可能还是落在旧worker上,由旧版本代码处理。如果你新版本改动很大,想让所有请求尽快切换到新版本,就尽快发送WINCH。
第二,旧worker退出需要时间。如果一个worker手上挂着一堆keepalive长连接,它就迟迟不会退出。我遇到过挂着几百条HTTP长连接的情况,WINCH发下去十几分钟那批worker还在。这时候不要急着QUIT旧master,否则旧worker会被一并带走。但你也不能无限等下去。我个人的判断标准是:核心业务的长连接已经退得差不多了、新版本没有产生新的error日志,就可以QUIT旧master。剩下那些长连接,等用户自然断开后旧worker自然会结束。
第三,如果新版本有问题怎么办。在没有执行QUIT之前,回滚都是安全的,方式如下:
# 向旧master发送HUP信号,旧master会重新拉起worker kill -HUP $(cat /usr/local/nginx/logs/nginx.pid.oldbin) # 向新master发送QUIT信号,关闭新版进程 kill -QUIT $(cat /usr/local/nginx/logs/nginx.pid)HUP信号原本的含义是重载配置,但它也可以让旧master重新fork新的worker。这样旧版本进程就重新接管了连接,新版本优雅退出。
注意:千万不要在升级前手动删掉
nginx.pid.oldbin,在线升级的回滚判断全靠这个文件里的old master pid。有次我在临场操作时误以为pid文件是垃圾文件,差点把它清理掉,后来反应过来吓出一身冷汗。
4. 升级后的验证清单、常见故障与回滚实操
4.1 升级后的验证清单:比curl -I更全的检查项
很多人升级完nginx,curl -I看到200就觉得完事了。但实际上,nginx这种基础组件的变化影响面是整个接入层,验证要做得更立体。我自己整理了一份验证清单,每次升级后照着过一遍:
| 检查项 | 命令/方法 | 预期结果 |
|---|---|---|
| 版本确认 | /usr/local/nginx/sbin/nginx -v | 输出目标版本号 |
| 配置正确性 | /usr/local/nginx/sbin/nginx -t | syntax is ok |
| 监听端口 | ss -lntp | grep nginx | 80/443等端口正常监听 |
| 进程模型 | ps -ef | grep nginx | 1个master+N个worker,无旧版本残留进程 |
| 本地访问 | curl -I http://127.0.0.1/ | 返回预期HTTP状态码 |
| HTTPS握手 | curl -vk https://127.0.0.1/ | TLS握手成功,无证书告警 |
| 站点批量访问 | 用脚本遍历多个业务域名带Host请求 | 全部返回预期状态 |
| 静态资源压缩 | curl -H 'Accept-Encoding: gzip' -I http://127.0.0.1/test.css | Content-Encoding: gzip |
| HTTP/2验证 | curl --http2 -I https://127.0.0.1/ | 能正常响应HTTP/2请求 |
| 错误日志 | tail -f /usr/local/nginx/logs/error.log | 无新增error/warn级别日志 |
| 访问日志字段 | 对比升级前后一条访问日志 | 字段格式一致或符合预期 |
| 反向代理接口 | 通过nginx访问一个后端接口并比对响应 | 与升级前结果一致 |
| 长连接稳定性 | 用WebSocket客户端连接一段时间 | 连接不中断、心跳正常 |
这个清单看着多,实际跑一遍也就几分钟。花这几分钟换来的是一整晚的安心。尤其是访问日志字段这一项,我之前升级完没检查,结果下游的日志采集分析任务当天就报错了——日志字段顺序变了。
4.2 几个容易踩的坑:从编译到运行期的教训
这里把我在历次nginx升级中踩过或见过别人踩的坑集中说一下,每一条都是真实教训。
坑一:编译参数和原来不一致,功能静默丢失。这是最常见的坑。有人升级时只加了--with-http_ssl_module,结果线上原本用着的--with-stream没了,所有四层TCP/UDP代理全部不可用,而错误日志里可能只会出现connect() failed之类的现象,排查起来一头雾水。对策很简单:升级前必须用nginx -V把原来参数原样抄下来,一个都不能少。
坑二:systemd管理下的在线升级会被误杀。如果你的nginx是通过systemd服务启动的,直接用kill -USR2做在线升级,过程里可能出现一个诡异现象:新master刚起来没几秒就被系统杀掉,服务状态变成failed。原因在于systemd默认跟踪主进程PID,而在线升级会改变PID,systemd会认为主进程崩溃了,于是把整个服务都干掉。处理方式有两种:如果你一定要用systemd,升级可以走systemctl restart nginx,等于直接升级的重启版本,会中断连接;如果必须在线升级,要配合systemd的Type=forking和PIDFile配置,让systemd感知PID变化。建议升级前先确认自己服务器用的是裸进程管理还是systemd管理,这个信息决定了你整个升级方案能不能按上面那套信号流程走。
坑三:tengine/OpenResty等衍生版本不能直接套nginx升级。如果你的nginx实际是tengine或者OpenResty,直接下载官方nginx源码去替换二进制,大概率会丢失大量自带module的编译选项,甚至导致配置指令都无法识别。这类衍生版本请到它们自己的官网下载对应版本源码,流程逻辑类似,但不要用官方nginx源码包。
坑四:旧worker迟迟不退导致内存占用翻倍。在线升级期间新旧两套worker并存,内存占用会短期上升。如果服务本身内存就很紧张,这个过程可能触发OOM。建议升级前看一眼free -m,内存不足时优先选择低峰期直接升级,而不是强制在线升级。
坑五:依赖库升级后旧二进制不可用。假如你为了让新版本支持HTTP/3升级了系统的OpenSSL动态库,升级完又因为业务原因回滚到旧nginx二进制,这时旧二进制可能因为动态链接库不匹配而无法启动。这是我在一次回滚操作中亲身经历的情况。所以涉及动态库变更的升级,回滚方案里必须包含对旧二进制可运行性的验证。
4.3 直接升级与在线升级怎么选:我的决策逻辑
很多刚接触这块的朋友会纠结到底用哪种升级方式。我的建议很简单:先问业务能不能承受秒级中断,再问环境支不支持完整编译条件,最后看自己是不是第一次做这类操作。
| 场景 | 推荐方式 | 原因 |
|---|---|---|
| 业务可接受短时中断 / 维护窗口 | 直接升级 | 简单可控、回滚直白 |
| 长连接业务、7x24核心服务 | 在线升级 | 连接不断、无缝切换 |
| 第一次升级、没有完整演练 | 直接升级 | 在线升级信号操作初看容易出错,窗口期状态不好判断 |
| 升级跨度大(跨多个大版本) | 直接升级 | 方便逐步验证,在线升级的新老配置差异更难控制 |
| systemd托管生产环境 | 直接升级或systemctl restart | 避免信号操作与systemd管理机制冲突 |
升级跨度大的场景我要多说一句:如果你是从1.16直接升到1.26这种跨了五六个小版本的情况,强烈建议先在一台测试机上模拟升级,跑一遍配置检查和业务冒烟测试。小版本之间的配置差异积累起来,很可能在某个点出现指令废弃或者默认行为变化,直接在生产上操作风险不可控。
4.4 升级不是一锤子买卖:收尾和记录
升级完成后,最后一步是把现场收拾干净并留下记录。这步很多人会漏掉,但运维的价值往往就体现在这种收尾动作里。
首先确认nginx.pid.oldbin文件已经被清理,旧master进程已经完全消失。然后更新自己的文档:把当前nginx版本号、编译参数、升级时间、执行方式、验证结果记录到运维变更记录里。这样下次再升级时,你手上就有了准确的nginx -V基线,不用再临时去服务器上抓取。
另外,升级后在服务器本地保留一份新版本的源码包和编译产物,不要急着删。我在实际工作中有一个习惯:/usr/local/src/nginx-1.26.x这样带版本号的目录会保留至少一个版本周期。将来如果出现疑难问题,可以直接回到源码目录查看当时的configure选项和编译环境,这是排查问题的一个很重要的参照物。
最后再分享一个我在多次升级中养成的习惯:每次升级前,把nginx -T(大写的T,输出完整配置)导出一份存到本地。升级后的配置如果有任何异常变化,用diff对比一下就知道差异在哪。这套组合拳下来,nginx升级这件事就不需要再提心吊胆了。