从代码看Nginx运维本质
2020-11-30 15:34:43 阿炯

本文系2016年运维世界大会深圳站的访谈总结,陶辉老师讲了到底Nginx怎么用、怎么玩,他有非常多的经验,不管是分布式,还是高性能;知道陶老师也混了很多大公司,阿里巴巴、腾讯,BAT都走遍了,接下来把舞台交给他,让他跟大家分享一下“通过代码看Nginx运维本质”。


【陶辉】:大家好!今天我跟大家分享的题目是“从代码看Nginx的运维本质”,为什么会选这个题目呢?我最初接触Nginx的是2009年,当时的最新版本是0.6.65,现在最新的Nginx稳定版本已经到了1.10,在座各位使用过Nginx的朋友能不能举一下手?几乎所有运维人都使用Nginx,所以我觉得压力山大,我今天如果说使用技巧或者说一些经验,相信大家在Google上可以很容易找到类似资料,这样的话今天的分享就没有什么意义,在大数据时代,我认为找到问题的答案是一件非常简单的事情,因为现在搜索引擎实在太强大了。但是我们想能够问出正确的问题,特别是在陌生前沿领域,在我们非常茫然无措的时候,如果能问出正确的问题就是一个重要的能力。

回到Nginx,我今天的分享希望达到的目的,就是把我们平常使用Nginx中,所做的所有事情,能跟Nginx的运行机制与原理结合起来,这样的话以后Nginx发布了一些新的功能,比如我们拉了新的第三方模块,有一些很新的配置,我们就能很好的使用它,这是我今天希望达到的目标。


最常见的Web架构有这么五个环节,一是Web请求,它的一生会经过这么五个组件,从上到下,一是预设的负载均衡,我相信只要我们的服务想提高容量,提高性能,一定有负载均衡,负载均衡往下一般会有Web加速服务器,因为虽然我们实际的请求是由应用服务器来做的,但是Tomcat这样的应用服务器,它们的出发点是功能很多,所以开发效率要求非常高,他们有很多设计模式,这些设计模式将会使用阻塞式调用,这样就会导致Web应用服务器的性能不是很好,所以一些静态资源,如CSS、JS文件,我们可以通过加速服务器直接返回,也减轻了应用服务器的压力。应用服务器最终是要落地的,数据通常落在数据库,数据库也是比较慢的,所以中间会有一个缓存。


从上到下请求的落地来说,如果我们直接在上面把请求返回,一个请求在加速服务器这里返回的话,通常只有几十毫秒,但到下面的缓存可能就要几百毫秒,如果有些页面是所有用户看起来都一样的话,就可以把它放在加速服务器。总结一下,Nginx上面的两个组件,负载均衡和Web加速服务器,刚刚提到的三个主要功能都是它非常擅长的,就像我刚刚说的,一个请求越在上面返回,整个性能表现越好,所以我们对上面组件的要求是希望它更快,我们选择Nginx的首要原因就是它非常快。第二个原因是节约机器,降低成本,同一台服务器,换成Nginx,每秒处理的请求更多,换句话说,Nginx处理每一个请求,它消耗的CPU指令更少、消耗的内存更少。还有一个非常重要的原因是我们现在使用Nginx,它的投入产出比非常高,如果一个架构,我在设计过程中,有很多组件,有很多方案,我一定会选一个经过广泛验证的,维护社区非常活跃,而不会做小白鼠,我相信大家应该都有做小白鼠的经验,一些很高大上的东西,看起来很新,总忍不住去尝试,最后踩了无数的坑。Nginx没有这个问题,现在有这么多人在使用,相对没有刚刚说的第一个问题。

第二个问题,它的模块设计非常好,扩展性非常好,很多第三方模块能够很容易的使用它的核心功能,Nginx是一个C程序,作为一个C程序,能实现这么丰富的Web功能,在于它的扩展性比较好。因为它的扩展性比较好,所以我们使用组件时不用很复杂的方案组合起来,因为Nginx本身足够了。

