点赞
评论
收藏
分享
举报
如何在高并发环境中灰度升级Nginx?
发表于2020-05-19 14:17

浏览 5.8k

2019年Nginx发布了6个stable版本以及12个mainline版本,这些发布要么修改了重要的漏洞,要么新增了很有用的特性。如果你不能及时升级Nginx,那么既无法享受到技术进步带来的降本增效,还会让服务暴露在安全风险之下。

十多年前,我们大可以升级前在官网上发个公告,声明某个凌晨不提供服务,那时可以从容地停止进程、更换程序、重启服务。然而,当下的用户却很难容忍停机升级这种体验,尤其对于接入层充当负载均衡的Nginx来说,它的并发连接数以百万计,哪怕只终止Nginx进程1秒钟,也会导致大量用户出现业务中断。

怎样保证升级高负载的Nginx时,不影响到海量的在线用户呢?而且,虽然官方Nginx是稳定的,但毕竟Nginx在编译期可以定制加入各种C模块,如果某些模块在升级后出现异常,就需要将Nginx回滚到旧版本,此时又怎样保证降级时也不会影响到正常服务的在线用户?

实际上,Nginx的热升级功能可以解决上述问题,它允许新老版本灰度地平滑过渡,这受益于Nginx的多进程架构。本文将介绍该如何升级、回滚Nginx,以及Nginx的进程架构是怎样保障不对用户产生影响的。理解热升级后,你也能更透彻的掌握热加载功能(reload使新配置文件生效),因为热加载相当于简化版的热升级。

 

怎样才能平滑升级程序?

最简单的升级方式,是关闭现有的旧进程后,再基于新程序启动进程。许多可用性要求不高的场景,就是这么做的。然而,在多数服务SLA(Service-Level Agreement)高达4个9以上的今天(99.99%意味着服务一年内的总宕机时间不得超过0.876小时),这种简单粗暴的方式不可取,它对于服务质量影响太大。当旧进程关闭时,操作系统会对进程打开的所有TCP连接发送RST复位报文,强行关闭TCP连接,接着,所有浏览器都会收到ERR_CONNECTION_RESET错误。

 

为了不影响现有TCP连接,能不能在命令行中先启动新程序,由升级后的新程序服务后建立的TCP连接,而原TCP连接在全部自然终止后,再关闭老进程呢?这其实做不到。

这是因为服务器程序不同于客户端,通常它需要监听80等指定端口,这样客户端才能针对明确的80端口建立TCP连接,而OSI传输层(由Linux内核实现)保证报文可以到达Nginx进程。因此,两个完全不同的进程是不能打开同一个端口的,如果我们在旧进程关闭前,启动新程序,往往会遇到bind failed( Address already in use)错误,导致进程无法启动。

事实上,上述通过新老进程并存的升级方案,就是平滑升级的最佳解决方案。但是怎样绕过同一端口不能被两个进程同时打开的限制呢?其实通过父子进程(参见wiki)就可以做到,而Nginx的平滑升级也正是这么做到的。

操作系统规定,每一个进程都必须由另一个进程启动,这两个进程就称为父子进程,其中,子进程自动继承父进程已经申请到的资源,比如监听的80端口。在Linux中,子进程是由fork函数创建的,最初它只是父进程的副本。比如在生产环境中启动Nginx时(即master_process on;),nginx会在绑定80端口后再用fork函数生成worker子进程(注意,nginx会自动将父进程名字改为nginx: master process),这样,worker进程也可以通过80端口与客户端建立TCP连接。当然,多个worker进程同时监听80端口时,系统内核会有一套算法决定某个TCP连接由哪个worker进程处理(可以参考Linux 3.9内核版本后提供的SO_REUSEPORT选项),均衡多个worker子进程间的负载,如下图所示:

那么,既然master与worker可以绑定同一端口,那么升级新版本nginx时,也由现在的老master进程启动(子进程默认是父进程的副本,但通过exec函数可以载入新版本的nginx程序,下文会详细介绍),这样,新master进程就是老master进程的子进程,可以共享老版本nginx已经打开的、包括端口在内的各类资源。至此,两个版本的nginx皆在运行中,只要老版本的nginx停止建立新连接,内核自然只会将新的TCP连接交给新版本的nginx处理,等到老版本nginx处理完现存的客户请求后可令其退出,这就完成了平滑升级。

那么,到底怎样通知nginx升级呢?下面我们来看详细的操作步骤。

 

Nginx的平滑升级步骤是什么?

为了通知运行中的Nginx进程执行升级,我们必须使用一种进程间通讯的方案。在Linux中,通知进程的最简便方法是信号,Nginx便选择了这一方案。由于热升级涉及到复杂的回滚操作,必须对新老master进程独立的发送信号,因此Nginx决定由管理员通过命令行中的kill命令发送信号,完成热升级或者回滚。

