热部署与热升级
帖子数 2
关注者 27

陶辉 发表了文章

巧用 NGINX 实现大规模分布式集群的高可用性

本文是我对2019年GOPS深圳站演讲的文字整理。这里我希望带给各位读者的是,如何站在整个互联网背景下系统化地理解Nginx,因为这样才能解决好大流量分布式网络所面临的高可用问题。标题里有“巧用”二字,何谓巧用?同一个问题会有很多种解决方案,但是,各自的约束性条件却大不相同。巧用就是找出最简单、最适合的方案,而做到这一点的前提就是必须系统化的理解Nginx!本文分四个部分讲清楚如何达到这一目的:首先要搞清楚我们面对的是什么问题。这里会谈下我对大规模分布式集群的理解;Nginx如何帮助集群实现可伸缩性;Nginx如何提高服务的性能;从Nginx的设计思路上学习如何用好它。1. 大规模分布式集群的特点互联网是一个巨大的分布式网络,它有以下特点:多样化的客户端。网络中现存各种不同厂商、不同版本的浏览器,甚至有些用户还在使用非常古老的浏览器,而我们没有办法强制用户升级;多层代理。我们不知道用户发来的请求是不是通过代理翻墙过来的;多级缓存。请求链路上有很多级缓存,浏览器、正反向代理、CDN等都有缓存,怎么控制多级缓存?RFC规范中有明确的定义,但是有些Server并不完全遵守;不可控的流量风暴。不知道用户来自于哪些地区,不知道他们会在哪个时间点集中访问,不知道什么事件会触发流量风暴;网络安全的高要求:信息安全问题要求通信数据必须加密;快速迭代的业务需求:BS架构使软件开发方式发生了巨大变化,我们可以通过快速迭代、发布来快速验证、试错。上图是典型的REST架构,图中包括客户端、正反向代理、源服务器,$符号代表缓存可以服务于上游,也可以服务于下游。通过IP地址标识主机,通过域名系统简化使用,URI则指向具体资源,每种资源有许多种表述,而服务器通过HTTP协议将表述转移至客户端上展示。这便是REST名为表述性状态转移的缘由,我在极客时间《Web协议详解与抓包实战》课程第7、8节课中对此有详细的介绍。设计架构时有许多关注点,与本文主题相关的有4个要点:可伸缩性。核心点在于如何有效的、动态的、灰度的均衡负载。可扩展性指功能组件的独立进化。可以理解为某个Nginx模块独立升级后,并不影响Nginx整体服务的属性。网络效率,也就是如何提升信息传输的效率。HTTP协议功能的全面支持。HTTP1的RFC规范非常多,毕竟它经历了20多年的变迁,而这20多年里互联网的巨大变化是HTTP1的设计者无法预料到的,这些规范也并不被所有Server、Client支持。 当然HTTP2和HTTP3相对情况会好很多。Nginx有优秀的可插拔模块化设计,它基于统一管道架构。其中有一类模块我称它为upstream负载均衡模块,官方Nginx便提供了最小连接、RoundRobin、基于变量控制的hash、一致性hash等负载均衡策略,而大量的第三方模块更提供了许多定制化的负载均衡算法。基于Lua语言的Openresty有自己的生态,这些Lua模块也提供了更灵活的实现方式。Nginx在性能优化上做得非常极致,大家知道最近F5收购了Nginx公司,为什么要收购?因为Nginx的性能可以与基于硬件的、价格昂贵的F5媲美!Nginx对HTTP协议的支持是比较全面的,当我们使用一些小众的替代解决方案时,一定要明确自己在HTTP协议有哪些独特需求。优秀的可配置性,在nginx.conf配置文件里我们可以使用脚本指令与变量实现复杂的功能。2. Nginx与scalability在讨论Nginx的负载均衡策略前,我们先来了解AKF扩展立方体,它能使我们对此建立整体思维。AKF扩展立方体有X、Y、Z轴,这三个轴意味着可以从3个角度实现可伸缩性:X轴指只需要增加应用进程,不用改代码就能水平的扩展。虽然最方便 ,但它解决不了数据不断增长的问题。Y轴按功能切分应用,它能解决数据增长的问题,但是,切分功能意味着重构代码,它引入了复杂性,成本很高。Z轴基于用户的属性扩展服务,运维Nginx时这招我们最常用,通常我们基于变量取到用户的IP地址、URL或者其他参数来执行负载均衡。当然,这三个轴可以任意组合以应对现实中的复杂问题。当然,要想解决可伸缩性问题,还必须在功能上支持足够多的协议。面向下游客户端主要是HTTP协议,当然Nginx也支持OSI传输层的UDP协议和TCP协议。受益于Nginx优秀的模块化设计,对上游服务器Nginx支持非常多的应用层协议,如grpc、uwsgi等。上图是Nginx执行反向代理的流程图,红色是负载均衡模块,任何一个独立的开发者都可以通过开发模块来添加新的LB策略。Nginx必须解决无状态HTTP协议带来的信息冗余及性能低下问题,而Cache缓存是最重要的解决手段,我们需要对Cache在反向代理流程中的作用有所了解。当下游是公网带宽并不稳定,且单用户信道较小时,通常Nginx应缓存请求body,延迟对上游应用服务建立连接的时间;反之,若上游服务的带宽不稳定,则应缓存响应body。理解nginx配置文件的3个关键点是:多级指令配置。通过大括号{},我们可以层层嵌套指令,借用父子关系来模块化的配置代码。变量,这是我们实现复杂功能,且不影响Nginx模块化设计的关键。变量是不同模块间低耦合交互的最有效方式!脚本引擎。脚本指令可以提供应用编程功能。很多人说Nginx的if指令是邪恶的,比如上图中的代码,其实我们只有理解if指令是如何影响父子嵌套关系后,才能正确的使用if。在《Nginx核心知识150讲》第141课我有详细介绍。Nginx官方迭代速度很快,在前两年差不多是两周一个版本,现在是一个月一个版本。频繁的更新解决了Bug也推出了新功能。但我们更新Nginx时却不能像更新其他服务一样,因为Nginx上任一时刻处理的TCP连接都太多了,如果升级Nginx时不能很好的应对就会出现大规模的用户体验问题。Nginx采用多进程结构来解决升级问题。它的master进程是管理进程,为所有worker进程保留住Syn半连接队列,所以升级Nginx时不会导致大规模三次握手失败。相反,单进程的HAProxy升级时就会出现连接建立失败问题。3. Nginx与集群performance缓存有两个实现维度:时间与空间。基于空间的缓存需要基于信息来预测,提前把用户可能请求的字节流准备好。而基于时间的缓存如上图所示,蓝色线条的请求触发了缓存(public share cache),这样红色线条的第二次请求可以直接命中缓存。浏览器中的是私有缓存,私有缓存只为一个用户服务。Nginx上实现了共享缓存,同时Nginx也可以控制浏览器中私有缓存的有效时间。RFC规范定义了许多缓存相关的头部,如果我们忽略了这些规则会很难理解Nginx如何基于下游的请求、上游的响应控制私有缓存及共享缓存,而且不了解这些规则其实不容易读懂nginx.conf中缓存相关指令的说明文档。在《Web协议详解与抓包实战》课程第29到32课我详细的介绍了缓存相关的规则。有些同学会问我,为什么部署Nginx之后没有看到上图中的Cache Loader和Cache Manger进程呢?因为我们没有启用Nginx的缓存。当然,即使我们开启缓存后,Cache Loader进程可能还是看不到的。为什么呢?因为Nginx为了高性能做了很多工作。当重启Nginx时,之前保存在磁盘上的缓存文件需要读入内存建立索引,但读文件的IO速度是很慢的,读缓存文件(文件很大很多)这一步骤可能耗时非常久,对服务器的负载很大,这会影响worker进程服务用户请求的能力。CL进程负责每次只读一小部分内容到共享内存中,这大大缓解了读IO慢的问题。CM进程负责淘汰过期缓存。当下游有一份过期资源时,它会来询问Nginx时:此资源还能用吗?能用的话,通过304告诉我,不要返回响应body(可能很大!)了。当Nginx缓存的资源可能过期时,它也可以问上游的web应用服务器:缓存还能用吗?能用的话通过304告诉我,我来更新缓存Age。RFC7033文档详细定义了这一过程,我在《Web协议详解与抓包实战》第28课有详细介绍。Nginx的not_modified过滤模块便负责执行这一功能。我在《Nginx核心知识150讲》课程第97、98课对此有详细介绍。如果我们突然发布了一个热点资源,许多用户请求瞬间抵达访问该资源,可是该资源可能是一个视频文件尺寸很大,Nginx上还没有建立起它的缓存,如果Nginx放任这些请求直达上游应用服务器(比如可能是Tomcat),非常可能直接把上游服务器打挂了。因为上游应用服务器为了便于功能的快速迭代开发,性能上是不能与Nginx相提并论的。这就需要合并回源请求。怎么合并回源请求呢?第一个请求过来了,放行!第二个请求也到了,但因为第1个请求还没有完成,所以上图中的请求2、4、5都不放行,直到第6步第1个请求的响应返回后,再把缓存的内容作为响应在第8、9、10中返回。这样就能缓解上游服务的压力。减少回源请求是一个解决方案,但如果Nginx上有过期的响应,能不能先将就着发给用户?当然,同时也会通过条件请求去上游应用那里获取最新的缓存。我们经常提到的互联网柔性、分级服务的原理与此是相同的。既然最新内容暂时由于带宽、性能等因素不能提供,不如先提供过期的内容,当然前提是不对业务产生严重影响。Nginx中的proxy_cache_use_stale指令允许使用stale过期缓存,上图中第1个请求放行了,第2、3请求使用旧缓存。从这里可以看出Nginx应对大流量有许多成熟的方案。我们在网页上会使用播放条拖动着看视频,这可以基于Http Range协议实现。但是,如果不启用Slice模块Nginx就会出现性能问题,比如现在浏览器要访问一个视频文件的第150-249字节,由于满足了缓存条件,Nginx试图先把文件拉取过来缓存,再返回响应。然而,Nginx会拉取完整的文件缓存!这是很慢的。怎么解决这个问题呢?使用Nginx的slice模块即可,如果配置100字节作为基础块大小,Nginx会基于100-199、200-299产生2个请求,这2个请求的应用返回并存入缓存后再构造出150-249字节的响应返回给用户。这样效率就高很多!通常,Nginx作为CDN使用时都会打开这一功能。互联网解决信息安全的方案是TLS/SSL协议,Nginx对其有很好的支持。比如,Nginx把下游公网发来的TLS流量卸载掉TLS层,再转发给上游;同时,它也可以把下游传输来的HTTP流量 ,根据配置的证书转换为HTTPS流量。在验证证书时,在nginx.conf中我们可以通过变量实现证书或者域名验证。虽然TLS工作在OSI模型的表示层,但Nginx作为四层负载均衡时仍然可以执行同样的增、删TLS层功能。Nginx的Stream模块也允许在nginx.conf中通过变量验证证书。Nginx处理TLS层性能非常好,这得益于2点:Nginx本身的代码很高效,这既因为它基于C语言,也由于它具备优秀的设计。减少TLS握手次数,包括:session缓存。减少TLS1.2握手中1次RTT的时间,当然它对集群的支持并不好,而且比较消耗内存。Ticket票据。Ticket票据可应用于集群,且并不占用内存。当然,减少TLS握手的这2个策略都面临着重放攻击的危险,更好的方式是升级到TLS1.3。我在《Web协议详解与抓包实战》第80课有详细介绍。4. 巧用NginxNginx模块众多,我个人把它分为四类,这四类模块各自有其不同的设计原则。请求处理模块。负责生成响应或者影响后续的处理模块,请求处理模块遵循请求阶段设计,在同阶段内按序处理。过滤模块。生成了HTTP响应后,此类模块可以对响应做再加工。仅影响变量的模块。这类模块为其他模块的指令赋能,它们提供新的变量或者修改已有的变量。负载均衡模块。它们提供选择上游服务器的负载均衡算法,并可以管理上游连接。请求处理模块、过滤模块、负载均衡模块均遵循unitform pipe and filter架构,每个模块以统一的接口处理输入,并以同样的接口产生输出,这些模块串联在一起提供复杂的功能。Nginx把请求处理流程分为11个阶段,所有请求处理模块必须隶属于某个阶段,或者同时在多个阶段中工作。每个处理阶段必须依次向后执行,不可跳跃阶段执行。同阶段内允许存在多个模块同时生效,这些模块串联在一起有序执行。当然,先执行的模块还有个特权,它可以决定忽略本阶段后续模块的执行,直接跳跃到下一个阶段中的第1个模块执行。每个阶段的功能单一,每个模块的功能也很简单,因此该设计扩展性很好。上图中的灰色模块Nginx框架中的请求处理模块。上图中右边是Openresty默认编译进Nginx的过滤模块,它们是按序执行的。图中用红色框出的是关键模块,它们是必须存在的,而且它们也将其他模块分为三组,开发第三方过滤模块时必须先决定自己应在哪一组,再决定自己应在组内的什么位置。Nginx中的变量分为:提供变量的模块和使用变量的模块。其含义我在《Nginx核心知识150讲》第72课有介绍,关于框架提供的变量在第73、74课中有介绍。无论我们使用了哪些模块,Nginx框架中的变量一定是默认提供的,它为我们提供了基础功能,理解好它们是我们使用好Nginx变量的关键。框架变量分为5类:HTTP 请求相关的变量TCP 连接相关的变量Nginx 处理请求过程中产生的变量发送 HTTP 响应时相关的变量Nginx 系统变量最后我们来谈谈Openresty,它其实是Nginx中的一系列模块构成的,但它由于集成了Lua引擎,又延伸出Lua模块并构成了新的生态。看看Openresty由哪些部分组成:Nginx,这里指的是Nginx的框架代码。Nginx官方模块,以及各类第三方(非Openresty系列)C模块。Openresty生态模块,它包括直接在Nginx中执行的C模块,例如上图中的绿色模块,也包括必须运行在ngx_http_lua_module模块之上的Lua语言模块。当然,Openresty也提供了一些方便使用的脚本工具。Openresty中的Lua代码并不用考虑异步,它是怎么在Nginx的异步C代码框架中执行的呢?我们知道,Nginx框架由事件驱动系统、HTTP框架和STREAM框架组成。而Openresty中的ngx_http_lua_module和ngx_stream_lua_module模块给Lua语言提供了编程接口,Lua语言通过它们编译为C代码在Nginx中执行。我们在nginx.conf文件中嵌入Lua代码,而Lua代码也可以调用上述两个模块提供的SDK调动Nginx的功能。Openresty的SDK功能强大,我个人把它分为以下8大类:Cosocket提供了类似协程的网络通讯功能,它的性能优化很到位,许多其他Lua模块都是基于它实现的。基于共享内存的字典,它支持多进程使用,所有worker进程间同步通常通过Shared.DICT。定时器。基于协程的并发编程。获取客户端请求与响应的信息修改客户端请求与响应,包括发送响应子请求,官方Nginx就提供了树状的子请求,用于实现复杂功能,Openresty用比C简单的多的Lua语言又实现了一遍。工具类,大致包含以下5类:正则表达式日志系统配置编解码时间类最后做个总结。在恰当的时间做恰当的事,听起来很美好,但需要我们有大局观。我们要清楚大规模分布式网络通常存在哪些问题,也要清楚分布式网络的常用解决方案,然后才能谈如何用Nginx解决上述问题。而用好Nginx,必须系统的掌握Nginx的架构与设计原理,理解模块化设计、阶段式设计,清楚Nginx的核心流程,这样我们才能恰到好处地用Nginx解决掉问题。

点赞 10
408 次浏览
3 条评论

陶辉 发表了文章

如何在高并发环境中灰度升级Nginx?

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进程仍然存在。 由于老master进程是由老版本的nginx二进制文件启动,这样回滚很容易,只要将它的worker进程重新拉起,即可向用户提供旧版本服务,同时要求新版本的Nginx进行优雅退出即可。 这就是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信号。 在升级过程中新版本的nginx启动后,nginx.pid中只会存放新master进程的id,而老master进程的id则会改放在nginx.pid.oldbin文件中。 当老版本的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配置文件时也使用了优雅退出这一功能,如下图所示:小结 本文介绍了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热升级方案相似吗?具体是怎样实现的?欢迎你在帖子下方留言,与我一起探讨更好的热部署实现方案。 

点赞 8
406 次浏览
3 条评论