第三,我们可以不用造轮子,只搭积木,你只要重复造轮子,就一定要写代码,你只要写代码,一定有Bug,而且运维成本和开发成本也不成比例,如果我们去第三方模块,去网上找一些人家造好的轮子,就像搭积木一样,在Nginx config里面配置相应的功能,最后实现我们的效果。


运维Nginx的步骤,首先是编译,config非常重要,它决定了哪些模块怎么样起作用。其次是部署,安装到正确位置,开机自启,Access日志按天分割。三是修改配置,这是我们做得最主要的事情,要熟悉它的配置格式,特别是脚本式配置方法,可能有些人觉得有些不可思议,一方面觉得一个配置怎么那么强大,另一方面这个配置怎么那么恶心,不像正常的脚本语言。四是access日志,任何一个Web服务器都会记录access服务器,每一个请求如果记录下一行access日志,也就是把请求的结果记录下来了,所以里面非常丰富,但是每个人维护access日志的差别非常大,因为很多第三方模块自身是提供变量的,但有些人没有使用它,有些人使用它了,使用它以后可能就会记录非常多的信息,最终能够帮助你非常多的东西。有了access日志之后,首先你当然可以用眼睛看,甚至可以写一些脚本去找一找,什么样的请求是一个爬虫过来爬的,但更好的办法是用goaccess,类似这样开源的、免费的东西,它可以提供实时access日志分析。最后是error日志,相信大家写Nginx config没有不出错的情况,一定有出错的,写错了怎么办?通过经验的方法当然可以,但如果你的配置非常复杂的话,这个时候你还通过经验的方法效率就很差,我们看error日志会有很多提示,方便快速定位问题。另一方面,如果这个问题非常复杂,我们在config的时候把debug日志打开,帮我们定位更复杂的问题,后面我会相信说debug日志怎么看。


再到日常运维,维护Nginx主要就是提升性能,这是它的老本行,但是在我看来,提升性能的第一步一定是从整个产品的架构来看,从整个产品架构提升性能的时候,Web服务其实很明显,就是各个组件之间的访问速度不一致,碰到这个问题,最简单、粗暴、有效的方法是加缓存,我们一定要首先想到Nginx是支持缓存的,虽然它是磁盘式缓存,但往往效果非常好。接下来需要考虑优化传输效率,比如我们能不能优先考虑长连接代替短连接,大家知道这可能不是Nginx一家的事情,但对Nginx有好处。我们要考虑gzip压缩一下,公网带宽很昂贵,相信大家都了解过,我们还要考虑Nginx进程自身的消耗要降低,大家都知道磁盘寻址是比较慢的,COPY很快,但我们默认的情况下,每一行记一个日志,如果我们能批量的记,这样就可以减少磁盘寻址的数量。还有sendfile这样的零COPY技术,也可以让进程消耗更少。

限制流量,现在所谓的大数据时代,我需要什么数据的时候,会很轻松的几个小时之内搞一个scrapy爬虫,把一个网站的所有结构化数据拉下来,这对我来说是非常容易的事情,但作为运维服务人员来说,我特别讨厌这件事情,如果碰到爬虫的话,我当然想把它杜绝掉。限制速度是一种方法,限制速度还有一个很重要的原因是保护后面的应用服务器,刚才说了,应用服务器的设计模式决定了它非常慢,而且到了一定的阈值以后性能会急剧恶化,一定要通过限制速度保护后面的服务器。

最后一点是修改操作系统的配置,操作系统是大众化默认的配置,而Nginx跑在小众化场景下,不管是负载均衡还是Web加速度服务器,都是小众化场景,如果你不修改配置的话,它不太适合你做非常多的并发,如果你想做非常多的并发,就要提高Backlog,把端口范围扩大一下,TCP连接本身内核给它维护了一些内存,并发多了你就要缩小一些。


下面看一下Nginx到底能做些什么,这幅图从左向右看,有三种请求,相信大家对上面这两种非常熟悉,Web和EMAIL,TCP traffic是这两年才出现的,以前我们向找一个反向代理的时候,第一时间想如果是七层的,我们就找Nginx,如果是四层的,我们可能会找商业的,现在Nginx也支持四层反向代理,其实四层反向代理也很容易。这个图中绿色的部分在Nginx部分,反向代理放在后面,HTTP这一块会把静态内容处理一下。


