
陶辉nginx
NGINX顶级专家web
现任职于杭州智链达数据有限公司CTO。著有《深刻理解Nginx》一书,在极客时间上开设有一门包含150多节课程的视频课《Nginx核心知识100讲》,并在阿里、腾讯、思科等大厂工做中对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链接在所有天然终止后,再关闭老进程呢?这其实作不到。websocket
这是由于服务器程序不一样于客户端,一般它须要监听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升级呢?下面咱们来看详细的操做步骤。
接着,你能够用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个阶段:
-
仅老nginx进程在运行,此时先备份nginx binary文件,再把新版本的nginx覆盖原位置,最后经过kill发送USR2信号。 -
新老nginx进程同时并存,此时须要经过信号命令老master进程优雅退出。 当处理完全部请求后,老的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件事:
设置worker_shutdown_timeout定时器,由于有些应用协议nginx并不解析,也就无从判断什么时候会结束。好比,使用stream模块作四层负载均衡,或者用做七层的websocket反向代理时,nginx都没法判断什么时候该关闭链接。所以,旧版本的nginx进程会长时间存在。设置定时器后,worker进程会在worker_shutdown_timeout秒后强行退出。固然,一般状况下不须要配置worker_shutdown_timeout,由于老worker进程长时间存在并不会影响新nginx的业务。
关闭监听着的全部端口;
关闭全部空闲的TCP链接;
设置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热升级方案类似吗?具体是怎样实现的?欢迎你在帖子下方留言,与我一块儿探讨更好的热部署实现方案。

活动推荐

本文分享自微信公众号 - NGINX开源社区(gh_0d2551f1bdb6)。
若有侵权,请联系 support@oschina.cn 删除。
本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一块儿分享。