如何在高并发环境中灰度升级Nginx?
点赞 8
3 条评论
406 次浏览
发表于 : 2020-05-19 14:17

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子进程间的负载,如下图所示:

图片.png 

那么,既然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子进程提供服务,这些父子进程的关系如下图所示:

图片.png 

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

图片.png 

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

图片.png   

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

 

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

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

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

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

 图片.png

在新老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热升级方案相似吗?具体是怎样实现的?欢迎你在帖子下方留言,与我一起探讨更好的热部署实现方案。

 

如果您觉得不错,就打赏支持一下吧~

打赏
3 条评论
jiangyidigital 2020-05-19 18:19
感谢陶大师分享,学习中
点赞 0
回复
陶辉 回复 jiangyidigital 2020-05-29 09:58
^_^
点赞 0
回复
Radio 2020-06-15 12:11
谢谢
点赞 0
回复

要回复文章请先登录注册

关于作者
回答 64
文章 12
粉丝 52
相关推荐
本文是我对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解决掉问题。
PDF课件是放在github上的,地址是https://github.com/russelltao/geektime-nginx  因为github对国内网络不太稳定,有同学在微信群里请我发下课件,干脆我把6个PDF课件放到这篇文章的附件里,需要的同学请下载取用。   Nginx核心知识100讲-第一部分课件.pdf  Nginx核心知识100讲-第二部分课件.pdf  Nginx核心知识100讲-第三部分课件.pdf  Nginx核心知识100讲-第四部分课件.pdf  Nginx核心知识100讲-第五部分课件.pdf  Nginx核心知识100讲-第六部分课件.pdf 
Nginx是当下最流行的Web服务器,通过官方以及第三方C模块,以及在Nginx上构建出的Openresty,或者在Openresty上构建出的Kong,你可以使用Nginx生态满足任何复杂Web场景下的需求。Nginx的性能也极其优秀,它可以轻松支持百万、千万级的并发连接,也可以高效的处理磁盘IO,因而通过静态资源或者缓存,能够为Tomcat、Django等性能不佳的Web应用扛住绝大部分外部流量。 但是,很多刚接触Nginx的同学,对它的理解往往失之偏颇,不太清楚Nginx的能力范围。比如:你可能清楚Nginx对上游应用支持Google的gRPC协议,但对下游的客户端是否支持gRPC协议呢?Openresty中的Nginx版本是单号的,而Nginx官网中的stable稳定版本则是双号的,我们到底该选择哪个版本的Nginx呢?安装Nginx时,下载Nginx docker镜像,或者用yum/apt-get安装,都比下载源代码再编译出可执行文件要简单许多,那到底有必要基于源码安装Nginx吗?当你下载完Nginx源码后,你清楚每个目录与文件的意义吗? 本文是《从头搭建1个静态资源服务器》系列文章中的第1篇,也是我在6月4日晚直播内容的文字总结,在这篇文章中我将向你演示:Nginx有什么特点,它的能力上限在哪,该如何获取Nginx,Nginx源代码中各目录的意义又是什么。 Nginx到底是什么?  Nginx是一个集静态资源、负载均衡于一身的Web服务器,这里有3个关键词,我们一一来分析。 l    Web我爱把互联网服务的访问路径,与社会经济中的供应链放在一起做类比,这样很容易理解“上下游”这类比较抽象的词汇。比如,购买小米手机时,实体或者网上店铺是供应链的下游,而高通的CPU则是上游。类似地,浏览器作为终端自然是下游,关系数据库则是上游,而Nginx位于源服务器和终端之间,如下图所示:                                                弄明白了上下游的概念后,我们就清楚了“Web服务器”的外延:Nginx的下游协议是Web中的HTTP协议,而上游则可以是任意协议,比如python的网关协议uwsgi,或者C/C++爱用的CGI协议,或者RPC服务常用的gRPC协议,等等。 在Nginx诞生之初,它的下游协议仅支持HTTP/1协议,但随着版本的不断迭代,现在下游还支持HTTP/2、MAIL邮件、TCP协议、UDP协议等等。 Web场景面向的是公网,所以非常强调信息安全。而Nginx对TLS/SSL协议的支持也非常彻底,它可以轻松的对下游或者上游装载、卸载TLS协议,并通过Openssl支持各种安全套件。 l    静态资源 Web服务器必须能够提供图片、Javascript、CSS、HTML等资源的下载能力,由于它们多数是静态的,所以通常直接存放在磁盘上。Nginx很擅长读取本机磁盘上的文件,并将它们发送至下游客户端!你可能会觉得,读取文件并通过HTTP协议发送出去,这简直不要太简单,Nginx竟然只是擅长这个?这里可大有文章! 比如,你可能了解零拷贝技术,Nginx很早就支持它,这使得发送文件时速度可以至少提升一倍!可是,零拷贝对于特大文件很不友好,占用了许多PageCache内存,但使用率却非常低,因此Nginx用Linux的原生异步IO加上直接IO解决了这类问题。再比如,小报文的发送降低了网络传输效率,而Nginx通过Nagle、Cork等算法,以及应用层的postpone_out指令批量发送小报文,这使得Nginx的性能远远领先于Tomcat、Netty、Apache等竞争对手,因此主流的CDN都是使用Nginx实现的。 l    负载均衡 在分布式系统中,用加机器扩展系统,是提升可用性的最有效方法。但扩展系统时,需要在应用服务前添加1个负载均衡服务,使它能够将请求流量分发给上游的应用。这一场景中,除了对负载均衡服务的性能有极高的要求外,它还必须能够处理应用层协议。在OSI网络体系中,IP网络层是第3层,TCP/UDP传输层是第4层,而HTTP等应用层则是第7层,因此,在Web场景中,需求量最大的自然是7层负载均衡,而Nginx非常擅长应用层的协议处理,这体现在以下4个方面: 1.      通过多路复用、事件驱动等技术,Nginx可以轻松支持C10M级别的并发;2.      由C语言编写,与操作系统紧密结合的Nginx(紧密结合到什么程度呢?Nginx之父Igor曾经说过,他最后悔的就是让Nginx支持windows操作系统,因为它与类Unix系统差异太大,这使得紧密结合的Nginx必须付出很大代价才能实现),能够充分使用CPU、内存等硬件,极高的效率使它可以同时为几十台上游服务器提供负载均衡功能;3.      Nginx的架构很灵活,它允许任何第三方以C模块的形式,与官方模块互相协作,给用户提供各类功能。因此,丰富的生态使得Nginx支持多种多样的应用层协议(你可以在Github上搜索到大量的C模块),你也可以直接开发C模块定制Nginx。4.      Nginx使用了非常开放的2-clause BSD-like license源码许可协议,它意味着你在修改Nginx源码后,还可以作为商业用途发布,TEngine就受益于这一特性。当Lua语言通过C模块注入Nginx后,就诞生了Openresty及一堆Lua语言模块,这比直接开发C语言模块难度下降了很多。而在Lua语言之上,又诞生了Kong这样面向微服务的生态。 从上述3个关键词的解释,我相信你已经明白了Nginx的能力范围。接下来,我们再来看看如何安装Nginx。 怎样获取Nginx? Nginx有很多种获取、安装的方式,我把它们分为以下两类: l    非定制化安装 主要指下载编译好的二进制文件,再直接安装在目标系统中,比如:u    拉取含有Nginx的docker镜像;u    在操作系统的应用市场中直接安装,比如用apt-get/yum命令直接安装Nginx;u    获取到网上编译好的Nginx压缩包后,解压后直接运行; l    定制化安装 在http://nginx.org/en/download.html上或者https://www.nginx-cn.net/product上下载Nginx源代码,调用configure脚本生成定制化的编译选项后,执行make命令编译生成可执行文件,最后用make install命令安装Nginx。 非定制化安装虽然更加简单,但这样的Nginx默认缺失以下功能: u    不支持更有效率的HTTP2协议;u    不支持TCP/UDP协议,不能充当4层负载均衡;u    不支持TLS/SSL协议,无法跨越公网保障网络安全;u    未安装stub_status模块,无法实时监控Nginx连接状态;你可以通过configure –help命令给出的--with-XXX-module说明,找到Nginx默认不安装的官方模块,例如:(dynamic是动态模块,在后续文章中我会演示其用法)--with-http_ssl_module enable ngx_http_ssl_module   --with-http_v2_module enable ngx_http_v2_module   --with-http_realip_module enable ngx_http_realip_module   --with-http_addition_module enable ngx_http_addition_module   --with-http_xslt_module enable ngx_http_xslt_module   --with-http_xslt_module=dynamic enable dynamic ngx_http_xslt_module   --with-http_image_filter_module enable ngx_http_image_filter_module   --with-http_image_filter_module=dynamic enable dynamic ngx_http_image_filter_module   --with-http_geoip_module enable ngx_http_geoip_module   --with-http_geoip_module=dynamic enable dynamic ngx_http_geoip_module   --with-http_sub_module enable ngx_http_sub_module   --with-http_dav_module enable ngx_http_dav_module   --with-http_flv_module enable ngx_http_flv_module   --with-http_mp4_module enable ngx_http_mp4_module   --with-http_gunzip_module enable ngx_http_gunzip_module   --with-http_gzip_static_module enable ngx_http_gzip_static_module   --with-http_auth_request_module enable ngx_http_auth_request_module   --with-http_random_index_module enable ngx_http_random_index_module   --with-http_secure_link_module enable ngx_http_secure_link_module   --with-http_degradation_module enable ngx_http_degradation_module   --with-http_slice_module enable ngx_http_slice_module   --with-http_stub_status_module enable ngx_http_stub_status_module 因此,从功能的全面性上来说,我们需要从源码上安装Nginx。 你可能会想,那为什么不索性将所有模块都编译到默认的Nginx中呢?按需编译模块,至少有以下4个优点: u    执行速度更快。例如,通过配置文件关闭功能,就需要多做一些条件判断。u    减少nginx可执行文件的大小。u    有些模块依赖项过多,在非必要时启用它们,会增加编译、运行环境的复杂性。u    给用户提供强大的自定义功能,比如在configure时设定配置文件、pid文件、可执行文件的路径,根据实际情况重新指定编译时的优化参数等等。 当然,最重要的还是可以通过configure --add-module选项任意添加自定义模块,这赋予Nginx无限的可能。 由于Nginx有许多分支和版本,该如何选择适合自己的版本呢?这有两个技巧,我们先来看mainline和stable版本的区别,在http://nginx.org/en/download.html上你会看到如下页面: 这里,mainline是含有最新功能的主线版本,它的迭代速度最快。另外,你可能注意到mainline是单号版本,而Openresty由于更新Nginx的频率较低,所以为了获得最新的Nginx特性,它通常使用mainline版本。 stable是mainline版本稳定运行一段时间后,将单号大版本转换为双号的稳定版本,比如1.18.0就是由1.17.10转换而来。 Legacy则是曾经的稳定版本。如果从头开始使用Nginx,那么你只需要选择最新的stable或者mainline版本就可以了。但如果你已经在使用某一个Legacy版本的Nginx,现在是否把它升级到最新版本呢?毕竟在生产环境上升级前要做完整的功能、性能测试,成本并不低。此时,我们要从CHANGES变更文件中,寻找每个版本的变化点。点开CHANGES文件,你会看到如下页面: 这里列出了每个版本的发布时间,以及发布时的变更。这些变更共分为以下4类:u    Feature新功能,比如上图HTTP框架新增的auth_delay指令。u    Bugfix问题修复,我们尤其要关注一些重大Bug的修复。u    Change已知特性的变更,比如之前允许HTTP请求头部中出现多个Host头部,但在1.17.9这个Change后,就认定这类HTTP请求非法了。u    Security安全问题的升级,比如1.15.6版本就修复了CVE-2018-16843等3个安全问题。 从Feature、Bugfix、Change、Security这4个方面,我们就可以更有针对性的升级Nginx。 认识Nginx的源码目录 当获取到Nginx源码压缩包并解压后,你可能对这些目录一头雾水,这里我对它们做个简单说明。比如1.18.0版本的源代码目录是这样的: 其中包含5个文件和5个目录,我们先来看单个文件的意义: u    CHANGES:即上面介绍过的版本变更文件。u    CHANGES.ru:由于Igor是俄罗斯人,所以除了上面的英文版变更文件外,还有个俄文版的变更文件。u    configure:如同其他Linux源码类软件一样,这是编译前的必须执行的核心脚本,它包含下面4个子功能:Ø  解析configure脚本执行时传入的各种参数,包括定制的第三方模块;Ø  针对操作系统、体系架构、编译器的特性,生成特定的编译参数;Ø  生成Makefile、ngx_modules.c等文件;Ø  在屏幕上显示汇总后的执行结果。u    LICENSE:这个文件描述了Nginx使用的2-clause BSD-like license许可协议。u    README:它只是告诉你去使用http://nginx.org官网查询各模块的用法。 再来看各个目录的意义: u    auto:configure只是一个简单的入口脚本,真正的功能是由auto目录下各个脚本完成的。u    conf:当安装完Nginx后,conf目录下会有默认的配置文件,这些文件就是从这里的conf目录复制过去的。u    contrib:包含了Nginx相关的周边小工具,比如下一讲将要介绍vim中如何高亮显示Nginx语法,就依赖于其中的vim子目录。u    html:安装完Nginx并运行后,会显示默认的欢迎页面,以及出现错误的500页面,这两个页面就是由html目录拷贝到安装目录的。u    man:目录中仅包含nginx.8一个文件,它其实是为Linux系统准备的man帮助文档,使用man -l nginx.8命令,可以看到Nginx命令行的使用方法: u    src:放置所有Nginx源代码的目录。关于src下的子目录,后续我分析源码时再详细介绍它们。 以上就是官方压缩包解压后的内容,当你执行完configure脚本后,还会多出Makefile文件以及objs目录,在下一篇文章中我会介绍它们。 小结最后,对《从头搭建静态资源服务器》系列第1篇做个总结。 Nginx是集静态资源与负载均衡与一身的Web服务器,它支持C10M级别的并发连接,也通过与操作系统的紧密结合,能够高效的使用系统资源。除性能外,Nginx通过优秀的模块设计,允许第三方的C模块、Lua模块等嵌入到Nginx中运行,这极大丰富了Nginx生态。 下载源码编译安装Nginx,可以获得定制Nginx的能力。这样不仅用助于性能的提升,还通过各类模块扩展了Nginx的功能。 Nginx源代码中有5个文件和5个一级目录,其中configure脚本极为关键,在它执行后,还会生成Makefile文件和objs目录,它们与定制化的模块、系统的高性能参数密切相关,此后才能正式编译Nginx。 下一篇,我将在《如何configure定制出属于你的Nginx》一文中介绍configure脚本的用法,配置文件的语法格式,以及如何配置出静态资源服务。 
在实时性要求较高的特殊场景下,简单的UDP协议仍然是我们的主要手段。UDP协议没有重传机制,还适用于同时向多台主机广播,因此在诸如多人会议、实时竞技游戏、DNS查询等场景里很适用,视频、音频每一帧可以允许丢失但绝对不能重传,网络不好时用户可以容忍黑一下或者声音嘟一下,如果突然把几秒前的视频帧或者声音重播一次就乱套了。使用UDP协议作为信息承载的传输层协议时,就要面临反向代理如何选择的挑战。通常我们有数台企业内网的服务器向客户端提供服务,此时需要在下游用户前有一台反向代理服务器做UDP包的转发、依据各服务器的实时状态做负载均衡,而关于UDP反向代理服务器的使用介绍网上并不多见。本文将讲述udp协议的会话机制原理,以及基于nginx如何配置udp协议的反向代理,包括如何维持住session、透传客户端ip到上游应用服务的3种方案等。UDP协议简介许多人眼中的udp协议是没有反向代理、负载均衡这个概念的。毕竟,udp只是在IP包上加了个仅仅8个字节的包头,这区区8个字节又如何能把session会话这个特性描述出来呢?图1 UDP报文的协议分层在TCP/IP或者 OSI网络七层模型中,每层的任务都是如此明确:物理层专注于提供物理的、机械的、电子的数据传输,但这是有可能出现差错的;数据链路层在物理层的基础上通过差错的检测、控制来提升传输质量,并可在局域网内使数据报文跨主机可达。这些功能是通过在报文的前后添加Frame头尾部实现的,如上图所示。每个局域网由于技术特性,都会设置报文的最大长度MTU(Maximum Transmission Unit),用netstat -i(linux)命令可以查看MTU的大小:  而IP网络层的目标是确保报文可以跨广域网到达目的主机。由于广域网由许多不同的局域网,而每个局域网的MTU不同,当网络设备的IP层发现待发送的数据字节数超过MTU时,将会把数据拆成多个小于MTU的数据块各自组成新的IP报文发送出去,而接收主机则根据IP报头中的Flags和Fragment Offset这两个字段将接收到的无序的多个IP报文,组合成一段有序的初始发送数据。IP报头的格式如下图所示:图2 IP报文头部IP协议头(本文只谈IPv4)里最关键的是Source IP Address发送方的源地址、Destination IP Address目标方的目的地址。这两个地址保证一个报文可以由一台windows主机到达一台linux主机,但并不能决定一个chrome浏览的GET请求可以到达linux上的nginx。4、传输层主要包括TCP协议和UDP协议。这一层最主要的任务是保证端口可达,因为端口可以归属到某个进程,当chrome的GET请求根据IP层的destination IP到达linux主机时,linux操作系统根据传输层头部的destination port找到了正在listen或者recvfrom的nginx进程。所以传输层无论什么协议其头部都必须有源端口和目的端口。例如下图的UDP头部:图3 UDP的头部TCP的报文头比UDP复杂许多,因为TCP除了实现端口可达外,它还提供了可靠的数据链路,包括流控、有序重组、多路复用等高级功能。由于上文提到的IP层报文拆分与重组是在IP层实现的,而IP层是不可靠的所有数组效率低下,所以TCP层还定义了MSS(Maximum Segment Size)最大报文长度,这个MSS肯定小于链路中所有网络的MTU,因此TCP优先在自己这一层拆成小报文避免的IP层的分包。而UDP协议报文头部太简单了,无法提供这样的功能,所以基于UDP协议开发的程序需要开发人员自行把握不要把过大的数据一次发送。对报文有所了解后,我们再来看看UDP协议的应用场景。相比TCP而言UDP报文头不过8个字节,所以UDP协议的最大好处是传输成本低(包括协议栈的处理),也没有TCP的拥塞、滑动窗口等导致数据延迟发送、接收的机制。但UDP报文不能保证一定送达到目的主机的目的端口,它没有重传机制。所以,应用UDP协议的程序一定是可以容忍报文丢失、不接受报文重传的。如果某个程序在UDP之上包装的应用层协议支持了重传、乱序重组、多路复用等特性,那么他肯定是选错传输层协议了,这些功能TCP都有,而且TCP还有更多的功能以保证网络通讯质量。因此,通常实时声音、视频的传输使用UDP协议是非常合适的,我可以容忍正在看的视频少了几帧图像,但不能容忍突然几分钟前的几帧图像突然插进来:-)UDP协议的会话保持机制有了上面的知识储备,我们可以来搞清楚UDP是如何维持会话连接的。对话就是会话,A可以对B说话,而B可以针对这句话的内容再回一句,这句可以到达A。如果能够维持这种机制自然就有会话了。UDP可以吗?当然可以。例如客户端(请求发起者)首先监听一个端口Lc,就像他的耳朵,而服务提供者也在主机上监听一个端口Ls,用于接收客户端的请求。客户端任选一个源端口向服务器的Ls端口发送UDP报文,而服务提供者则通过任选一个源端口向客户端的端口Lc发送响应端口,这样会话是可以建立起来的。但是这种机制有哪些问题呢?问题一定要结合场景来看。比如:1、如果客户端是windows上的chrome浏览器,怎么能让它监听一个端口呢?端口是会冲突的,如果有其他进程占了这个端口,还能不工作了?2、如果开了多个chrome窗口,那个第1个窗口发的请求对应的响应被第2个窗口收到怎么办?3、如果刚发完一个请求,进程挂了,新启的窗口收到老的响应怎么办?等等。可见这套方案并不适合消费者用户的服务与服务器通讯,所以视频会议等看来是不行。有其他办法么?有!如果客户端使用的源端口,同样用于接收服务器发送的响应,那么以上的问题就不存在了。像TCP协议就是如此,其connect方的随机源端口将一直用于连接上的数据传送,直到连接关闭。这个方案对客户端有以下要求:不要使用sendto这样的方法,几乎任何语言对UDP协议都提供有这样的方法封装。应当先用connect方法获取到socket,再调用send方法把请求发出去。这样做的原因是既可以在内核中保存有5元组(源ip、源port、目的ip、目的端口、UDP协议),以使得该源端口仅接收目的ip和端口发来的UDP报文,又可以反复使用send方法时比sendto每次都上传递目的ip和目的port两个参数。对服务器端有以下要求:不要使用recvfrom这样的方法,因为该方法无法获取到客户端的发送源ip和源port,这样就无法向客户端发送响应了。应当使用recvmsg方法(有些编程语言例如python2就没有该方法,但python3有)去接收请求,把获取到的对端ip和port保存下来,而发送响应时可以仍然使用sendto方法。 接下来我们谈谈nginx如何做udp协议的反向代理。Nginx的stream系列模块核心就是在传输层上做反向代理,虽然TCP协议的应用场景更多,但UDP协议在Nginx的角度看来也与TCP协议大同小异,比如:nginx向upstream转发请求时仍然是通过connect方法得到的fd句柄,接收upstream的响应时也是通过fd调用recv方法获取消息;nginx接收客户端的消息时则是通过上文提到过的recvmsg方法,同时把获取到的客户端源ip和源port保存下来。我们先看下recvmsg方法的定义:ssize_t recvmsg(int sockfd, struct msghdr *msg, int flags);相对于recvfrom方法,多了一个msghdr结构体,如下所示:struct msghdr {             void         *msg_name;       /* optional address */             socklen_t     msg_namelen;    /* size of address */             struct iovec *msg_iov;        /* scatter/gather array */             size_t        msg_iovlen;     /* # elements in msg_iov */             void         *msg_control;    /* ancillary data, see below */             size_t        msg_controllen; /* ancillary data buffer len */             int           msg_flags;      /* flags on received message */ };其中msg_name就是对端的源IP和源端口(指向sockaddr结构体)。以上是C库的定义,其他高级语言类似方法会更简单,例如python里的同名方法是这么定义的:(data, ancdata, msg_flags, address) = socket.recvmsg(bufsize[, ancbufsize[, flags]])其中返回元组的第4个元素就是对端的ip和port。配置nginx为UDP反向代理服务以上是nginx在udp反向代理上的工作原理。实际配置则很简单:# Load balance UDP-based DNS traffic across two servers stream {             upstream dns_upstreams {                         server 192.168.136.130:53;                         server 192.168.136.131:53;             }                  server {                         listen 53 udp;                         proxy_pass dns_upstreams;                         proxy_timeout 1s;                         proxy_responses 1;                         error_log logs/dns.log;             } }在listen配置中的udp选项告诉nginx这是udp反向代理。而proxy_timeout和proxy_responses则是维持住udp会话机制的主要参数。UDP协议自身并没有会话保持机制,nginx于是定义了一个非常简单的维持机制:客户端每发出一个UDP报文,通常期待接收回一个报文响应,当然也有可能不响应或者需要多个报文响应一个请求,此时proxy_responses可配为其他值。而proxy_timeout则规定了在最长的等待时间内没有响应则断开会话。如何通过nginx向后端服务传递客户真实IP最后我们来谈一谈经过nginx反向代理后,upstream服务如何才能获取到客户端的地址?如下图所示,nginx不同于IP转发,它事实上建立了新的连接,所以正常情况下upstream无法获取到客户端的地址:图4 nginx反向代理掩盖了客户端的IP上图虽然是以TCP/HTTP举例,但对UDP而言也一样。而且,在HTTP协议中还可以通过X-Forwarded-For头部传递客户端IP,而TCP与UDP则不行。Proxy protocol本是一个好的解决方案,它通过在传输层header之上添加一层描述对端的ip和port来解决问题,例如:但是,它要求upstream上的服务要支持解析proxy protocol,而这个协议还是有些小众。最关键的是,目前nginx对proxy protocol的支持则仅止于tcp协议,并不支持udp协议,我们可以看下其代码:可见nginx目前并不支持udp协议的proxy protocol(笔者下的nginx版本为1.13.6)。虽然proxy protocol是支持udp协议的。怎么办呢?方案1:IP地址透传可以用IP地址透传的解决方案。如下图所示:图5 nginx作为四层反向代理向upstream展示客户端ip时的ip透传方案这里在nginx与upstream服务间做了一些hack的行为:nginx向upstream发送包时,必须开启root权限以修改ip包的源地址为client ip,以让upstream上的进程可以直接看到客户端的IP。server {              listen 53 udp;          proxy_responses 1;          proxy_timeout 1s;          proxy_bind $remote_addr transparent;               proxy_pass dns_upstreams; }upstream上的路由表需要修改,因为upstream是在内网,它的网关是内网网关,并不知道把目的ip是client ip的包向哪里发。而且,它的源地址端口是upstream的,client也不会认的。所以,需要修改默认网关为nginx所在的机器。# route del default gw 原网关ip # route add default gw nginx的ipnginx的机器上必须修改iptable以使得nginx进程处理目的ip是client的报文。# ip rule add fwmark 1 lookup 100 # ip route add local 0.0.0.0/0 dev lo table 100  # iptables -t mangle -A PREROUTING -p tcp -s 172.16.0.0/28 --sport 80 -j MARK --set-xmark 0x1/0xffffffff这套方案其实对TCP也是适用的。方案2:DSR(上游服务无公网)除了上述方案外,还有个Direct Server Return方案,即upstream回包时nginx进程不再介入处理。这种DSR方案又分为两种,第1种假定upstream的机器上没有公网网卡,其解决方案图示如下:图6 nginx做udp反向代理时的DSR方案(upstream无公网)这套方案做了以下hack行为:1、在nginx上同时绑定client的源ip和端口,因为upstream回包后将不再经过nginx进程了。同时,proxy_responses也需要设为0。server {         listen 53 udp;     proxy_responses 0;         proxy_bind $remote_addr:$remote_port transparent;          proxy_pass dns_upstreams; }2、与第一种方案相同,修改upstream的默认网关为nginx所在机器(任何一台拥有公网的机器都行)。3、在nginx的主机上修改iptables,使得nginx可以转发upstream发回的响应,同时把源ip和端口由upstream的改为nginx的。例如:# tc qdisc add dev eth0 root handle 10: htb # tc filter add dev eth0 parent 10: protocol ip prio 10 u32 match ip src 172.16.0.11 match ip sport 53 action nat egress 172.16.0.11 192.168.99.10 # tc filter add dev eth0 parent 10: protocol ip prio 10 u32 match ip src 172.16.0.12 match ip sport 53 action nat egress 172.16.0.12 192.168.99.10 # tc filter add dev eth0 parent 10: protocol ip prio 10 u32 match ip src 172.16.0.13 match ip sport 53 action nat egress 172.16.0.13 192.168.99.10 # tc filter add dev eth0 parent 10: protocol ip prio 10 u32 match ip src 172.16.0.14 match ip sport 53 action nat egress 172.16.0.14 192.168.99.10方案3:DSR(上游服务有公网)DSR的另一套方案是假定upstream上有公网线路,这样upstream的回包可以直接向client发送,如下图所示:图6 nginx做udp反向代理时的DSR方案(upstream有公网)这套DSR方案与上一套DSR方案的区别在于:由upstream服务所在主机上修改发送报文的源地址与源端口为nginx的ip和监听端口,以使得client可以接收到报文。例如:# tc qdisc add dev eth0 root handle 10: htb # tc filter add dev eth0 parent 10: protocol ip prio 10 u32 match ip src 172.16.0.11 match ip sport 53 action nat egress 172.16.0.11 192.168.99.10结语以上三套方案皆可以使用开源版的nginx向后端服务传递客户端真实IP地址,但都需要nginx的worker进程跑在root权限下,这对运维并不友好。从协议层面,可以期待后续版本支持proxy protocol传递客户端ip以解决此问题。在当下的诸多应用场景下,除非业务场景明确无误的拒绝超时重传机制,否则还是应当使用TCP协议,其完善的流量、拥塞控制都是我们必须拥有的能力,如果在UDP层上重新实现这套机制就得不偿失了。 
第三章示例源代码:chapter3第四章示例源代码:chapter4第五章示例源代码:chapter5第六章示例源代码:chapter6