大家提到Nginx的高性能时,一定会想是因为事件驱动框架,我们有一个配置叫events,选择一个内核,内核提供的这种模型,把一堆事件放在一个队列,Nginx进程从队列中一个一个取,取到一个处理一个,处理完了没事件了就在这边等,所有事件有很多种,连接建立、读、写、磁盘相关。


这样一个事件驱动框架的优点很明显,就像我刚刚说的,传统服务器像应用服务器,为了开发效率,一个请求在一个线程内如果没有处理完的话,通常不会处理第二个请求。而且为了编程的方便性,它会有很多阻塞式调用,就像这个图,进程1里面蓝色的请求,如果它自己调了一个阻塞式调用,当前条件不满足,它就切出去了。还有一种场景是我们为了保持并发性,会有很多进程或线程跑,都在跑的话,CPU是有限的,你的时间内用完了以后,往往得不到执行,往往要等其他的进程都执行掉。Nginx不是这样的,蓝色请求还没有执行完,绿色请求就开始执行,绿色请求没有执行完,橙色又开始执行了,所以它的上下文切换很少。当我们负载很小的时候,上下文你看不出来什么,但并发连接很多的时候,上下文切换是很恐怖的一件事情,会导致你的请求速度根本上不去。


事件驱动框架的缺点也很明显,就像下面这个图(PPT演示),假设绿色请求使用了第三方Nginx模块,而这个模块恰好你走到了它的一行使用了阻塞式调用的方法,就会导致这个进程上的所有其他请求全部卡住,可能这个进程上跑了几万个连接或者几十万个连接,这是很明显的性能下降问题,这个性能下降问题怎么解决?


我相信大家应该看过这篇文章,说Nginx和THREAD POOL,使用了THREAD POOL后有9倍的性能提升,官方的一篇文章,大家有没有看过?其实是这么一个问题,我们下载默认的官方版本Nginx,这一行配置sendfile是打开的,这是一个很先进的东西,叫零拷贝,是一个很先进的技术,它可以把磁盘上的东西在内核态拷贝到玩法上,绕过用户态进程,实际上是很先进的东西,但是就像Linus Torvalds所说,这个sendfile设计只是用来跑在page cache上的,也就是说没有跑在page cache上的时候,它就退化成阻塞式调用。如果Nginx维护的静态内容非常大,远远大于内存,这些静态资源访问的频率基本差不多,这个时候一定会撞到这种场景,sendfile退化成阻塞式调用,在这种情况下,你加上THREAD POOL,dofault线程池的名字,threads32个线程来处理,max queue大家知道多线程模型都是这么玩的,一堆生产者往队列中铺事件,一堆消费者取事件,然后在这个场景下才有9倍的提升。当然这个里面配置大家看得很清楚,TASKS QUEUE如果溢出了,就会出错,线程池的使用场景很狭窄,几乎所有第三方模块都用不了,它用起来还是比较困难。


Master-Work进程结构,默认情况下,master process on是默认开的,也就是生产环境下,是一个Master进程,多个work进程,还有cache相关进程。在一个架构下,我对它的总结就是清闲的Master和繁忙的Work,为什么这样说呢?因为我希望Master进程尽量稳定,它只有做的事情少才能稳定,而它做的事情确实很少,启动、读取配置文件、解析配置文件,监控work进程、子进程,监控命令,Work进程是出于实际请求的,我们希望它越繁忙越好,所以会加一些配置。比如worker_priority-10,大家都知道,Nginx之下,进程有优先级概念,动态优先级是内核自己玩的,我们控制不了,但它的静态优先级有40个,默认是0,如果把它调整到-10,就意味着同等进程条件下,Nginx的work进程更有机会获得CPU,如果内核分配时间片的时候,就会给它分配的时间更大。因为我们想让它繁忙,所以又加了这么一行“worker_processes auto”,auto就是说你有多少个CPU就配多少个work进程,就是希望一个work进程永远霸占CPU,永远在执行,我们还是希望它比较繁忙。我们又加了一个配置“worker_cpu_affinity auto”,也就是每一个work进程绑定到一个CPU上执行,还是希望你连切换都不要做了,就老老实实忙你的CPU就OK了,所以我们一直希望它比较繁忙。