我们先来看热升级的步骤。升级前,建议你先将老的binary二进制文件后(即/usr/local/nginx/sbin/nginx文件)备份到另一个位置,为后续可能的回滚做准备。接着,你需要把新版本的nginx二进制文件覆盖老文件,这样,运行中的master进程生成子进程后才能载入新版本的nginx。注意,虽然你覆盖了老nginx,但并不会影响运行中的老nginx进程。

接着,你可以用ps命令找到master进程的pid,并通过kill命令向它发送USR2信号,这样master进程就会生成新的子进程,同时用exec函数载入新版本的nginx二进制文件,并将进程改名为nginx: master process。当然,新的master也会依据nginx.conf中的内容,再次启动新worker子进程提供服务,这些父子进程的关系如下图所示:

此时,老版本的nginx已经停止监听80端口,你可以通过netstat命令看到,现在只有新版本的nginx进程会监听80端口了,今后新建立的TCP连接都会由新版本进程处理:

那么,如何让老版本的nginx进程在处理完现存TCP连接后退出呢?很简单,使用nginx的优雅退出功能即可,具体通过kill向老master进程发送WINCH或者QUIT信号即可:

当老版本的master、worker进程都退出后,根据Linux内核的规则,pid为1的系统守护进程将成为新master的父进程(目前的守护进程为systemd,其演进流程参见酷壳上的这篇文章)。

 

因此,平滑升级Nginx通常会经历3个阶段:

1、 仅老nginx进程在运行,此时先备份nginx binary文件,再把新版本的nginx覆盖原位置,最后通过kill发送USR2信号。

2、 新老nginx进程同时并存,此时需要通过信号命令老master进程优雅退出。

3、 当处理完所有请求后,老的nginx进程退出,此时平滑升级完毕。

在新老nginx并存时,如果向老master进程发送了QUIT信号,那么在它的worker子进程退出后,老master进程也会自行退出。这时如果需要从新版本回滚到老版本,就得重新执行一次“升级”。还有一种更简单的回滚方法,向老master进程发送WINCH信号,这样老worker进程全部退出后,老master进程仍然存在

图片.png

 

由于老master进程是由老版本的nginx二进制文件启动,这样回滚很容易,只要将它的worker进程重新拉起,即可向用户提供旧版本服务,同时要求新版本的Nginx进行优雅退出即可。

图片.png 

这就是Nginx平滑升级和回滚的全过程,这是我们在大流量生产环境中必须掌握的步骤。


Nginx是怎样实现 “平滑”升级的?

最后,我们结合Nginx的进程架构,从实现层面分析Nginx到底是如何执行平滑升级的,这样就可以快速定位热升级时可能遇到的问题。

平滑升级涉及两个关键的子功能,一是在收到USR2信号后,启动新版本Nginx;二是将不再监听端口的nginx进程优雅退出。先来看USR2信号的处理。

在Linux中,使用fork函数就可以生成子进程副本,再用execve函数载入新版本的nginx二进制文件运行,就进入新老版本nginx并存的阶段。此时,写入master进程pid的nginx.pid文件内容会发生变化(了解了这一点就清楚找不到nginx.pid文件后,nginx的命令行为何不再生效)。

由于nginx支持通过命令行发送信号,比如上文介绍过的热加载,其实与向master进程发送HUP信号是完全一致的。但日常我们更习惯通过更方便的nginx -s reload命令行来完成,reload命令在读取nginx.pid文件中的进程id后,就会向master进程发送HUP信号。

图片.png

 

在升级过程中新版本的nginx启动后,nginx.pid中只会存放新master进程的id,而老master进程的id则会改放在nginx.pid.oldbin文件中。

图片.png

 

当老版本的master进程优雅退出后,nginx.pid.oldbin文件会被自动删除。这些细节可以协助分析热升级时遇到的问题。

再来看nginx是如何优雅退出的,即worker进程怎样判定所有TCP连接都处理完了。当master进程收到QUIT或者WINCH信号后,会向所有worker子进程发送QUIT信号。而worker进程收到QUIT信号后,会做以下4件事:

1、 设置worker_shutdown_timeout定时器,因为有些应用协议nginx并不解析,也就无从判断何时会结束。比如,使用stream模块做四层负载均衡,或者用作七层的websocket反向代理时,nginx都无法判断何时该关闭连接。因此,旧版本的nginx进程会长时间存在。设置定时器后,worker进程会在worker_shutdown_timeout秒后强行退出。当然,通常情况下不需要配置worker_shutdown_timeout,因为老worker进程长时间存在并不会影响新nginx的业务

2、 关闭监听着的所有端口;

3、 关闭所有空闲的TCP连接;

4、 设置ngx_exiting标志位为1(协助业务模块关闭连接),等待业务模块关闭所有的TCP连接后,自行退出进程。比如对于HTTP短连接请求而言(即HTTP头部中存在Connection: closed),当nginx发送完响应后就可以主动关闭TCP连接。如果是HTTP长连接(即存在Connection: keep-alive头部),正常情况下应当由客户端关闭连接,或者连接上处理过的请求个数超过了keepalive_request_count才能由nginx关闭连接,但在优雅退出这个场景中,nginx可以在处理完当前http请求后立刻关闭连接,如下代码所示:

    if (!ngx_terminate
         && !ngx_exiting //在优雅退出时,ngx_exiting会置为1
         && r->keepalive
         && clcf->keepalive_timeout > 0)
    {
        ngx_http_set_keepalive(r); //作为HTTP长连接继续复用
        return;
    }


