
老早之前就在琢磨一个问题线上Nginx正在跑着业务升级版本的时候到底能不能做到服务不中断试过的人都知道直接把旧进程停了再启新的那几秒钟的502就够监控告警炸半天。后来折腾了一次Nginx平滑升级把这块彻底搞明白了今天就把完整过程写出来包括原理、步骤、踩过的坑照着做基本不会出问题。适合的人群大概是自己维护服务器、需要在生产环境升Nginx版本、但不想承担停机风险的运维和开发同学。如果你是第一次操作建议先在一台测试机器上完整跑一遍流程再上生产。1. 平滑升级为什么能平滑master-worker进程模型和信号机制要理解平滑升级先得搞明白Nginx的进程架构。Nginx启动后会有两类进程一个master主进程和一堆worker工作进程。master进程不处理请求它的活儿是读取配置文件、管理worker进程的生命周期真正干活的是worker进程负责accept连接、处理HTTP请求、转发反向代理等。平时我们执行nginx -s reload能实现配置热加载而不中断服务靠的也是这个架构master进程收到HUP信号后会重新读取配置文件然后启动新的worker进程再让旧的worker进程优雅退出。所谓优雅退出就是不再接收新请求但已经建立连接的处理完再退不会粗暴掐断正在进行的请求。平滑升级的原理和reload类似但更彻底——因为要换掉的是master进程本身。整个升级过程涉及四个关键信号信号作用备注USR2启动新版本的master进程和worker进程新旧master会短暂共存WINCH让旧master的worker进程优雅退出旧连接处理完才退出QUIT退出旧的master进程此时旧进程完全退出HUP重载配置文件或回滚回滚时会用到整个升级窗口期新老进程会同时存在一段时间新的master和新的worker开始处理新连接的请求老的worker则慢慢把手头连接处理完。你从外部看服务一直处于可用状态这就是平滑升级的魔力。实际升级过程中一个连接甚至可能前半段由老worker处理后半段由新worker接手吗这个基本不用担心因为旧worker只接收之前已经建立的连接新连接全走新worker而一个HTTP请求的生命周期通常很短秒级就结束了所以实际影响范围很小。2. 升级前的关键前提把旧版编译参数原样背下来很多人在平滑升级时翻车不是流程不对而是编译参数掉了模组。Nginx很多功能是编译时通过configure参数决定要不要的比如SSL模块、Stream模块、Stub Status模块。升级时如果只想着把源码跑一遍编译安装忘了带上原来所有的编译参数那么升级后轻则少功能重则SSL直接不能用了。查看当前Nginx的编译参数执行nginx -V输出中是configure arguments这一行比如configure arguments: --prefix/usr/local/nginx --with-http_ssl_module --with-http_stub_status_module --with-http_realip_module --with-stream --with-stream_ssl_module这一步一定要复制保存好。后面./configure的时候这些参数一个都不能少。还有一个小细节如果当初是用官方源或者系统包管理器装的Nginxnginx -V输出里没有configure arguments那么建议直接用nginx -v先确认版本号再通过官方文档确认默认编译包含的模块这里各发行版的包管理默认参数不一样千万别想当然。给一个实际升级示例。假设旧版本是1.24.0要升级到1.26.2旧版本编译参数是./configure --prefix/usr/local/nginx --with-http_ssl_module --with-http_v2_module --with-http_realip_module --with-stream --with-stream_ssl_module那么升级的时候configure命令要写成完全一样的只是源码换了新版本。另外要确认几个编译依赖是否还在gcc、make、libpcre3-dev、zlib1g-dev、libssl-dev。这些在初次安装时一般都有但如果服务器做了裁剪或重置过环境编译时会报错可以先补装apt-get install -y gcc make libpcre3-dev zlib1g-dev libssl-devCentOS系对应的是yum install -y gcc make pcre-devel zlib-devel openssl-devel。为什么这些依赖这么重要因为configure脚本会检查这些库是否可用少了任何一个编译时都有可能报错或者产生功能残疾的Nginx。我之前就遇到过一次升级后才发现openssl版本太旧导致新编译的Nginx无法使用HTTP/2折腾了很久才找到原因。3. 完整升级操作步骤从编译到信号切换3.1 下载源码与备份现有二进制首先下载新版本源码并解压同时把当前正在跑的Nginx二进制备份出一份以防升级失败后需要回滚。老版本Nginx的二进制路径一般是/usr/local/nginx/sbin/nginx如果用的包管理方式安装路径可能不同先通过which nginx确认。# 下载新版本源码以1.26.2为例 wget https://nginx.org/download/nginx-1.26.2.tar.gz tar -zxvf nginx-1.26.2.tar.gz cd nginx-1.26.2 # 备份当前旧版本二进制 cp /usr/local/nginx/sbin/nginx /usr/local/nginx/sbin/nginx.old备份出的nginx.old就是将来回滚的救命稻草。注意光备份二进制还不够如果之前的配置文件、日志目录或临时文件有变化也要一起考虑但正常情况下配置文件路径不会变备份二进制是最关键的。这里补充一句线上如果跑着多个Nginx实例不同配置文件目录通过nginx -V查看不够要用ps aux | grep nginx确认master进程的-c参数指向哪个配置文件以及nginx -t -c 配置文件路径来测试配置。多实例环境操作时要特别小心信号对应的PID别发错进程了。3.2 编译新版本但先不要make install进入新版本源码目录后执行带有完整旧参数的configure./configure --prefix/usr/local/nginx --with-http_ssl_module --with-http_v2_module --with-http_realip_module --with-stream --with-stream_ssl_module执行完没有报错后运行编译make -j$(nproc)这里注意千万不要马上执行make install。因为make install会直接覆盖安装目录下的二进制文件如果当前正在运行的master进程还在使用旧版本文件句柄覆盖后可能引发不可预期的问题而且无法利用Nginx自身的平滑升级机制。正确思路是编译得到新二进制然后手动控制替换时机。编译完成后新二进制在源码目录下的objs/nginx。可以确认一下版本./objs/nginx -v看到输出是新版本号说明编译成功。3.3 用新二进制替换旧二进制接下来把新编译好的二进制拷贝到Nginx安装目录覆盖旧的sbin/nginxcp objs/nginx /usr/local/nginx/sbin/nginx建议先备份再覆盖虽然刚才已经备份过nginx.old但此时会再强调一次因为很多人在这一步犹豫旧的master还在跑覆盖会不会有影响答案是不会——master进程已经在内存中加载了旧二进制运行期间不依赖磁盘上的文件覆盖完成后它还是会按照旧代码逻辑跑完自己的生命周期。新连接由之后启动的新master进程处理。3.4 向旧master发送USR2信号启动新master这一步是平滑升级的核心。先找到当前Nginx master进程的PIDps -ef | grep nginx | grep master输出类似root 1234 1 0 10:00 ? 00:00:00 nginx: master process /usr/local/nginx/sbin/nginx然后向它发送USR2信号kill -USR2 1234这个信号会触发旧master做两件事一是把nginx.pid文件重命名为nginx.pid.oldbin二是启动一个使用新二进制的全新master进程。此时新旧两个master是并存的新master的PID会记录在nginx.pid中旧master的PID在nginx.pid.oldbin中。怎么验证新master是否成功启动执行ps -ef | grep nginx正常情况下会看到两个master进程其中一个是新的。也可以用cat /usr/local/nginx/logs/nginx.pid和cat /usr/local/nginx/logs/nginx.pid.oldbin分别查看新旧PID。这里有个容易忽略的点新master进程启动时也会读取配置文件并测试如果配置文件有问题新master启动会失败此时旧master仍然正常工作但你需要检查error.log确认失败原因。所以升级前强烈建议先执行nginx -t测试配置语法。3.5 发送WINCH信号让旧worker优雅退出新master已经接管了新连接的请求但旧master和它的worker进程还在运行。要优雅地退出旧worker向旧master发送WINCH信号kill -WINCH 旧master的PIDWINCH信号会让旧master停止启动新的worker并通知现有worker处理完手头连接后退出。这一步执行后观察进程ps -ef | grep nginx你会看到新的worker进程以及还在收尾的旧worker进程。如果业务量很大旧worker可能需要几分钟才能全部退出这期间服务仍然正常不用着急。等旧worker全部消失后整个系统就只有新master和新worker了。如果某些worker一直没有退出说明上面有长时间未完成的连接。可以用ss -tnp | grep nginx查看连接情况确认是哪个连接占着。大部分情况下都是长连接或websocket处理完自然退出。实在等不了可以继续执行下一步退出旧master的流程旧master退出时如果有残留worker会被强制清理但这种操作不推荐能等就等。3.6 向旧master发送QUIT信号完全退出旧worker全部退出后旧master就等于光杆司令了发送QUIT信号让它优雅退出kill -QUIT 旧master的PID执行后再次检查进程ps -ef | grep nginx如果列表里只有新master和它的worker说明升级流程完整结束。顺带说一句make upgrade这个官方目标会自动完成USR2、WINCH、QUIT这套信号操作但升级前如果已经手动改了二进制或想自己控制节奏建议还是手工发信号。手工流程虽然多几步但每一步都可以确认状态踩坑概率更低。4. 升级后的验证清单与回滚方案4.1 升级成功的基本验证升级完成后不能光看进程要把功能验证一遍。我的常规验证清单包括版本确认/usr/local/nginx/sbin/nginx -v确保输出新版本号。进程确认ps -ef | grep nginx只有新master和对应worker。端口确认ss -tlnp | grep 8080端口仍然正常监听。功能确认curl -I http://127.0.0.1/响应头中Server: nginx/1.26.2是新版本号。日志确认tail -n 20 /usr/local/nginx/logs/error.log升级期间如果有异常会有记录。证书确认如果有HTTPS站点curl -kI https://127.0.0.1/确认SSL握手正常。这些检查项建议按顺序执行每一项都有意义。版本号对了不代表功能正常证书和代理都测一遍才算完整。如果服务器上还挂了反向代理建议访问几个实际业务接口确认代理转发正常。4.2 升级失败如何回滚回滚有两种场景取决于旧master是否还活着。场景一旧master还活着。如果你在升级过程中发现新版本有问题或者新master启动后配置异常此时旧master还在跑发送QUIT之前直接向旧master发送HUP信号让它重新读取配置文件并启动新的worker进程然后向新master发送QUIT信号退出新master这样整个系统就回到旧版本了。# 旧master PID假设是1234 kill -HUP 1234 # 新master PID查看nginx.pid假设是5678 kill -QUIT 5678这个回滚方式不需要动磁盘上的二进制文件因为旧master内存中加载的还是旧版本代码。前提是旧master还没退出。场景二旧master已经退出。那只能用备份的nginx.old二进制覆盖回去cp /usr/local/nginx/sbin/nginx.old /usr/local/nginx/sbin/nginx kill -USR2 新masterPID # 触发使用旧二进制启动新master不过这块信号对象要想清楚操作顺序是先把旧二进制恢复回去然后向当前正在运行的master发送USR2前提是它在配置文件中能找到nginx.old对应的二进制路径但更稳妥的是直接停掉当前master然后用恢复好的旧二进制正常启动服务。# 先停止当前master记得收集好PID kill -QUIT 当前masterPID # 恢复二进制后正常启动 /usr/local/nginx/sbin/nginx这种先停再启的方式严格来说不算平滑会有一个服务中断窗口。所以最理想的回滚窗口是场景一建议先把命令行工具准备好一旦新版本有异常立刻回滚别给故障留时间。5. 实测中容易踩的坑SSL模块丢失、pid路径问题与进程假死5.1 SSL模块丢失导致证书不生效这个坑我印象最深。某次升级后站点直接无法通过HTTPS访问浏览器报ERR_SSL_PROTOCOL_ERROR而不是证书错误。排查半天发现nginx -V输出的编译参数里没有--with-http_ssl_module。原因就是当初configure的时候图省事没带上这个参数编译出来的新Nginx根本不支持SSL。这个场景和很多人在nginx替换ssl证书不生效这个问题上卡住的原理一样——证书配置了、重启了但Nginx编译时压根没有SSL模块配置指令都识别不了自然就不生效。排查方法很简单nginx -V看是否有--with-http_ssl_module如果没有需要重新configure加上这个参数再编译。所以升级前一定要仔细核对nginx -V的每一段参数可能是最不起眼但最致命的一环。5.2 nginx.pid路径和权限问题平滑升级过程中nginx.pid的重命名和重建牵涉到目录写权限。如果Nginx运行用户对logs目录没有写权限USR2信号处理时可能无法创建nginx.pid.oldbin导致新master启动流程卡住。这种情况在非root用户运行Nginx时尤其常见。建议升级前手动检查logs目录权限ls -ld /usr/local/nginx/logs确保运行用户对该目录有写权限。另外多实例部署时每个实例的pid文件路径通过配置文件指定比如pid /run/nginx-8080.pid发信号前要确认用的是正确的pid文件路径别把另一个实例的master杀了。5.3 升级过程中的进程假死USR2信号发出去后偶尔会遇到新master没起来、旧master也没退出像是卡住的情况。这种情况八成是配置文件写到了新master读取的位置但存在错误新master启动校验失败。用tail -f观察error.log几秒钟内就能看到具体的报错行。另外有一个冷门情况如果旧Nginx是由systemd管理的kill -USR2操作有可能被systemd干扰因为systemd对Nginx这类服务有进程监控逻辑它认为master突然重启了。此时建议升级前先把systemd服务停掉然后在后台手动启动systemctl stop nginx /usr/local/nginx/sbin/nginx升级完成后再用systemctl start nginx重新纳管。这样虽然写起来麻烦一点但避免systemd在升级过程中横插一脚把新旧master都干掉的尴尬。我遇到过这个情况信号发出去几秒后systemd检测到master进程退出然后自动拉起一个导致新旧进程混乱。排查了很久才发现是systemd的Restart策略在作祟。5.4 编译依赖缺失导致configure报错新版本Nginx源码对依赖库版本有要求比如OpenSSL 1.1.1以上才能完整支持HTTP/2。如果服务器是老系统自带的openssl版本太低configure会直接报错。处理方式就是先升级openssl-devel或者编译时指定新版openssl路径。但注意openssl是系统级组件升级它可能影响其他服务要谨慎操作。如果只是要给Nginx用可以单独编译openssl并通过--with-openssl路径指定。另外pcre和zlib库缺失configure也会报错。建议编译前置检查一步到位pcre-config --version zlib-devel 是否安装 openssl version少哪个补哪个不要等configure报错了再回头装。5.5 Stream模块和代理功能的连带问题如果你的Nginx用了stream模块做TCP/UDP代理比如转发数据库或Redis流量升级后忘记带上--with-stream参数那么所有的stream配置都会被忽略端口直接停止监听。这个比SSL模块丢失更隐蔽因为HTTP站点功能可能完全正常只有TCP代理静默失效。同理--with-http_realip_module丢了通过代理获取真实IP的配置会失效--with-http_stub_status_module丢了/nginx_status监控接口直接404。升级前把线上Nginx配置文件里用到的所有指令对应的模块梳理一遍做成清单configure时逐一核对。5.6 升级后顺手精简配置和优化参数既然升完了建议趁热打铁检查一下配置里的一些隐藏参数。比如worker_processes和worker_connections很多老配置还停留在worker_processes 1或1024的水平默认worker_connections是1024如果业务并发高很容易到瓶颈。新版Nginx默认的http2支持、ssl_protocols的默认值都有变化正好借升级机会把TLS配置调整到当前推荐值。在nginx最大并发链接数老是用超这个问题上核心就是worker_connections和系统文件描述符限制两个地方。升级时可以顺便确认ulimit -n和worker_rlimit_nofile是否匹配不然新版Nginx照样被文件句柄限制卡脖子。events { worker_connections 4096; } worker_rlimit_nofile 65535; http { server_tokens off; ssl_protocols TLSv1.2 TLSv1.3; }升级之后调这些参数符合动静结合的原则Nginx二进制换了配置也顺手整理一遍双倍收益。根据我个人这几轮升级经验最稳妥的做法是先在测试环境把configure命令、升级步骤、验证清单完整走一遍把过程中所有输出保存下来生产环境操作时按文档一步步执行每发一个信号就确认一次进程状态。宁慢勿快。另外强烈建议把当前版本的编译参数存到一个文件里放到服务器上固定位置下次升级直接拿来用省得每次都要靠nginx -V去反推。