Master-Work进程架构的优点,其实就是它的灰度发布做得非常好,比如上面这幅图,有四个Work进程(绿色),这四个进程是老的配置,我们改了配置,执行了-s reload,这个时候就会起四个黄色的用了新配置的Work进程,老的配置不再接受新的请求,黄色的work进程开始处理新的请求,老的当前请求处理完就退出,这是非常好的灰度升级的过程。

下面的图是升级binary,升级binary跟升级配置几乎相同,唯一的区别就在于我们的binary改了,所以会有两个Master进程,两个Master进程之间通过环境变量交互信息。同样的道理,黄色的老的当前请求处理完,它们自己退出,也是非常好的灰度升级,就是因为有这么好的灰度升级,所以Nginx一直没有做动态库,但是它最近终于妥协了,因为它做了商业版本的,现在有一个动态模块提出用动态库,下面简单说一下。


说动态库之前,先看一下它的监听端口复用,刚刚那张图Master-Work,打开listen socket,打开配置监听端口,这些东西是在Master进程做的,Master进程做完以后,work进程作为它的子进程,天然进行了listen socket,它们实际上是在共享listen socket,这个时候就有了竞争关系,有了竞争关系就有一个问题,叫做惊群,惊群在Nginx高版本已经把accep这样的调用解决了,但是epoll_wait这样的调用还是没有解决。Nginx怎么办呢?他有一个解决方案,自己搞了一个负载均衡锁,默认是打开的,负载均衡锁其实就是利用这把锁告诉同一时刻只有一种work进程在listen,其他work进程不能在listen,通过这种方式来避免惊群。


这样的解决方案好不好呢?肯定不好,因为你在用用户解决,实际上现在Nginx有一个版本支持到reuseport,我们在这里配了一个“server {listen 80reuseport;} ”以后,每一个work进程对80端口都会建一个listen socket,我们用lsof可以很轻松的看到这个现象。有了reuseport之后有多大的性能提升呢?ms轴上有三个区块,默认的default“{accept_mutexon;}”负载均衡锁打开了,中间是关闭的负载均衡锁,最后是使用reuseport。Y轴是每秒处理的请求数,红线是请求的平均时延latency,蓝线是时延方差,所以我们可以看得非常清楚,用了reuseport以后,处理量请求更多了,latency更短了,但是请求的波段也大大缩小了,但是我们想用它必须要用linux3.9内核以上,因为只有它才支持reuseport,我相信大家应该有很多服务是用2.6内核,这就比较悲剧了。


看一下Nginx模块结构,刚才一直说Nginx模块设计得非常非常好,到底怎么个好法呢?Nginx把模块分为这么几类,下面是核心模块,像SS,还有我刚刚说的线程池,这一类叫核心模块,然后有配置模块,解析配置文件的,所有的事件驱动叫做事件模块。刚刚我们看到那幅图,三类请求对应三类模块,四层反向代理、mail、HTTP,HTTP是最复杂的,我们大部分做的工作都在这一块,所以我详细说一下,HTTP模块又分为三类模块,一类是一个Web请求到达Nginx以后,我们处理这个请求的模块叫做请求处理模块,处理完以后,我们交给上游或者自己处理,处理完把这个响应发送给浏览器,这个响应再进行一次过滤处理,这种叫做响应过滤模块,反向代理还有一类模块。我们在执行config的时候,可以用with_module等命令把这些模块加进来。


动态模块,以前我们是把Nginx代码和模块代码放在Nginx编译系统之中,一起编译出一个binary,它的流程就是configure加入模块到c代码、编译进binary、启动时初始化模块数组。现在的动态模块用SOL以后,首先模块要支持动态模块的功能,编译出来就会在代码本身加一些Nginx符号,然后放到SO里面去。启动的时候读取Nginx config,读到module这个SO,diOpen这种方法把SO动态库打开,打开之后找到符号,加进去初始化。


