最近公司线上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-streamconfigure 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.pid2.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拉起新masterNginx的平滑升级核心靠的是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指向新masternginx.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正则表达式支持、zlibgzip压缩支持、OpenSSLHTTPS支持。不同Linux发行版安装命令不一样。Debian/Ubuntu系apt update apt install -y build-essential libpcre3-dev zlib1g-dev openssl libssl-devCentOS/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 installconfigure成功后会生成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.oldbinnginx.pid里是新master的PIDnginx.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重新拉起workerkill -HUP $(cat /usr/local/nginx/logs/nginx.pid.oldbin)旧master会按照内存里的旧代码重新fork出worker开始接收新连接。注意这里HUP的意义和平时“重新加载配置”不太一样在升级回滚场景下HUP会直接让旧版本的worker重新接管流量。此时新旧两套进程又同时在跑。第二步等确认流量已经切到旧版本后关闭新masterkill -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包后重新configurenginx -t报unknown directive新版移除了指令或对应模块未编译补充编译参数重新编译或修改配置文件升级后HTTPS站点打不开新二进制没编入ssl_module必须加上--with-http_ssl_module重新编译USR2后没有新masterpid文件路径错误或权限不够确认cat的是实际nginx.pid用root执行WINCH后旧worker迟迟不退存在长连接/WebSocket/HTTP2连接等连接自然断开或维护窗口强杀旧workerworker频繁重启配置或模块不兼容查看error.log定位原因必要时回滚systemctl status nginx异常PID指向新mastersystemd状态判断错乱确认进程健康不要盲目restart5.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实例的机器上误杀了别的进程。