worker进程正是按照这样的优雅退出流程自行关闭的。热重载新的nginx.conf配置文件时也使用了优雅退出这一功能,如下图所示:

图片.png

小结

 

本文介绍了Nginx热升级的原理、运维操作步骤及架构实现。

平滑升级的前提是同时启动新老2个版本的Nginx进程,其中老进程服务于正在传输数据的TCP连接,而新进程处理之后建立的TCP连接。由于新老进程需要同时打开80等监听端口,这就需要利用父子进程可以共享资源这一特性,因此,新版本的Nginx必须由老的master进程启动。

Nginx提供的热升级功能,需要使用Linux命令行的kill命令发送信号。其中,USR2信号用于命令老master进程启动新版本的nginx;WINCH信号用于令老master进程优雅的终止worker子进程;HUP信号用于回滚时启动老worker进程;QUIT信号用于令老master及worker进程优雅地退出。

Nginx为了提供-s reload等命令行,需要将master进程的pid保存到nginx.pid文件中。需要注意的是,在热升级中nginx.pid文件的内容会发生变化。

优雅退出是平滑升级的关键,它需要业务模块的支持。比如http模块通常可以完美的实现优雅退出,而其他一些不解析协议内容的模块就很难做到,此时,nginx提供了优雅退出定时器,限制worker进程在worker_shutdown_timeout秒内必须关闭。这些措施都进一步增强了热升级的适用性。

最后能不能请你谈谈,你还使用过哪些其他支持热升级的软件?它们的实现方式与本文介绍的Nginx热升级方案相似吗?具体是怎样实现的?欢迎你在帖子下方留言,与我一起探讨更好的热部署实现方案。

 


已修改于2023-03-07 19:58
本作品系原创
创作不易,留下一份鼓励
陶辉

暂无个人介绍

关注



写下您的评论
发表评论
全部评论(0)

按点赞数排序

按时间排序

关于作者
陶辉
这家伙很懒还未留下介绍~
15
文章
0
问答
202
粉丝
相关文章
本文是我对2019年GOPS深圳站演讲的文字整理。这里我希望带给各位读者的是,如何站在整个互联网背景下系统化地理解Nginx,因为这样才能解决好大流量分布式网络所面临的高可用问题。标题里有“巧用”二字,何谓巧用?同一个问题会有很多种解决方案,但是,各自的约束性条件却大不相同。巧用就是找出最简单、最适合的方案,而做到这一点的前提就是必须系统化的理解Nginx!本文分四个部分讲清楚如何达到这一目的:首先要搞清楚我们面对的是什么问题。这里会谈下我对大规模分布式集群的理解;Nginx如何帮助集群实现可伸缩性;Nginx如何提高服务的性能;从Nginx的设计思路上学习如何用好它。1.大规模分布式集群的特点互联网是一个巨大的分布式网络,它有以下特点:多样化的客户端。网络中现存各种不同厂商、不同版本的浏览器,甚至有些用户还在使用非常古老的浏览器,而我们没有办法强制用户升级;多层代理。我们不知道用户发来的请求是不是通过代理翻墙过来的;多级缓存。请求链路上有很多级缓存,浏览器、正反向代理、CDN等都有缓存,怎么控制多级缓存?RFC规范中有明确的定义,但是有些Server并不完全遵守;不可控的流量风暴。
点赞 10
浏览 3.2k
上一篇文章中,我介绍了Nginx的特性,如何获取Nginx源代码,以及源代码中各目录的含义。本文将介绍如何定制化编译、安装、运行Nginx。当你用yum或者apt-get命令安装、启动Nginx后,通过nginx-t命令你会发现,nginx.conf配置文件可能在/etc/目录中。而运行基于源码安装的Nginx时,nginx.conf文件又可能位于/usr/local/nginx/conf/目录,运行OpenResty时, nginx.conf又被放在了/usr/local/openresty/nginx/conf/目录。这些奇怪的现象都源于编译Nginx前,configure脚本设置的--prefix或者--conf-path选项。Nginx的所有功能都来自于官方及第三方模块,如果你不知道如何使用configure添加需要的模块,相当于放弃了Nginx诞生16年来累积出的丰富生态。而且,很多高性能特性默认是关闭的,如果你习惯于使用应用市场中编译好的二进制文件,也无法获得性能最优化的Nginx。本文将会介绍定制Nginx过程中,configure脚本的用法。其中对于定制模块的选项,
点赞 8
浏览 5k
【版本更新】NGINX 主线版更新到 1.25.3 该版本改进了使用 HTTP/2 时对行为不端客户端的检测,提升了使用大量 location 配置时的启动速度。访问 NGINX 中文官方开源社区(nginx.org.cn)了解详情。
点赞 1
浏览 1.5k