下面重点说一下Web请求的处理,一个Web请求,看这幅图,可能有点绕,一个请求从上往下处理,首先第一步是读取请求头部,处理掉SSL,首先一定要把HTTP请求的header读完,然后再开始处理。我们会做一些URL、rewrite等事情,找相应的配置项location,然后会做限速,做完限速还有简单的用户密码等各种方式的验证,最后处理内容,处理内容的时候,可能静态内容自己处理掉,也可能交到上游服务器,这个请求在CONTENT时候处理,处理完以后把这个请求返回浏览器,返回浏览器会有各个过滤模块处理,比如我们把4K的内容压缩成1K,把一些大的图片压缩成小的图象,最后把日志记录下来,反馈给浏览器。


我刚刚说的这一大串流程,Nginx的HTTP模块把它们严格定义为11个阶段,所有模块必须属于这11个阶段之一,而且这11个阶段是严格有序的,不能逆行,只能从上到下,每一个阶段之内的所有模块定死了,只能一个一个执行,这就会造成很多可能会让我们困扰的现象,特别是模块非常多,加的配置非常复杂的时候,它们可能就会产生一些很奇怪的现象。很多模块的配置,像real_ip_header,它是在最上面、最开始的POST_READ阶段,if、return在REWRITE模块,limit conn在PREACCESS五阶段。详细看11个阶段各个模块处理的问题之前,我们先看一看每个请求究竟是哪一个location,我们在Nginx config里配的location在处理,因为我们配的大部分配置,要么是main级别,要么是Server级别,要么是location级别,但是不管怎么样,location级别会自动合并它所处的Serevr和main级别的配置,这些配置可能大部分情况下落在location上,所以我们先要看一看。比如说两个Serevr,80或者8000端口,我们的服务器上有很多网卡,有很多IP,有内网、公网等等,这个时候还会配一些URL,这样一种场景下,实际流程很简单。首先连接建立起来的时候,是一个TCP连接,自然是一个四元组,我找到下面这行配置,listen带IP地址,找到这行配置以后,我们把HTTP请求的header读完以后找到host,host是它的域名,匹配我们的server_name,我们很快找到这个server,然后通过URL找到location。


HTTP模块工作的时候是这样一个流程,首先master进程进入读取配置,也就是去扫Nginx config文件,假设扫到了event {},它就去调相应的event事件模块处理配置,如果是stream,那就去找相应的四层反向代理去做,如果是HTTP,那就是所有HTTP模块依次去找自己要处理的配置,找完配置,他才知道行为应该是什么样的,他会注册自己的回跳方法,这个回跳方法注册到我刚刚说的11个阶段里面,然后把所有的location配置加速处理。最后的监听端口,我刚才说了,reuseport会为每个work进程建立一个socket,这个阶段开始处理。处理完以后继续打开socket,然后启动work进程,监控进程状态,接受信号命令,work进程不停的处理事件,处理信号,处理事件的时候,如果是一个Web请求下来,就像我中间画的这个蓝色的框,这个HTTP模块在前面注册完了,处理的时候,接受完HTTP的包头以后,开始进入刚刚说的那11个阶段,所有模块依次开始执行,所有模块11个阶段执行完了,请求就执行完了,然后发送响应,发送响应的时候再调用所有过滤模块的方法,过滤模块又分为过滤header和过滤body。


看一个具体的例子,比如我们在这里做了这样一些配置,satisfy any可能是一个比较罕见的情况,一般默认都是12,allow、auth basic、gzip这些加了以后,实际运行原理可能是这样的,11个阶段我不可能全画出来,这里只画了三个阶段。比如说接受完HTTP头部以后,它一定是先到limit_conn,假设我们想对它做验证,但是现在我们对IT做了最大连接数目限制,这个时候limit_conn说这个连接我不处理的话,其实auth basic根本得不到机会执行。

