接手过不少线上 nginx 升级的活儿,说实话,每次听到“平滑升级”这四个字,第一反应不是激动,而是先出一身汗。尤其是用了编译安装方式部署的 nginx,意味着这台机器大概率承载着核心业务,容不得半点闪失。但平滑升级又是一个不得不掌握的技能——安全补丁要打、新模块要加、老版本被扫描出漏洞要修,总不能每次升级都挑凌晨三四点停机维护吧。
这篇文章就聚焦在 Linux 环境下,用编译安装方式部署的 nginx 怎么做平滑升级。我会从进程原理讲到实际操作,再到失败回滚,把我这些年踩过的坑和养成的习惯全部交代一遍。项目正文里的描述很简洁,但热搜词里那些"nginx配置文件详解""nginx反向代理""nginx 负载均衡配置"等高频词说明大家其实更关心的是:怎么在不动线上配置、不中断服务的前提下,把二进制版本换了,顺带保证原有模块一个不少、配置全部生效。这才是我下面要解决的核心问题。
1. 为什么编译安装的 nginx 必须单独设计升级方案
1.1 包管理器升级与编译安装的本质差异
很多人一开始接触 nginx 升级,第一反应是yum update nginx或者apt upgrade nginx。如果当初是通过 yum/apt 安装的 nginx,这样做确实最省事,因为包管理器会帮你处理二进制替换、依赖更新和服务重启的整套流程。但如果你当初选择的是编译安装——也就是从官网下载源码包,自己 configure、make、make install 的那种——那整条升级路径就要换思路了。
编译安装最大的特点在于:nginx 二进制文件、配置文件、日志目录、模块扩展都散落在你自己指定的路径里,比如常见的/usr/local/nginx,包管理器根本感知不到这个目录的存在。你执行yum update nginx,系统会告诉你没装这个包。即使硬把 rpm 包装上,它管理的也是/etc/nginx下的配置,和你/usr/local/nginx/conf/nginx.conf完全是两套体系,操作不当甚至会搞乱服务器环境。
更关键的是,编译安装的 nginx 通常带着非常个性化的 configure 参数——比如启用了--with-http_v2_module、--with-stream、--with-http_realip_module,或者静态编译了某个第三方模块。而 yum/apt 仓库里的 nginx 二进制是发行版维护者按他们自己的参数集编译的,两者功能集合不一致,你升级完发现某个旧配置指令报"unknown directive",那基本就是编译参数漂移了。
所以,用编译安装方式部署的 nginx,升级就必须走"源码重新编译 + 信号触发切换"这条路,这也是本文标题里括号特别标注"编译安装方式"的原因。
1.2 什么时候必须走编译安装升级这条路
我用实际工作场景来说,以下三种情况基本没有别的选择,只能编译安装升级:
第一种是安全漏洞修复。nginx 官方发布新版本修复了 HTTP/2 或 HTTP/3 相关的 CVE 漏洞,而线上使用的旧版本恰好有风险。等包管理器同步新版本固然稳妥,但大厂的镜像源同步往往有滞后,而且 Red Hat / Ubuntu 的老版本 LTS 可能只会推送补丁版本,不会给你推送最新主线版。
第二种是启用第三方模块或 OpenResty 生态扩展。比如你要接入 lua-nginx-module、nginx-rtmp-module 或 brotli 压缩模块,这些模块基本只提供源码,必须和 nginx 源码一起编译集成,包管理器根本没有对应的二进制包。
第三种是定制化编译选项。比如为了配合国密算法或特定的 OpenSSL 库,我在某金融项目里就遇到过必须指定--with-openssl=/usr/local/openssl-gm的需求,这种场景下 yum 安装的 nginx 根本无法满足。
1.3 升级前必须确认的三件事
说句大实话,平滑升级这件事,真正的风险不在升级动作本身,而在升级前的准备工作。拿这些年带新人的经验来看,我会强制让他们在操作前确认三件事,一个都不能少:
- 先确认当前版本和编译参数:执行
/usr/local/nginx/sbin/nginx -V,把输出完整保存下来,后面新版本 configure 时要尽量保持参数一致。 - 确认线上配置文件语法无误:执行
/usr/local/nginx/sbin/nginx -t,保证nginx.conf和所有 include 的配置文件都通过检查。 - 确认磁盘空间充足:至少保证
/usr/local和源码解压目录所在分区有两倍 nginx 源码包大小以上的剩余空间,否则 make 过程中途报错会让你很被动。
这三件事你花十分钟做完,后面升级就是按部就班的事情。
2. nginx 的进程模型与信号调度:平滑升级的底层逻辑
2.1 master/worker 进程模型与平滑升级的关系
要理解平滑升级为什么能"平滑",必须先看 nginx 的进程模型。nginx 启动后会有两类进程:master 进程和 worker 进程。master 进程是管理者,负责读取配置、绑定端口、fork 出 worker 进程、接收外部信号;worker 进程是实际干活的,负责处理客户端请求、转发代理、处理静态文件。
这里的核心关键在于:master 进程和 worker 进程是分开的,worker 进程之间通过 socket 共享监听端口。也就是说,当 master 进程收到信号要切换二进制时,真正在服务旧请求的是 worker 进程,而 worker 进程不会立刻退出,它会耐心地把当前正在处理的请求处理完,然后才优雅退出。这就给平滑升级留出了操作窗口——新老 worker 进程可以在一段时间内并存,各自承担一部分请求。
所谓平滑升级,本质上就是让 master 进程重新加载一个新的二进制文件,然后用"旧 worker 逐步退出、新 worker 逐步接管"的方式完成版本切换。整个过程从客户端角度看,TCP 连接不会断开,请求不会中断,最多只是新连接被新版本接管、旧连接被老 worker 处理完为止。
2.2 信号指令对照表:USR2、WINCH、QUIT 分别做了什么
nignx 对信号的处理是平滑升级的"指令集",那组信号就是我们要手动敲给 master 进程的命令。我在实际操作中只用这几个,整理成表格方便对照:
| 信号 | 作用 | 使用场景 |
|---|---|---|
HUP | 重载配置文件,平滑重启worker进程 | 配置变更但不换版本 |
USR2 | 启动新的master进程和新的worker进程 | 平滑升级的核心信号,新老master并存 |
WINCH | 逐步关闭旧的worker进程 | 升级切换后让老worker优雅退出 |
QUIT | 优雅退出,处理完当前请求后关闭进程 | 关闭老master进程或某个进程 |
TERM/INT | 快速退出,不等待请求处理完 | 非平滑场景或应急关闭 |
USR1 | 重新打开日志文件 | 日志切割场景 |
平滑升级的主要流程就是围绕USR2、WINCH、QUIT这三个信号展开的。USR2让旧的 master 进程以一个新文件名启动新的 master 进程,此时新旧两个 master 同时存在,旧的 worker 还在处理请求,新的 worker 也已经开始接管新连接。WINCH信号发给旧 master,让它优雅地关闭它的 worker 进程,但保留旧 master 进程本体,这样一旦发现问题还能回滚。QUIT发给旧 master,才算彻底把旧 master 生命周期的收尾工作做完。
2.3 为什么不推荐用 HUP 信号走完整个升级流程
你可能听说过,kill -HUP <nginx_pid>可以重载配置,并且重新启动 worker 进程。确实,这个信号在日常配置变更时很好用,但在二进制版本升级的场景下,我完全不推荐只用 HUP。原因在于,HUP 只会让 master 进程重新读取配置文件并重新 fork worker 进程,但 master 进程自己还在运行旧版本的二进制文件。也就是说,master 进程还持有旧代码,而 worker 进程已经变成新代码了,这种"新老混血"的状态会带来不可预测的行为,尤其是在 master 进程需要派生新 worker 或者处理某些全局逻辑时。
更重要的是,HUP 升级后如果发现问题,回滚的路径很别扭。因为你没法单纯通过 HUP 把 worker 进程的版本降回旧版本,必须把旧 master 迁回来,操作链路不干净。而利用 USR2 + WINCH + QUIT 这套标准组合拳,回滚路径是天然设计好的——旧 master 只是处于"休眠"状态,随时可以唤醒。所以,千万别图省事用 HUP 替代真正的平滑升级流程。
3. 编译新版本前的准备:参数对齐与源码校验
3.1 从旧版本导出完整 configure 参数
新版本源码下下来之后,第一件事不是急着./configure,而是先拿旧版本的编译参数。我一般是这么操作的:
# 查看当前 nginx 版本号和编译参数 /usr/local/nginx/sbin/nginx -V # 输出示例 # nginx version: nginx/1.20.2 # built by gcc 4.8.5 20150623 (Red Hat 4.8.5-44) (GCC) # configure arguments: --prefix=/usr/local/nginx --with-http_ssl_module --with-http_v2_module --with-stream --with-http_realip_module --with-http_stub_status_module把configure arguments这一行完整复制出来,这是你配置新版 nginx 时的基准参数。很多人容易忽略的问题在于:新版 nginx 源码包可能有新的编译选项,比如旧版没有--with-compat,新版为了支持动态模块建议加上,但如果你不确定这些新增选项是否会影响现有配置,就保守一点,先保持和旧版本完全一致的参数集,升级成功后再考虑增量开启新功能。
3.2 编译参数常见差异与模块完整性问题
我自己踩过这样一个坑:早期编译某版本 nginx 时用的是系统自带的 PCRE 库,后来换了一台机器去升级,系统库版本比较新,新版 nginx 的 configure 居然自动探测到了不同的 PCRE 路径,结果编译出来的 nginx 在解析原有正则 location 时行为出现了细微差异。后来我学乖了,在 configure 时尽量显式指定依赖库路径,避免系统自动探测带来的不确定性。
另一个高频问题是第三方模块。如果你的旧 nginx 用了某个第三方模块,比如nginx-rtmp-module或者lua-nginx-module,你必须确认该第三方模块与新版本 nginx 的兼容性。最稳妥的办法是先看模块的官方文档有没有声明支持到哪个 nginx 主线版本,然后在新版本源码解压目录下先做一次完整的 configure + make,确认没有编译错误再继续。如果第三方模块代码较老,可能会因为 nginx 内部 API 变动而编译失败,这时候你需要提前找替代方案或补丁,而不是等到升级现场才临时抱佛脚。
3.3 依赖库校验:pcre、openssl、zlib
编译 nginx 依赖的库主要是 PCRE、OpenSSL 和 zlib。PCRE 负责正则表达式解析,OpenSSL 提供 TLS/SSL 支持,zlib 负责 gzip 压缩。这三者的版本直接影响你后续使用的功能范围和安全性。
我建议在新机器或新环境上编译时,先用命令确认这些库的版本:
# 检查系统已安装的动态库 pcre-config --version 2>/dev/null || rpm -qa | grep pcre openssl version zlib-config --version 2>/dev/null || rpm -qa | grep zlib如果你需要特定版本的 OpenSSL 来支持 TLS 1.3 或国密算法,那就要下载 OpenSSL 源码包,然后在 configure 时通过--with-openssl=/path/to/openssl-src指定。这样 nginx 会编译时静态链接该 OpenSSL 源码,不会受系统库版本干扰。不过这么做也有代价,静态链接后系统升级 OpenSSL 库也不会影响 nginx,但你需要自己关注 OpenSSL 的安全公告,及时重编译 nginx。
3.4 编译与安装路径问题
编译安装的细节直接决定升级动作成败,这里有个关键认知要提前建立:nginx 的 configure 参数里,--prefix指定的是安装根目录,默认是/usr/local/nginx。而make install会向这个目录写入二进制、配置文件和 html 静态文件。麻烦点在于,如果你直接在新版本源码目录里make install,它会覆盖掉原目录下的nginx.conf和 html 页面,这是绝对不可接受的。
我个人的做法是,永远只把make install当作"安装到临时目录"的辅助手段,真正生产环境升级时根本不用make install,而是手动将编译生成的objs/nginx二进制文件复制到安装目录。这样能最大程度保护配置文件和静态资源。后面第 4 部分我会详细拆解这个过程。
源码解压的路径也有讲究,我习惯放在/tmp/nginx-build/下,并且不同版本用不同子目录隔离,比如/tmp/nginx-build/nginx-1.22.1/。这样即使编译失败或要清理,也不会影响目录结构。
4. 平滑升级完整操作流程:从备份到信号切换
4.1 备份旧版本二进制与配置
这是整套流程里我最不厌其烦强调的一步。很多教程会说"平滑升级很安全,不用备份",但我见过太多因为没备份而翻车的案例。备份其实只需要两三条命令,却能让你在出问题时少流三小时的汗。
# 1. 备份当前 nginx 二进制 cp /usr/local/nginx/sbin/nginx /usr/local/nginx/sbin/nginx.bak.$(date +%Y%m%d) # 2. 备份整个配置目录 cp -r /usr/local/nginx/conf /usr/local/nginx/conf.bak.$(date +%Y%m%d) # 3. 确认备份成功 ls -l /usr/local/nginx/sbin/ ls -l /usr/local/nginx/conf.bak.* | head这里有个细节值得注意:备份二进制时,我建议把nginx和nginx.bak.*放在同一个目录下,这样后续通过信号切换时,旧 master 进程依赖的路径语义保持一致。如果放到其他目录,某些老版本 nginx 在启动新 master 时会因为找不到同目录下的某些辅助文件而出问题。
4.2 编译新版 nginx 但不覆盖现有目录
备份完成后,回到源码目录进行配置和编译。我的习惯是先make,不make install,这样只生成二进制,不动任何目录文件。
# 进入新版源码目录 cd /tmp/nginx-build/nginx-1.22.1/ # 使用旧版本的 configure 参数,加上必要的路径定义 ./configure \ --prefix=/usr/local/nginx \ --with-http_ssl_module \ --with-http_v2_module \ --with-stream \ --with-http_realip_module \ --with-http_stub_status_module \ --with-pcre=/opt/pcre-8.45 \ --with-zlib=/opt/zlib-1.3 \ --with-openssl=/opt/openssl-3.0.13 # 编译,生成 objs/nginx make -j4编译完成后,先别急着覆盖线上二进制。在临时环境验证一下编译产物的版本和配置加载情况是必要的。可以用-t参数测试新版二进制对配置文件的兼容性。但由于--prefix=/usr/local/nginx,新版 nginx 默认会使用该目录下的conf/nginx.conf,而我们已经备份过配置,可以直接测试:
# 测试新二进制加载现有配置是否正常 /usr/local/nginx/sbin/nginx -t我建议至少多跑几遍nginx -t,确保输出是syntax is ok和test is successful两行提示。如果配置里用到第三方模块的指令,而你在 configure 时没带上对应模块,这个测试阶段就会报错,提前暴露问题。
4.3 通过信号完成新旧进程切换
干净利落的验证通过后,正式进入切换环节。我习惯把切换拆成四步,每步之间留出观察时间,不要一口气执行完,否则出故障时很难定位是哪一步引起的。
第一步,备份旧版二进制文件为当前生效名。严格来说我们前面已经备份了,但有些团队会把nginx.bak.20250101和nginx.bak.20250201混淆,所以切换前我习惯只保留一个明确的当前二进制副本:
# 若前面已备份则跳过,否则立即备份 cp -f /usr/local/nginx/sbin/nginx /usr/local/nginx/sbin/nginx.bak.latest第二步,用新编译的二进制覆盖当前的 nginx 文件:
# 覆盖二进制文件 cp -f /tmp/nginx-build/nginx-1.22.1/objs/nginx /usr/local/nginx/sbin/nginx # 确认文件类型和版本 file /usr/local/nginx/sbin/nginx /usr/local/nginx/sbin/nginx -V注意,这一步执行完,磁盘上的 nginx 已经变成新版本了,但当前运行的进程还是旧版本,它们互不影响。这也是平滑升级能成立的关键前提——你替换的是磁盘文件,而不是正在运行的进程镜像。
第三步,向当前运行的旧 master 进程发送 USR2 信号:
# 获取旧 master 进程 PID export OLD_MASTER_PID=$(cat /usr/local/nginx/logs/nginx.pid) echo "old master pid: ${OLD_MASTER_PID}" # 发送 USR2 信号给旧 master kill -USR2 ${OLD_MASTER_PID}发送 USR2 后,旧 master 会以当前磁盘上的新二进制为模板,启动一个全新的 master 进程。此时系统中应该是两个 master 进程并存,新 master 会读取同一个nginx.conf,重新绑定监听端口,并 fork 一批新的 worker 进程。因为旧 worker 还占着连接,新 worker 会和旧 worker 共享同样的监听 socket,内核的负载均衡会把新的请求分配到新 worker 上。
这个时候,旧 master 的 PID 会记录在logs/nginx.pid.oldbin文件中(有些版本是自动生成的),新 master 的 PID 会覆盖写入logs/nginx.pid。你可以用ps -ef | grep nginx看到一排进程,其中新旧 worker 混在一起,那个场面我第一次见的时候还挺紧张,担心是不是配置冲突了,其实这是正常的中间状态。
第四步,观察无异常后再优雅关闭旧 worker 进程:
# 确认新旧 master 都在运行,然后发送 WINCH 给旧 master kill -WINCH ${OLD_MASTER_PID}发送 WINCH 后,旧 master 会通知它的所有 worker 进程处理完手头请求后退出。这个过程可能持续几秒到几十秒,取决于线上是否有长连接或慢请求。你可以通过ps -ef | grep nginx观察旧 worker 数量逐步减少,最终只剩下一个新 master 和新 worker。旧 master 进程本身不会退出,这是故意保留的"后门"。
4.4 收尾操作与旧 worker 进程退出确认
看到旧 worker 全部退出后,不要急着高兴,先确认一下新版本的运行状态:
# 确认当前运行的 nginx 版本 /usr/local/nginx/sbin/nginx -v # 实际运行进程中的 master 版本确认 ps -ef | grep "master process" # 当前进程状态理想情况下应该是: # nginx: master process /usr/local/nginx/sbin/nginx # nginx: worker process再检查一下端口监听和访问日志,确认新连接能正常建立:
# 查看监听端口状态 ss -lntp | grep 80 curl -I http://127.0.0.1/ tail -f /usr/local/nginx/logs/access.log确认线上 Web 服务正常后,才算真正完成升级。最后一步是发送 QUIT 给旧 master,彻底退出旧进程:
# 如果确认新版本一切正常,退出旧 master kill -QUIT ${OLD_MASTER_PID} # 清理旧 master 残留的 pid 文件(如存在) ls -l /usr/local/nginx/logs/*.pid这里要特别强调:如果新版本有问题,绝对不要发送 QUIT,而是要把回滚流程执行完,第 5 部分我会细讲。
5. 升级后的验证与一键回滚方案
5.1 升级后必须检查的指标
升级完成不等于万事大吉。我以前就经历过"升级很顺、隔天发现监控报警"的尴尬,后来学乖了,每次升级后强制自己按清单检查几项硬指标。
第一项是请求成功率。升级后至少观察 5 到 10 分钟,看看 nginx 自身的access.log里有没有异常的大面积 4xx/5xx 状态码。如果服务有上游后端的话,同时看后端服务的日志,确认 nginx 与后端的连接没有异常断开。
第二项是错误日志。盯着error.log,重点看有没有[emerg]、[alert]、[crit]级别的输出。平滑升级后偶尔会出现[warn]级别提示,比如配置项在新版本已被标记为废弃,这类提醒不致命,但要想清楚是否处理。
第三项是 worker 进程数量和连接数。ss -s或者 nginx 的 stub_status 模块输出,对比升级前的 worker 进程数与连接数,正常情况下不会出现数量级差异。如果新 worker 进程数异常多或异常少,说明 worker_processes 配置或 cpu 亲和性设置在新版本里的行为有变化,要及时排查。
第四项是定时任务的异常。如果这台机器上还有日志切割脚本、监控采集脚本或者其他依赖 nginx 二进制文件路径的程序,升级后跑一遍这些脚本,确保它们调用的是/usr/local/nginx/sbin/nginx -s reload或者nginx -t这类命令时不会因为新版本的输出格式变化而出错。
5.2 回滚的完整命令序列
哪怕做得再仔细,回滚方案也必须有。回滚的原理其实很简单:让旧 master 进程重新接管流量,退出新 master。具体命令序列如下:
首先,在发送 WINCH 之后、发送 QUIT 之前,如果你发现问题,还没有把旧 master 杀掉,那么回滚很安全。执行kill -HUP ${OLD_MASTER_PID}唤醒旧 master 进程,让它重新 fork 一批旧版本 worker 进程。此时旧 worker 和新 worker 并存,流量会回到旧版本的处理逻辑中。接着用kill -QUIT关闭新 master:
# 恢复旧 master 的 worker 进程 export OLD_MASTER_PID=$(cat /usr/local/nginx/logs/nginx.pid.oldbin) kill -HUP ${OLD_MASTER_PID} # 关闭新版 master 和它的 worker kill -QUIT $(cat /usr/local/nginx/logs/nginx.pid)但如果已经发送了 QUIT 给旧 master,那旧 master 彻底退出了,回滚方式只能靠启动备份的旧二进制:
# 用备份的旧二进制替换当前二进制 cp -f /usr/local/nginx/sbin/nginx.bak.latest /usr/local/nginx/sbin/nginx # 用旧二进制启动一个全新的 nginx(内部会 fork 新 master 和新 worker) /usr/local/nginx/sbin/nginx -t && /usr/local/nginx/sbin/nginx # 关闭当前运行的新版 master kill -QUIT $(cat /usr/local/nginx/logs/nginx.pid 2>/dev/null)注意这种回滚会导致短暂的停机窗口,所以最稳妥的回滚时间点永远在"发送 QUIT 之前"。这也是我为什么反复强调,网上很多教程把最后一步 QUIT 写得非常随意,这就埋下了隐患。
5.3 快速回滚的脚本化思路
考虑到生产环境的紧张程度,我把回滚流程固定成了一个脚本,放在/usr/local/nginx/rollback.sh,平时不执行,出问题时一跑就行。脚本的逻辑就是把上面的命令串起来,加上日志输出和状态判断。这个脚本的重点是明确当前是"已发 WINCH 未发 QUIT"还是"已发 QUIT",因为操作路径完全不同,不能让运维在半夜三更去翻教程。
#!/bin/bash # 回滚 nginx 升级 set -e NGINX_DIR=/usr/local/nginx LOG_FILE=/var/log/nginx_rollback.log echo "$(date '+%F %T') start nginx rollback" >> ${LOG_FILE} # 判断 oldbin 文件是否存在 if [ -f "${NGINX_DIR}/logs/nginx.pid.oldbin" ]; then OLD_MASTER_PID=$(cat ${NGINX_DIR}/logs/nginx.pid.oldbin) echo "$(date '+%F %T') old master pid: ${OLD_MASTER_PID}" >> ${LOG_FILE} kill -HUP ${OLD_MASTER_PID} sleep 2 kill -QUIT $(cat ${NGINX_DIR}/logs/nginx.pid) echo "$(date '+%F %T') rollback to old version done" >> ${LOG_FILE} else echo "$(date '+%F %T') oldbin pid not found, restoring binary" >> ${LOG_FILE} cp -f ${NGINX_DIR}/sbin/nginx.bak.latest ${NGINX_DIR}/sbin/nginx ${NGINX_DIR}/sbin/nginx -t kill -QUIT $(cat ${NGINX_DIR}/logs/nginx.pid) sleep 2 ${NGINX_DIR}/sbin/nginx echo "$(date '+%F %T') restore binary and start service done" >> ${LOG_FILE} fi这个脚本我实际用过好几次,最典型的场景是升级到某个新版本后,客户端突然出现大量 TLS 握手超时,一查是因为新版本默认的 SSL 配置对加密套件的要求更严格,线上还有些老客户端不兼容。我直接跑脚本回到旧版本,然后从容调整配置后再次升级,整个过程也就一分钟的事。
6. 实战中容易踩的坑与排查思路
6.1 编译参数不一致导致的模块静默丢失
这个坑我见得最多,坑得也最狠。某次升级 nginx 后,所有页面突然开始 404,翻看error.log发现是rewrite相关规则全部失灵。排查半天才意识到,旧的编译参数里带了--with-pcre-jit,而新版本 configure 时我没带这个参数,JIT 编译支持丢失后,某些正则重写规则在新版本里走了不同的处理路径,最终出现了地址无法匹配的问题。
所以,升级后的验证阶段,一定要做一次全站链接扫描或核心接口回归。你可以在升级前后各跑一次相同的自动化测试用例,把核心的 rewrite、proxy_pass、负载均衡策略、SSL 证书加载这些逻辑全部覆盖到。不要因为nginx -t通过就放松警惕,nginx -t只检查配置语法,不检查行为一致性。
6.2 升级后配置直接报错的场景
有时候新版本的指令集会发生变更,旧配置里用了某个在新版本中已被移除或改名的指令。常见的有ssl指令在新版本中被要求放到server块,或者http2的开启方式从listen 443 ssl http2改成了listen 443 ssl; http2 on;这样的变化。这类问题基本在nginx -t测试时就会暴露出来,所以每次编译新版本后,用新二进制-t检查线上配置这个动作绝不能省。
更隐蔽的情况是配置语法没问题,但某些模块在新版本中默认行为变了。比如旧版本proxy_set_header的默认行为可能在新版本里有所不同。这时候最有效的排查方式不是翻 changelog,而是对比新旧两个二进制在相同配置下的行为差异。我在升级时习惯保留一份旧版本的完整配置副本,出现问题后直接跑nginx.bak.latest -t来对照检查,就能快速定位是配置兼容性还是模块缺失的问题。
6.3 磁盘空间不足与编译中途失败
不要小看磁盘空间这个问题。nginx 源码包本身不大,但解压后的源码目录加编译中间产物,还有你指定的 OpenSSL、PCRE 源码目录,加起来很容易超过 2GB。如果你的/tmp分区很小,或者/usr/local所在根分区空间不足,make会在链接阶段报No space left on device。这种报错会清空你当前编译的中间产物,重新 make 往往又能过,因为有些临时文件被清理了,但如果你没注意磁盘已满,反复尝试只会反复失败。
我在新机器上编译前习惯看一遍分区空间:
df -h /tmp /usr/local /opt如果空间确实紧张,不要犹豫,直接把源码目录放到一个空间充足的大分区下,比如/data/build/nginx-1.22.1/。同时把 configure 时--with-openssl指向的源码目录也放到同一分区,避免跨分区链接导致性能下降或路径权限问题。
6.4 升级后的依赖库动态链接问题
有时候你只是升级了 nginx,但 nginx 依赖的动态库发生变化,会导致新二进制启动时报error while loading shared libraries。最常见的场景是libssl.so、libpcre.so这些库的版本号不匹配。比如系统里只有libssl.so.1.1,而新编译的 nginx 链接时依赖libssl.so.3,运行时就会加载失败。
排查命令很简单:
ldd /usr/local/nginx/sbin/nginx | grep "not found"如果发现某个.so报 not found,说明当前系统的动态库版本和编译时指定的版本不一致。解决办法要么是让系统安装对应版本的动态库,要么是在编译时全部静态链接。个人建议 nginx 编译时尽量把 OpenSSL、PCRE、zlib 都用源码静态链接进去,这样运行时就不会受系统库升级的影响,复制到同架构的其他机器上也能直接跑。
6.5 平滑升级对上游连接的影响
平滑升级虽然对客户端友好,但对上游后端的连接还是会有微小影响。新 worker 进程启动后,它与后端的 keepalive 连接池是空的,需要重新建立连接。如果后端服务连接数限制严格,新建连接的一瞬间可能会触发后端的连接数峰值。在节假日或业务高峰时期,我会避免在这个窗口期升级,或者提前通知后端团队关注连接数变化。
这个问题在大型代理场景下尤其明显。我之前处理过一个日请求量上亿的 nginx 集群,每次升级切换时后端服务会短暂观察到新建连接数翻倍,影响不大,但如果同时多个节点一起升级,后端就有压力了。所以现在我在生产环境做这种升级时,会制定分批策略:每台升级完观察一段时间,确认无误后再升级下一台,避免所有节点同时切换造成后端连接风暴。
6.6 动态模块的加载路径问题
如果你的 nginx 在编译时启用了动态模块,也就是 configure 时带了类似--add-dynamic-module=/path/to/module的参数,那么升级时还要额外注意.so文件的版本一致性。动态模块是和 nginx 主程序一起编译的,模块文件内部记录了它依赖的 nginx 版本和 ABI 接口信息。直接用旧版本的 .so 挂到新版本 nginx 上,启动时大概率会报module is not binary compatible。
这种情况下的处理方式是把新版本动态模块的 .so 文件一并编译并复制到对应目录,同时确认配置中load_module指令指向的是新 .so 文件路径,而不是旧路径。如果新旧版本跨度较大,建议先看官方 changelog 确认动态模块接口是否有变更,避免升级后某个特性静默失效。
写在最后的个人习惯
每次升级完成后,我都会在/usr/local/nginx下新建一个UPGRADE_LOG文件,把本次升级的时间、源版本、目标版本、configure 参数、操作人、回滚结果全部记录进去。这个习惯一开始觉得多余,但后来排查问题或者交接给其他同事时,这份日志省了大功夫。另一个小技巧是升级后把当前生效的二进制做一个 md5 记录,方便后续做安全基线比对。
平滑升级这件事,说到底是把"换文件"和"切进程"拆成两个可独立操作的步骤,再靠 nginx 的信号机制把切换过程做得足够平滑。这篇内容看着命令不多,但每一步背后都有考究,真正动手时建议先把本文第 3、4、5 部分通读两遍,在测试环境完整演练一次,再去生产操作。毕竟 nginx 这种基础设施,稳一小时不如稳一年。