同样的道理,进入所有preaccess阶段,也许我们还会加入第三方模块,这个第三方模块恰好也是执行preaccess阶段,也会有同样的问题。到了access阶段,access阶段其实还是比较复杂的,因为它多了一个东西叫satisfy any,默认satisfy all就是所有模块都要一个一个往后走,satisfy any是有一个走完了,它回答说“我通过了”,后面就不用再走了。因为auth basic模块在access模块之前,而且定死了改不了。

它们处理完之后一个一个这样跳转,这11个阶段都处理完以后,开始发动响应,首先过滤模块,这个数据也很重要,你本来是4K有序的支付流,你用gzip压缩以后,人已经不可读了,你再通过其他模块来处理肯定是乱的。image_filter也一样,你把图片内容都改了,header处理也是一样的,你有时候把一些header都改掉了,后面的模块怎么处理呢?所以过滤模块之间的顺序是非常重要的,后面我会说一下这个模块的数据是怎么定的。


说到模块数据之前,还要说一个跟模块数据有关的事情,我们用很多功能好像很强大的第三方模块的时候,这些模块支持配置时是支持变量式配置,它又不会说支持哪些变量,它可能在例子上写一小部分变量,那么我们是不是可以认为所有变量都支持呢?其实所有的内部变量都支持,为什么我说都支持呢?看它的原理就知道了,其实这方面太简单了,没有什么好举例的,实际上分为提供变量的模块和使用变量的模块,比如我跟access日志说我提供支持变量的用法,你可以在这里加很多符号,或者很多第三方模块都支持,它们叫做使用变量模块。master进程在启动过程中和读取配置过程中,它的流程是这样的,他顶会先做这件事情,我把它们做了上下的区分。Preconfiguration其实就是读Nginx config之前,它会在代码中写死说我这种模块提供一个变量,这个变量名是什么、解析方法是什么,解析方法就是通过HTTPheader支付流。提供变量模块做完这件事情以后,Nginx才开始解析Nginx config,这时候有很多模块开始使用这个变量,它们都是在这个之后,前面所有变量都可以使用。

实际处理过程中,我们接受了HTTP header到这11个阶段,11个阶段依次找所有模块,比如access日志模块,它说我走到最后个阶段了,就开始调用方法,把这个东西读出来,然后写到日志里面,所有模块的工作原理都是这样。


除了这类变量以外,还有一类变量,我们在Nginx config里调sent,这种叫做脚本类变量,脚本式变量有很多。还有反向代理类模块,UWSGI也是脚本类配置。脚本类配置的执行流程是这样的,每一个模块有自己的脚本,跟其他模块不搭,比如rewrite模块,它在启动过程中读Nginx config,它发现这是脚本类配置,就把它变成一条指令,指令数目是有限的,就那么几条,每种指令都放在配置里面,实际请求执行的时候,重要的事情强调三遍,11个阶段,到了这个阶段,rewrite模块开始生效,到了它以后,它先建立上下文,有一个变量站,任何一门语言,学过编译原理都知道,它会有一个站存储一些临时变量。它就像CPU的IP集成器一样,有一个IP集成器指向下一条要执行的指令。不同模块的脚本执行阶段是不一样的,比如反向代理是在CONTENT阶段,我们如果看debug日志时,就会发现他们打脚本日志是不一样的。


再说一下模块顺序,刚刚反复说模块顺序很重要,模块顺序是谁来定的呢?我们把Nginx版本下载完之后,它有一个auto目录,aute目录下面有一个叫modules的脚本,这个脚本里做了很多事,其中最重要的事就是把模块排序,这些模块的顺序官方默认的注入释写得很清楚,这个顺序很重要,不能改。但是第三方模块没写进去,第三方模块它也不知道,所以第三方模块可以任意插进去,插得对不对就要看作者。这些模块顺序确认好以后,我们要定位问题的时候,首先确认它到底是什么样的顺序,可以通过执行config以后生成一个objs目标目录,这里面有一个ngx modules.c文件,文件里有一个ngx modules数族,这个数族里定义了所有模块的顺序,但我们要注意一下,其他模块都是按照我们看到的,但是过滤模块是反的,是-filter字样,因为数族里的顺序对于过滤模块来说不是最终的顺序,因为最终它是往列表尾插列表头执行,是一个反向的过程。


最后说一下DEBUG级别日志的阅读,大家看DEBUG的时候满屏都是日志,根本没法看,你做性能测试时挺困难的,但你可以配DEBUG Connection。定位问题的时候,对于我来说,我先要找第一行,把请求找到,找到之后看这个日志“http request line”,根据URL找到第一行日志,URL不能完全决定的时候,我们还要看header,header再来确认是不是我要找的日志。所有header读完以后要打一行“http header done”,这就像我刚刚反复说的,首先我们要读HTTP头部,然后再进入那11个阶段的处理。

接下来我们就要看11个阶段怎么处理,在处理它之前,我们一定要找到哪些模块使用什么样的配置处理这行请求,我们要先找到location,因为找到location就可以反过来参照Nginx config找到那些配置。有什么样的日志呢?有这样几种:testlocation,我现在去测试某个location能不能处理这个请求,找到了以后会有一个using configuratoin,这个时候我们就知道哪个location在处理,就能对应到相应的模块,看它有没有正常工作。接下来找哪些模块处理了,就像我刚刚说的,那11个阶段都会打出来,每个模块在处理过程中都会打DEBUG日志,刚刚我一直说脚本,脚本指令有很多,有一类叫做COPY指令,这是反向代理特别喜欢打的东西,用COPY指令时它会打 copy。

最后是发送HTTP响应,首先会把完整的头部打出来,我们一般都能很轻易的找到,接下来它会把所有头部做过滤模块日志的处理,每一个固定模块都会打相应的日志,发送多少字节也有。


做一个简单的总结,有些公司可能是一整个团队在维护Nginx,有些公司是一个人不光要维护Nginx,还要维护一堆东西,尤其对于后者,我们应该尽量做Nginx最擅长做的事情,这才能做到低投入、高产出,而你想做Nginx最擅长的事情,而不是做所有都能做的事情,这个时候最重要的一点是你要理解Nginx,对一个没有接触过Nginx的人来说,首先你应该看几小时几天学会东西的教程,亲自把Nginx搭起来,所有功能使用起来,接下来要对每一个相应的领域渠道各个专家高手的博客上去找一找,当你的知识由定到面时,你就可以学Nginx的架构,Nginx是一个C程序,你一定要知道你的Nginx所跑的操作系统的独特内核运行机制。接下来你要理解产品场景,最后了解高并发相关的性能知识,不管看了多少文章,这些文章都有上下文,但作者不可能把所有的上下文都写出来,有些时候一些场景会误导你,这个时候看源码是最真实的,最能反映情况的,然后做Gdb调试,我认为这是最行之有效的方法。

【刘宇】:太精彩了,坚持到最后你们真的很强,后面还有人站着,我发现一个问题,这个广告没有二维码,陶辉在Nginx上的造诣真的非常深,从源码的分析,以前我用Nginx的时候,最多看一下参数选项代表的含义以及跟操作系统内核之间的基础关系,然后就几乎不管了。今天听完之后,原来Nginx里面的底层逻辑调用,包括模块加载顺序对性能的影响这么大,我恍然大悟,再次感谢陶辉。

【提问】:你给大家展示了很美好的景象,但有美好就有不美好,用Nginx来取代squid、varnish会不会有一些不美好的事情,你对这个问题怎么看?

【陶辉】:我稍微延伸一下,我做架构设计的时候其实有很多种方案,我往往不会拘泥于一种方案,就像我刚刚一直在说的,我们一直要做低投入高产出的事情,我最后还说了一件事情,我不想做Nginx所有能做的事情,当然如果我有一个团队能维护Nginx的话,我当然愿意,他们还可以贡献很多开源代码,但如果做不到这一点,我往往会选择它最适合做的事情。squid和varnish两个软件做反向代理都有很悠久的历史,特别是squid,比Nginx要悠久得多,它们处理的方法很多都不一样,比如HTTP的body,Nginx是先收完,再往后面传,squid是收一点传一点,这实际上是跟它复杂的业务相关的,我反复在说Nginx有这么复杂的模块,就会有一些约束,这些约束就会导致它在某些极限的场景下可能表现不如你所想的场景。我再把它延伸一点,你在你的公司、你的团队做一件事情的时候,需要考虑性价比,这可能是很屌丝的词,高富帅买东西从来不考虑性价比,但我认为我们做事情的时候一定要考虑性价比,如果你的Nginx跑了几千台、上万台机器,你优化了1%,这为公司节约了多少成本?

我们首先要清楚Nginx就是这样的,它的模块架构设计导致它处理body就必须这个样子。我最开始画的第二张图不知道你看到没有,负载均衡和一环套一环的时候,如果它们能布属在一起的话,往往你越在前面返回,少跑一个环节,速度肯定快了很多,所以你从整个架构考虑的时候,往往能容忍它的不足。还是我刚刚一直在说的,只有你理解了Nginx,你只会选择做它最擅长做的事,它不擅长做的事你能够理解它。

【提问】:我前面听的时候一直有一个很在意的东西,sendfile在什么情况下会遇到你说的阻塞?

【陶辉】:刚刚可能说得太简短了,其实要想触发它的话,做静态服务或者做cache都会导致这个问题,我们要从Nginx所在这台机器上把文件读出来,发送给客户端浏览器的时候,就有可能触发。什么时候会触发呢?也就是我们发的文件不可能落到page cache上,我记得当时Nginx做了一个测试场景,8G内存,文件大小32G就触发到了,关键是你浏览器的行为、用户的行为,如果你用户的行为以很热点的方式落在少量文件上,实际上你的操作系统高速缓存非常有效,这个时候就触发不到。而当你的用户实际上没有什么热点文件的时候,它就开始出现了,出现了以后,sendfile才会有多少倍的提升,否则是没有意义的,而你触发到这个的时候,证明你的服务器的量已经达到非常大的程度了至少是几十万的请求处理量。

【提问】:一开始说到爬虫的问题,你给了一个限制的方法,限制速度,我们也有爬虫,但用的是很LOW的方法,Nginx有没有更优雅的方法?

【陶辉】:你说的问题是一个大问题,因为我自己也经常爬数据,但我也经常防御爬数据,两个角色都干。我看过很多种解决方法,我相信大家经常爬一爬链家网,你会发现它的解决方案挺好的,它的核心目的是确认你是不是人,到底是人在操作,还是爬虫在操作,如果你的速度达到一定程度,按照我的业务场景下,我的业务通常一个页面一个页面点击,最大的程度只能达到这样的程度,爬虫爬起来多快,如果是这种场景下,发现了,我就搞一个验证方法,通过图片等等各种方案都可以,验证你到底是不是人,如果是的话我就不做限制,这样的方案目前来看挺好。

【提问】:陶老师,我有两个小问题,第一个问题是work线程池工作机制是怎么样的?主要是针对后端服务器做线程池,还是针对客户端做?第二个问题是当我们在高并发的情况下,采用长连接好还是用短连接好?

【陶辉】:第一个问题,其实我刚刚简单说了一遍,但没有详细说清楚,线程池我刚刚说的是其他模块很难使用它,我还说目前只有发文件时才能使用它,所以它的功能是这样的,前端所有模块都处理完了,最后发现是静态文件内容,我把它读出来发出去,这个时候它把sendfile方法放到线程池里执行,就是这么一个场景,其他场景都用不了,所以跟反向代理都没有关系,它只是在发文件的时候把它扔到池子里面做,发完了就不再做了,线程池目前只做这件事,我相信未来可以做更多事情。

第二个问题,用长连接好还是短连接好,这是毫无疑问的,一定是长连接好,如果你开发追求的是速度,或者不想解决繁琐问题的时候,用短连接更好,或者不是你们开发的,改第三方的东西成本很高,这个时候你用短连接,但是你问我哪个效率高,毫无疑问一定是长连接。