陶辉 《深入理解Nginx:模块开发与架构解析》作者 极客时间《Nginx核心知识100讲》讲师
浏览 5.3 K
文章 12
订阅 110
从通用规则中学习Nginx模块的定制指令 陶辉 发表于 : 2020-09-21 10:48

上一篇文章中,我介绍了如何定制属于你自己的Nginx,本文将介绍nginx.conf文件的配置语法、使用方式,以及如何学习新模块提供的配置指令。每个Nginx模块都可以定义自己的配置指令,所以这些指令的格式五花八门。比如content_by_lua_block后跟着的是Lua语法,limit_req_zone后则跟着以空格、等号、冒号等分隔的多个选项。这些模块有没有必然遵循的通用格式呢?如果有,那么掌握了它,就能快速读懂生产环境复杂的nginx.conf文件。其次,我们又该如何学习个性化十足的模块指令呢?其实,所有Nginx模块在介绍它的配置指令时,都遵循着相同的格式:Syntax、Default、Context、Description,这能降低我们的学习门槛。如果你还不清楚这一套路,那就只能学习其他文章翻译过的二手知识,效率很低。比如搭建静态资源服务用到的root、alias指令,该如何找到、阅读它的帮助文档?为什么官方更推荐使用root指令?alias指令又适合在哪些场景中使用呢? nginx.conf配置文件中的语法就像是一门脚本语言,你既可以定义变量(set指令),也可以控制条件分支(if指令),还有作用域的概念(server{}块、location{}块等)。所以,为复杂的业务场景写出正确的配置文件,并不是一件很容易的事。为此,Nginx特意针对vim编辑器提供了语法高亮功能,但这需要你手动打开,尤其是include文件散落在磁盘各处时。本文将会系统地介绍nginx.conf配置文件的用法,并以搭建静态资源服务时用到的root、alias指令为例,看看如何阅读Nginx模块的指令介绍。同时,本文也是Nginx开源社区基础培训系列课程第一季,即6月11日晚第2次视频直播的部分文字总结。快速掌握Nginx配置文件的语法格式Nginx是由少量框架代码、大量模块构成的,其中,Nginx框架会按照特定的语法,将配置指令读取出来,再交由模块处理。因此,Nginx框架定义了通用的语法规则,而Nginx模块则定义了每条指令的语法规则,作为初学者,如果将学习目标定为掌握所有的配置指令,方向就完全错了,而且这是不可能完成的任务。比如,ngx_http_lua_module模块定义了content_by_lua_block指令,只要它符合框架定义的{}块语法规则,哪怕大括号内是一大串Lua语言代码,框架也会把它交由ngx_http_lua_module模块处理。因此,下面这行指令就是合法的:content_by_lua_block {ngx.say("Hello World ")}再比如,ngx_http_limit_req_module模块定义了limit_req_zone指令,只要它符合指令行语法(以分号;结尾),框架就会将指令后的选项将由模块处理。所以,即使下面这行指令出现了r/s(每秒处理请求数)这样新定义的单位,仍然是合法的:limit_req_zone $binary_remote_addr zone=one:10m rate=1r/s;所以,在我看来,只要弄清楚了以下2点,就能快速掌握Nginx配置文件,:1.     Nginx框架定义了每条指令的基本格式,这是所有模块必须遵守的规则,这包括以下5条语法:n  通过{}大括号作为分隔符的配置块语法。比如http{ }、location{ }、upstream{ }等等,至于配置块中究竟是放置Javascript语言、Lua语言还是字符串、数字,这完全由定义配置块的Nginx模块而定。n  通过;分号作为分隔符的指令语法。比如root html;就打开了静态资源服务。n  以#作为关键字的注释语法。比如#pid logs/nginx.pid;指令就是不会生效的。n  以$作为关键字的变量语法。变量是Nginx模块之间能够互相配合的核心要素,也是Nginx与管理员之间的重要接口,通过$变量名的形式,就可以灵活控制Nginx模块的行为。下一篇文章我会详细介绍Nginx变量。n  include指令可以将其他配置文件载入到nginx.conf中,这样可以提升配置的可维护性。例如includemime.types;语句,就将Content-Type与文件后缀名的映射关系,放在了独立的mime.types文件中,降低了耦合性。2.     Nginx框架为了提高模块解析指令选项的效率,提供了一系列通用的工具函数,绝大多数模块都会使用它们,毕竟这降低了模块开发的难度以及用户的学习成本。比如,当配置文件中包含字节数时,Nginx框架提供了ngx_conf_set_size_slot函数, 各模块通过它就可以解析以下单位: 空间单位 意义 k/K KB m/M MB g/G GB 因此,limit_req_zone指令中zone=one:10m中就定义10MB的共享内存,这替代了很不好理解的10485760字节。再比如,读取时间可以使用以下单位: 时间单位 意义 ms 毫秒 s 秒 m 分钟 h 小时 d 天 w 周 M 月 y 年 这样,ssl_session_cache shared:SSL:2h;指令就设置TLS会话信息缓存2小时后过期。除以上规则外,如果编译了pcre开发库后,你还可以在nginx.conf中使用正则表达式,它们通常以~符号打头。如何使用Nginx配置文件?掌握了语法规则后,nginx.conf配置文件究竟是放在哪里的呢?编译Nginx时,configure脚本的--prefix选项可以设置Nginx的运行路径,比如:./configure –prefix=/home/nginx此时,安装后的Nginx将会放在/home/nginx目录,而配置文件就会在/home/nginx/conf目录下。如果你没有显式的指--prefix选项,默认路径就是/usr/local/nginx。由于OpenResty修改了configure文件,因此它的默认路径是/usr/local/openresty/nginx。在默认路径确定后,nginx.conf配置文件就会放在conf子目录中。当然,通过--conf-path选项,你可以分离它们。另外在运行Nginx时,你还可以通过nginx-c PATH/nginx.conf选项,指定任意路径作为Nginx的配置文件。由于配置语法比较复杂,因此Nginx为vim编辑器准备了语法高亮功能。在Nginx源代码中,你可以看到contrib目录,其中vim子目录提高了语法高亮功能:[contrib]# tree vim  vim  |-- ftdetect  | `-- nginx.vim  |-- ftplugin  | `-- nginx.vim  |-- indent  | `-- nginx.vim  `-- syntax   `-- nginx.vim当你将contrib/vim/* 复制到~/.vim/目录时(~表示你当前用户的默认路径,如果.vim目录不存在时,请先用mkdir创建),再打开nginx.conf你就会发现指令已经高亮显示了: 出于可读性考虑,你或许会将include文件放在其他路径下,此时再用vim打开这些子配置文件,可能没有语法高亮效果。这是因为contrib/vim/ftdetect/nginx.vim文件定义了仅对4类配置文件使用语法高亮规则://对所有.nginx后缀的配置文件语法高亮  au BufRead,BufNewFile *.nginx set ft=nginx  //对/etc/nginx/目录下的配置文件语法高亮  au BufRead,BufNewFile */etc/nginx/* set ft=nginx  //对/usr/local/nginx/conf/目录下的配置文件语法高亮  au BufRead,BufNewFile */usr/local/nginx/conf/* set ft=nginx  //对任意路径下,名为nginx.conf的文件语法高亮  au BufRead,BufNewFile nginx.conf set ft=nginx因此,你可以将这类文件的后缀名改为.nginx,或者将它们移入/etc/nginx/、/usr/local/nginx/conf/目录即可。当然,你也可以向ftdetect/nginx.vim添加新的识别目录。即使拥有语法高亮功能,对于生产环境中长达数百、上千行的nginx.conf,仍然难以避免出现配置错误。这时可以通过nginx -t或者nginx -T命令,检查配置语法是否正确。出现错误时,Nginx会在屏幕上给出错误级别、原因描述以及到底是哪一行配置出现了错误。例如:# nginx -t  nginx: [emerg] directive "location" has no opening "{" in /usr/local/nginx/conf/notflowlocation.conf:1281  nginx: configuration file /usr/local/nginx/conf/nginx.conf test failed从上面的错误信息中,我们知道Nginx解析配置文件失败,错误发生在include的子配置文件/usr/local/nginx/conf/notflowlocation.conf的第1281行,从描述上推断是location块的配置出现了错误,可能是缺失了大括号,或者未转义的字符导致无法识别出大括号。当你修改完配置文件后,可以通过nginx -s reload命令重新载入指令。这一过程不会影响正在服务的TCP连接,在描述Nginx进程架构的文章中,我会详细解释其原因。搭建静态资源服务,root与alias有何不同?接下来我们以root和alias指令为例,看看如何掌握配置指令的使用方法。配置指令的说明,被放置在它所属Nginx模块的帮助文档中。因此,如果你对某个指令不熟悉,要先找到所属模块的说明文档。对于官方模块,你可以进入nginx.org站点查找。搭建静态资源服务的root/alias指令是由ngx_http_core_module模块实现的,因此,我们可以进入http://nginx.org/en/docs/http/ngx_http_core_module.html页面寻找指令介绍,比如root指令的介绍如下所示:Syntax:root path;  Default: root html;  Context:http, server, location, if in location这里Syntax、Default、Context 3个关键信息,是所有Nginx配置指令共有的,下面解释下其含义:l  Syntax:表示指令语法,包括可以跟几个选项,每个选项的单位、分隔符等。rootpath指令,可以将URL映射为磁盘访问路径path+URI,比如URL为/img/a.jpg时,磁盘访问路径就是html/img/a.jpg。注意,这里path既可以是相对路径,也可以是绝对路径。作为相对路径,path的前缀路径是由configure --prefix指定,也可以在运行时由nginx -p path指定。l  Default:表示选项的默认值,也就是说,即使你没有在nginx.conf中写入root指令,也相当于配置了root html;l  Context:表示指令允许出现在哪些配置块中。比如root可以出现在server{}中,而alias则只能出现在location{}中。为什么root指令的Context,允许其出现在http{ }、server{ }、location { }、if { }等多个配置块中呢?这是因为,Nginx允许多个配置块互相嵌套时,相同指令可以向上继承选项值。例如下面两个配置文件是完全等价的:server{   root html;   location / {   } }  server{   root html;   location / {   root html;   } }这种向上承继机制,可以简化Nginx配置文件。因此,使用root指令后,不用为每个location块重复写入root指令。相反,alias指令仅能放置在location块中,这与它的使用方式有关:Syntax:alias path;  Default:—  Context:locationalias的映射关系与其所属的location中匹配的URI前缀有关,比如当HTTP请求的URI为/a/b/c.html时,在如下配置中,实际访问的磁盘路径为/d/b/c.html:location /a {   alias /d;  }因此,当URI中含有磁盘路径以外的前缀时,适合使用alias指令。反之,若完整的URI都是磁盘路径的一部分时,则不妨使用root指令。学习其他指令时,如果你不清楚它属于哪一个模块,还可以查看以字母表排序的指令索引http://nginx.org/en/docs/dirindex.html页面,点击后会进入所属模块的指令介绍页面。如果是第三方模块,通常在README文件中会有相应的指令介绍。比如OpenResty模块的指令会放在GitHub项目首页的README文件中: 而TEngine模块的指令介绍则会放在tengine.taobao.org网站上: 从这两张截图中可以看到,第三方模块在解释指令的用法时,同样遵循着上文介绍过的方式。小结本文介绍了Nginx配置文件的使用方法。学习Nginx的通用语法时,要先掌握Nginx框架解析配置文件的5条基本规则,这样就能读懂nginx.conf的整体结构。其次,当模块指令包含时间、空间单位时,会使用Nginx框架提供的通用解析工具,熟悉这些时、空单位会降低你学习新指令的成本。配置文件的位置,可以由编译期configure脚本的—prefix、--conf-path选项指定,也可以由运行时的-p选项指定。复杂的配置文件很容易出错,通过nginx -t/T命令可以检测出错误,同时屏幕上会显示出错的文件、行号以及原因,方便你修复Bug。用vim工具编辑配置文件时,将Nginx源码中contrib/vim/目录复制到~/.vim/目录,就可以打开语法高亮功能。对于子配置文件,只有放置在/etc/nginx或者/usr/local/nginx/conf目录中,或者后缀为.nginx时,才会高亮显示语法。当然,你可以通过ftdetect/nginx.vim文件修改这一规则。学习模块指令时,要从它的帮助文档中找到指令的语法、默认值、上下文和描述信息。比如,root和alias的语法相似,但alias没有默认值,仅允许出现在location上下文中,这实际上与它必须结合URI前缀来映射磁盘路径有关。由于每个Nginx模块都能定义独特的指令,这让nginx.conf变成了复杂的运维界面。在掌握了基本的配置语法,以及第三方模块定义指令时遵循的潜规则后,你就能游刃有余地编写Nginx配置文件。 

点赞 4
0 条评论
都是事件驱动,为什么Nginx的性能远高于Redis? 陶辉 发表于 : 2020-09-16 09:31

谈到Redis缓存,我们描述其性能时会这么说:支持1万并发连接,几万QPS。而我们描述Nginx的高性能时,则会宣示:支持C10M(1千万并发连接),百万级QPS。Nginx用C语言开发,而Redis是用同一家族的C++语言开发的,C与C++在性能上是同一级数的。Redis与Nginx同样使用了事件驱动、异步调用、Epoll这些机制,为什么Nginx的并发连接会高出那么多呢?(本文不讨论Redis分布式集群)这其实是由进程架构决定的。为了让进程占用CPU的全部计算力,Nginx充分利用了分时操作系统的特点,比如增加CPU时间片、提高CPU二级缓存命中率、用异步IO和线程池的方式回避磁盘的阻塞读操作等等,只有清楚了Nginx的这些招数,你才能将Nginx的性能最大化发挥出来。为了维持Worker进程间的负载均衡,在1.9.1版本前Nginx使用互斥锁,基于八分之七这个阈值维持了简单、高效的基本均衡。而在此之后,使用操作系统提供的多ACCEPT队列,Nginx可以获得更高的吞吐量。本文将会沿着高性能这条主线介绍Nginx的Master/Worker进程架构,包括进程间是如何分担流量的,以及默认关闭的多线程模式又是如何工作的。同时,本文也是Nginx开源社区基础培训系列课程第一季,即6月18日晚第3次直播课的部分文字总结。如何充分使用多核CPU?由于散热问题,CPU频率已经十多年没有增长了。如下图统计了1970年到2018年间CPU性能的变化,可以看到表示频率的绿色线从2005年后就不再升高,服务器多数都在使用能耗比更经济的2.x GHz CPU: (图片来源:https://www.karlrupp.net/2018/02/42-years-of-microprocessor-trend-data/)我们知道,CPU频率决定了它的指令执行速度,当频率增长陷入瓶颈,这意味着所有单进程、单线程的软件性能都无法从CPU的升级上获得提升,包括本文开头介绍的Redis服务就是如此。如果你熟悉JavaScript语言,可能使用过NodeJS这个Web服务,虽然它也是高并发服务的代表,但同样受制于单进程、单线程架构,无法充分CPU资源。CPU厂商对这一问题的解决方案是横向往多核心发展,因此上图中表示核心数的黑色线至2005年后快速上升。由于操作系统使用CPU核心的最小单位是线程,要想同时使用CPU的所有核心,软件必须支持多线程才行。当然,进程是比线程更大的调度粒度,所以多进程软件也是可以的,比如Nginx。下图是Nginx的进程架构图,可以看到它含有4类进程:1个Master管理进程、多个Worker工作进程、1个CacheLoader缓存载入进程和1个Cache Manager缓存淘汰进程。 其中,Master是管理进程,它长期处于Sleep状态,并不参与请求的处理,因此几乎不消耗服务器的IT资源。另外,只有在开启HTTP缓存后,Cache Loader和Cache Manager进程才存在,其中,当Nginx启动时加载完磁盘上的缓存文件后,Cache Loader进程也会自动退出。关于这两个 Cache进程,我会在后续介绍Nginx缓存时中再详细说明。负责处理用户请求的是Worker进程,只有Worker进程能够充分的使用多核CPU,Nginx的QPS才能达到最大值。因此,Worker进程的数量必须等于或者大于CPU核心的数量。由于Nginx采用了事件驱动的非阻塞架构,繁忙时Worker进程会一直处于Running状态,因此1个Worker进程就能够完全占用1个CPU核心的全部计算力,如果Worker进程数超过了CPU核心数,反而会造成一些Worker进程因为抢不到CPU而进入Sleep状态休眠。所以,Nginx会通过下面这行代码自动获取到CPU核心数:ngx_ncpu = sysconf(_SC_NPROCESSORS_ONLN);如果你在nginx.conf文件中加入下面这行配置:worker_processes auto;Nginx就会自动地将Worker进程数设置为CPU核心数:if (ngx_strcmp(value[1].data, "auto") == 0) {  ccf->worker_processes = ngx_ncpu;   return NGX_CONF_OK; }为了防止服务器上的其他进程占用过多的CPU,你还可以给Worker进程赋予更高的静态优先级。Linux作为分时操作系统,会将CPU的执行时间分为许多碎片,交由所有进程轮番执行。这些时间片有长有短,从5毫秒到800毫秒不等,内核分配其长短时,会依据静态优先级和动态优先级。其中,动态优先级由内核根据进程类型自动决定,比如CPU型进程就能比IO型进程获得更长的时间片,而静态优先级可以通过setpriority函数设置。在Linux中,静态优先级共包含40级,从-20到+19不等,其中-20表示最高优先级。进程的默认优先级是0,所以你可以通过调级优先级,让Worker进程获得更多的CPU资源。在nginx.conf文件中,worker_priority配置就能设置静态优先级,比如:worker_priority -10;由于每个CPU核心都拥有一级、二级缓存(Intel的Smart Cache三级缓存是所有核心共享的),为了提高这两级缓存的命中率,还可以将Worker进程与CPU核心绑定在一起。CPU缓存由于离计算单元更近,而且使用了更快的存储介质,所以二级缓存的访问速度不超过10纳秒,相对应的,主存存取速度至少在60纳秒以上,因此频繁命中CPU缓存,可以提升Nginx指令的执行速度。在nginx.conf中你可以通过下面这行配置绑定CPU:worker_cpu_affinity auto;Nginx的多进程架构已经能够支持C10M级别的高并发了,那么Nginx中的多线程又是怎么回事呢?这要从Linux文件系统的非阻塞调用说起。Worker进程上含有数万个并发连接,在处理连接的过程中会产生大量的上下文切换。由于内核做一次切换的成本大约有5微秒,随着并发连接数的增多,这一成本是以指数级增长的。因此,只有在用户态完成绝大部分的切换,才有可能实现高并发。想做到这一点,只有完全使用非阻塞的系统调用。对于网络消息的传输,non-block socket可以完美实现。然而,Linux上磁盘文件的读取却是个大问题。我们知道,机械硬盘上定位文件很耗时,由于磁盘转速难以提高(服务器磁盘的转速也只有10000转/秒),所以定位操作需要8毫秒左右,这是一个很高的数字。写文件时还可以通过Page Cache磁盘高速缓存的write back功能,先写入内存再异步回盘,读文件时则没有好办法。虽然Linux提供了原生异步IO系统调用,但在内存紧张时,异步AIO会回退到阻塞API(FreeBSD操作系统上的AIO没有这个问题)。所以,为了缩小阻塞API的影响范围,Nginx允许将读文件操作放在独立的线程池中执行。比如,你可以通过下面这2条配置,为HTTP静态资源服务开启线程池:thread_pool name threads=number [max_queue=number]; aio threads[=pool];这个线程池是运行在Worker进程中的,并通过一个任务队列(max_queue设置了队列的最大长度),以生产者/消费者模型与主线程交换数据,如下图所示: 在极端场景下(活跃数据占满了内存),线程池可以将静态资源服务的性能提升9倍,具体测试可以参见这篇文章:https://www.nginx.com/blog/thread-pools-boost-performance-9x/。到这里你可能有个疑问:又是多进程,又是多线程,为什么Nginx不索性简单点,全部使用多线程呢?这主要由2个原因决定:l  首先,作为高性能负载均衡,稳定性非常重要。由于多线程共享同一地址空间,一旦出现内存错误,所有线程都会被内核强行终止,这会降低系统的可用性;l  其次,Nginx的模块化设计允许第三方代码嵌入到核心流程中执行,这虽然大大丰富了Nginx生态,却也引入了风险。因此,Nginx宁肯选用多进程模式使用多核CPU,而只以多线程作为补充。Worker进程间是如何协同处理请求的?当一个进程监听了80端口后,其他进程绑定该端口时会失败,通常你会看到“Address already in use”的提示。那么,所有Worker进程同时监听80或者443端口,又是怎样做到的呢?如果你用netstat命令,可以看到只有进程ID为2758的Master进程监听了端口: Worker进程是Master的子进程,用ps命令可以从下图中看到,2758有2个Worker子进程,ID分别为3188和3189: 由于子进程自然继承了父进程已经打开的端口,所以Worker进程也在监听80和443端口,你可以用lsof命令看到这一点: 我们知道,TCP的三次握手是由操作系统完成的。其中,成功建立连接的socket会放入ACCEPT队列,如下图所示: 这样,当Worker进程通过epoll_wait的读事件(Master进程不会执行epoll_wait函数)获取新连接时,就由内核挑选1个Worker进程处理新连接。早期Linux内核的挑选算法很糟糕,特别是1个新连接建立完成时,内核会唤醒所有阻塞在epoll_wait函数上的Worker进程,然而,只有1个Worker进程,可以通过accept函数获取到新连接,其他进程获取失败后重新休眠,这就是曾经广为人知的“惊群”现象。同时,这也很容易造成Worker进程间负载不均衡,由于每个Worker进程绑定1个CPU核心,当部分Worker进程中的并发TCP连接过少时,意味着CPU的计算力被闲置了,所以这也降低了系统的吞吐量。Nginx早期解决这一问题,在通过应用层accept_mutex锁完成的,在1.11.3版本前它是默认开启的:accept_mutex on;这把锁的工作原理是这样的:同一时间,通过抢夺accept_mutex锁,只有唯一1个持有锁的Worker进程才能将监听socket添加到epoll中:if (ngx_shmtx_trylock(&ngx_accept_mutex)) {  ngx_listening_t* ls = cycle->listening.elts;   for (ngx_uint_ti = 0; i < cycle->listening.nelts; i++) {  ngx_connection_t * c = ls[i].connection;  if (ngx_add_event(c->read, NGX_READ_EVENT, 0) == NGX_ERROR) {  return NGX_ERROR;  }  } }这就解决了“惊群”问题,我们再来看负载均衡功能是怎么实现的。在nginx.conf中可以通过下面这行配置,设定每个Worker进程能够处理的最大并发连接数:worker_connections number;当空闲连接数不足总数的八分之一时,Worker进程会大幅度降低获取accept_mutex锁的概率:ngx_int_tngx_accept_disabled = ngx_cycle->connection_n / 8 - ngx_cycle->free_connection_n; if (ngx_accept_disabled > 0) {  ngx_accept_disabled--; } else {  if (ngx_trylock_accept_mutex(cycle) == NGX_ERROR) {  return; }  ... }我们还可以通过accept_mutex_delay配置控制负载均衡的执行频率,它的默认值是500毫秒,也就是最多500毫秒后,并发连接数较少的Worker进程会尝试处理新连接:accept_mutex_delay 500ms;当然,在1.11.3版本后,Nginx默认关闭了accept_mutex锁,这是因为操作系统提供了reuseport(Linux3.9版本后才提供这一功能)这个更好的解决方案。我们先来看一下处理新连接的性能对比图: 上图中,横轴中的default项开启了accept_mutex锁。可以看到,使用reuseport后,QPS吞吐量有了3倍的提高,同时处理时延有明显的下降,特别是时延的波动(蓝色的标准差线)有大幅度的下降。reuseport能够做到这么高的性能,是因为内核为每个Worker进程都建立了独立的ACCEPT队列,由内核将建立好的连接按负载分发到各队列中,如下图所示: 这样,Worker进程的执行效率就高了很多!如果你想开启reuseport功能,只需要在listen指令后添加reuseport选项即可: 当然,Master/Worker进程架构带来的好处还有热加载与热升级。在https://www.nginx-cn.net/article/70这篇文章中,我对这一流程有详细的介绍。小结最后对本文的内容做个总结。材料、散热这些基础科技没有获得重大突破前,CPU频率很难增长,类似Redis、NodeJS这样的单进程、单线程高并发服务,只能向分布式集群方向发展,才能继续提升性能。Nginx通过Master/Worker多进程架构,可以充分使用服务器上百个CPU核心,实现C10M。 为了榨干多核CPU的价值,Nginx无所不用其极:通过绑定CPU提升二级缓存的命中率,通过静态优先级扩大时间片,通过多种手段均衡Worker进程之间的负载,在独立线程池中隔离阻塞的IO操作,等等。可见,高性能既来自于架构,更来自于细节。 

点赞 7
1 条评论
如何configure定制出属于你的Nginx 陶辉 发表于 : 2020-06-24 07:30

上一篇文章中,我介绍了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脚本的用法。其中对于定制模块的选项,会从模块的分类讲起,带你系统的掌握如何添加Nginx模块。同时,也会介绍configure执行后生成的objs目录,以及Makefile文件的用法。这也是Nginx开源社区基础培训系列课程第一季,即6月11日晚第2次视频直播课前半部分的文字总结。configure脚本有哪些选项?在Linux系统(包括各种类Unix操作系统)下,编译复杂的C语言软件工程是由Makefile文件完成的。你一定注意到Nginx源码中并没有Makefile文件,这是因为Makefile是由configure脚本即时生成的。接下来我们看看,configure脚本悄悄的做了哪些事,这些工作又会对Nginx产生哪些影响。configure脚本支持很多选项,掌握它们就可以灵活的定制Nginx。为了方便理解,从功能上我把它们分为5类:l    改变Nginx编译、运行时各类资源的默认存取路径configure既可以设置Nginx运行时各类资源的默认访问路径,也可以设置编译期生成的临时文件存放路径。比如:n    --error-log-path=定义了运行期出现错误信息时写入error.log日志文件的路径。n    --http-log-path=定义了运行期处理完HTTP请求后,将执行结果写入access.log日志文件的路径。n    --http-client-body-temp-path=定义了运行期Nginx接收客户端的HTTP请求时,存放包体的磁盘路径。n    --http-proxy-temp-path=定义了运行期负载均衡使用的Nginx,临时存放上游返回HTTP包体的磁盘路径。n    --builddir=定义了编译期生成的脚本、源代码、目标文件存放的路径。等等。对于已经编译好的Nginx,可以通过nginx -V命令查看设置的路径。如果没有显式的设置选项,Nginx便会使用默认值,例如官方Nginx将--prefix的默认值设为/usr/local/nginx,而OpenResty的configure脚本则将--prefix的默认值设为/usr/local/openresty/nginx。l    改变编译器选项Nginx由C语言开发,因此默认使用的C编译器,由于C++向前兼容C语言,如果你使用了C++编写的Nginx模块,可以通过--cc-opt等选项,将C编译器修改为C++编译器,这就可以支持C++语言了。Nginx编译时使用的优化选项是-O,如果你觉得这样优化还不够,可以调大优化级别,比如OpenResty就将gcc的优化调整为-O2。当某些模块依赖其他软件库才能实现需求时,也可以通过--with-ld-opt选项链接其他库文件。l    修改编译时依赖的中间件Nginx执行时,会依赖pcre、openssl、zlib等中间件,实现诸如正则表达式解析、TLS/SSL协议处理、解压缩等功能。通常,编译器会自动寻找系统默认路径中的软件库,但当系统中含有多个版本的中间件时,就可以人为地通过路径来指定版本。比如当我们需要使用最新的TLS1.3时,可以下载最新的openssl源码包,再通过--with-openssl=选项指定源码目录,让Makefile使用它去编译Nginx。l    选择编译进Nginx的模块Nginx是由少量的框架代码、大量的C语言模块构成的。当你根据业务需求,需要通过某个模块实现相应的功能时,必须先通过configure脚本将它编译进Nginx(Nginx被设计为按需添加模块的架构),之后你才能在nginx.conf配置文件中启用它们。下一小节我会详细介绍这部分内容。l    其他选项还有些不属于上述4个类别的选项,包括:n    定位问题时,最方便的是通过error.log查看DEBUG级别日志,而打开调试日志的前提,是在configure时加入--with-debug选项。n    HTTP服务是默认打开的,如果你想禁用HTTP或者HTTP缓存服务,可以使用--without-http和--without-http-cache选项。n    大文件读写磁盘时,并不适宜使用正常的read/write系统调用,因为文件内容会写入PageCache磁盘高速缓存。由于PageCache空间有限,而大文件会迅速将可能高频命中缓存的小文件淘汰出PageCache,同时大文件自身又很难享受到缓存的好处。因此,在Linux系统中,可以通过异步IO、直接IO来处理文件。但开启Linux原生异步IO的前提,是在configure时加入--with-file-aio选项。n    开启IPv6功能时,需要加入--with-ipv6选项。n    生产环境中,需要使用master/worker多进程模式运行Nginx。master是权限更高的管理进程,而worker则是处理请求的工作线程,它的权限相对较低。通过--user=和--group=选项可以指定worker进程所属的用户及用户组,当然,你也可以在nginx.conf中通过user和group指令修改它。在大致了解configure提供的选项后,下面我们重点看下如何定制Nginx模块。如何添加Nginx模块?编译Nginx前,我们需要决定添加哪些模块。在定制化模块前,只有分清了模块的类别才能系统的掌握它们。Nginx通常可以分为6类模块,包括:l    核心模块:有限的7个模块定义了Nginx最基本的功能。需要注意,核心模块并不是默认一定编译进Nginx,例如只有在configure时加入--with-http_ssl_module或者--with-stream_ssl_modul选项时,ngx_openssl_module核心模块才会编译进Nginx。l    配置模块:仅包括ngx_conf_module这一个模块,负责解析nginx.conf配置文件。l    事件模块:Nginx采用事件驱动的异步框架来处理网络报文,它支持epoll、poll、select等多种事件驱动方式。目前,epoll是主流的事件驱动方式,选择事件驱动针对的是古董操作系统。l    HTTP模块:作为Web服务器及七层负载均衡,Nginx最复杂的功能都由HTTP模块实现,稍后我们再来看如何定制HTTP模块。l    STREAM模块:负责实现四层负载功能,默认不会编译进Nginx。你可以通过--with-stream启用STREAM模块。l    MAIL模块:Nginx也可以作为邮件服务器的负载均衡,通过--with-mail选项启用。在上图中,HTTP模块又可以再次细分为3类模块:l    请求处理模块:Nginx接收、解析完HTTP报文后,会将请求交给各个HTTP模块处理。比如读取磁盘文件并发送到客户端的静态资源功能,就是由ngx_http_static_module模块实现的。为了方便各模块间协同配合,Nginx将HTTP请求的处理过程分为11个阶段,如下图所示:  l    响应过滤模块:当上面的请求处理模块生成合法的HTTP响应后,将会由各个响应过滤模块依次对HTTP头部、包体做加工处理。比如返回HTML、JS、CSS等文本文件时,若配置了gzip on;指令,就可以添加content-encoding: gzip头部,并使用zlib库压缩包体。l    upstream负载均衡模块:当Nginx作为反向代理连接上游服务时,允许各类upstream模块提供不同的路由策略,比如ngx_http_upstream_hash_module模块提供了哈希路由,而ngx_http_upstream_keepalive_module模块则允许复用TCP连接,降低握手、慢启动等动作提升的网络时延。对于这3类模块,你可以从模块名中识别,比如模块中出现filter和http字样,通常就是过滤模块,比如ngx_http_gzip_filter_module。如果模块中出现upstream和http字样,就是负载均衡模块。我们可以使用--with-模块名,将其编译进Nginx,也可以通--without-模块名,从Nginx中移出该模块。需要注意的是,当你通过configure --help帮助中看到--with-打头的选项,都是默认不编译进Nginx的模块,反之,--without-打头的选项,则是默认就编译进Nginx的模块。还有一些HTTP模块并不属于上述3个类别,比如--with-http_v2_module是加入支持HTTP2协议的模块。当你需要添加第三方模块时,则可以通过--add-module=或者--add-dynamic-module=(动态模块将在后续文章中再介绍)选项指定模块源码目录,这样就可以将它编译进Nginx。如何安装并运行Nginx?当configure脚本根据指定的选项执行时,会自动检测体系架构、系统特性、编译器、依赖软件等环境信息,并基于它们生成编译Nginx工程的Makefile文件。同时,还会生成objs目录,我们先来看看objs目录中有些什么:objs  |-- autoconf.err#configure自动检测环境时的执行纪录  |-- Makefile#编译C代码用到的脚本  |-- ngx_auto_config.h#以宏的方式,存放configure指定的配置,供安装时使用  |-- ngx_auto_headers.h#存放编译时包含的头文件中默认生成的宏  |-- ngx_modules.c#根据configure时加入的模块,生成ngx_modules数组  `-- src#存放编译时的目标文件  |-- core#存放核心模块及框架代码生成的目标文件  |-- event#存放事件模块生成的目标文件  |-- http#存放HTTP模块生成的目标文件  |-- mail#存放MAIL模块生成的目标文件  |-- misc#存放ngx_google_perftools_module模块生成的目标文件  |-- os#存放与操作系统关联的源代码生成的目标文件  `-- stream#存放STREAM模块生成的目标文件接着,执行make命令就可以基于Makefile文件编译Nginx了。make命令可以携带4种参数:l    build:编译Nginx,这也是make不携带参数时的默认动作。它会在objs目录中生成可执行的二进制文件nginx。l    clean:通过删除Makefile文件和objs目录,将configure、make的执行结果清除,方便重新编译。l    install:将Nginx安装到configure时指定的路径中,注意install只针对从头安装Nginx,如果是升级正在运行的服务,请使用upgrade参数。l    upgrade:替换可执行文件nginx,同时热升级运行中的Nginx进程。因此,当我们首次安装Nginx时,只需要先执行make命令编译出可执行文件,再执行make install安装到目标路径即可。启动Nginx也很简单,进入Nginx目录后(比如/usr/local/nginx),在sbin目录下执行nginx程序即可,Nginx默认会启用Daemon守护者模式(参见daemon on;指令),这样shell命令行不会被nginx程序阻塞。至此,Nginx已经编译、安装完成,并成功运行。小结最后做个小结,本文介绍了定制化编译、安装及运行Nginx的方法。如果你想定制符合自己业务特点的Nginx,那就必须学会使用configure脚本,它会根据输入选项、系统环境生成差异化的编译环境,最终编译出功能、性能都不一样的Nginx。configure支持的选项分为5类,它允许用户修改资源路径、编译参数、依赖软件等,最重要的是可以选择加入哪些官方及第三方模块。定制模块前,先要掌握模块的类别。Nginx模块分为6类,作为Web服务器使用时,其中最复杂、强大的自然就是HTTP模块,它又可以再次细分为3小类:请求处理模块、响应过滤模块、负载均衡模块。我们可以使用--with或者--without选项增删官方模块,也可以通过--add-module或者--add-dynamic-module添加第三方模块。configure会生成源代码、脚本、存放目标文件的临时目录,以及编译C工程的Makefile文件。其中,Makefile支持4个选项,允许我们编译、安装、升级Nginx。由于Nginx支持Daemon模式,启动它时直接运行程序即可。下一篇将会介绍nginx.conf的配置语法,以及使用命令行或者免费的可视化工具分析access.log日志文件的方法。 

点赞 7
0 条评论
10分钟快速认识Nginx 陶辉 发表于 : 2020-06-08 21:27

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脚本的用法,配置文件的语法格式,以及如何配置出静态资源服务。 

点赞 8
3 条评论
《Nginx核心知识100讲》课件 陶辉 发表于 : 2020-06-03 14:01

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 

点赞 9
8 条评论
《深入理解NGINX:模块开发与架构解析》示例代码下载 陶辉 发表于 : 2020-05-20 09:11

第三章示例源代码:chapter3第四章示例源代码:chapter4第五章示例源代码:chapter5第六章示例源代码:chapter6

点赞 7
1 条评论
TA的热门
在实时性要求较高的特殊场景下,简单的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层上重新实现这套机制就得不偿失了。 
本文是我对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 
分布式系统提升可用性时,最有效的方案就是在空间维度上,将资源复制一份作为缓存,并把缓存放在离用户更近的地方。这样,通过缩短用户的访问路径,不只可以降低请求的时延,多份资源还能提升系统的健壮性。比如WEB服务中的CDN就是这样一个缓存系统。 Nginx由于具有下面3个特性,因此是最合适的缓存系统:l  首先,高并发、低延迟赋予了Nginx优秀的性能;l  其次,多进程架构让Nginx具备了很高的稳定性;l  最后,模块化的开源生态,以及从开放中诞生的Openresty、Kong等其他体系,这都让Nginx的功能丰富而强大。 所以,Nginx往往部署在企业最核心的边缘位置,在最外层的Nginx上部署共享缓存,能够给服务带来更大的收益,更短的访问路径带来了更佳的用户体验。然而,当整个系统的可用性极度依赖Nginx的缓存功能时,我们必须仔细地配置Nginx,还得使用到缓存的许多进阶功能。 比如,在超大流量下如果热点资源的缓存失效,那么在巨大的流量穿透Nginx缓存后,非常有可能把脆弱的上游服务打挂。此时合并回源请求功能,就是你的最佳应对手段!再比如,在源服务器暂时不可用时,使用失效过期的缓存,为用户提供有限的服务,可以通过降级体验来提升系统的健壮性,这要比给用户返回“系统暂时不可用”要好得多。再比如,Nginx重启后,需要为磁盘上大量的缓存文件,在共享内存中建立起索引,这一过程可能很漫长,我们必须防止它对正常的服务产生过大的影响,降低用户体验。 如果你不清楚Nginx缓存的这些用法,就无法在超大流量场景中,用Nginx缓存保障服务的高可用性。当然,你可能觉得加机器扩容简单粗暴又有效,可是,一旦Nginx缓存正常工作后,这些新增的机器利用率又变得极低,这提升了公司的IT成本。因此,Nginx缓存的这些进阶用法,是你必须掌握的知识点! 这篇文章将分为上、下两个部分,先带着你回顾下Nginx缓存的基本用法,再来看如何使用缓存的进阶功能。 缓存的基本工作流程时间局部性原理,决定了缓存的有效性。比如,当客户端首次获取到/url1对应的资源时,它可以用key/value的形式,将请求与响应同时缓存到磁盘上,其中,key就是/url1,而value就是响应中的资源。只要服务器没有更改/url1对应的资源内容,那么在时间维度上,后续访问/url1的请求,就一直可以使用磁盘上的缓存代替网络访问来提升性能,如下图所示:通过缓存,请求的时延得到极大的下降,因为访问磁盘不过几十毫秒,但经过以秒级计量的网络再到达源服务器,并由源服务器再读取磁盘,速度就慢了很多,尤其在网络是不稳定、不可控的时候。 然而,服务器上的资源一旦发生了更新,客户端的缓存就失效了,使用这样的缓存有可能导致业务逻辑出错,怎么解决这一问题呢?通常,由于服务器最清楚资源的变更频率,这样,在服务器发送的响应中,设置一个预估缓存失效时间的HTTP头部,就可以解决这一问题。就像Cache-Control: max-age头部,或者Expires头部,等等。 客户端基于服务器告知的失效时间设立定时器,当定时器归零触发时,再将缓存设置为失效即可。注意,失效的缓存仍然有多方面的价值。 首先,客户端的失效定时器,毕竟来源于服务器的预估时间,时间过期后服务器上的资源也可能并未发生变化。这样,如果我们给每份资源生成唯一的ID(也就是缓存摘要,RFC规范用etag头部表示摘要),而客户端在判定缓存失效后,能够携带着摘要访问服务器,这就允许服务器通过304 Not Modified空包体回复客户端。如果资源是一个数百MB的视频文件,这一下就省去了数百MB字节的网络传输,如下图所示: Etag摘要究竟是怎样生成的呢?Nginx会将文件大小和最近修改时间,拼接为一个字符串作为文件摘要(详见《Nginx核心知识100讲》第97课),虽然区分度稍差,但优点是生成速度非常快。 其次,在某些场景下,给用户提供不那么及时的过期页面,要比返回500 服务器内部错误好得多。Nginx的proxy_cache_use_stale指令就可以完成这一功能,我们下一篇再细说。 缓存可以存放在任何位置!其中,仅存放于终端、只为一个用户服务的缓存,叫做私有缓存(Private Cache),而存放于服务器上,可以被多个用户共享使用的缓存,叫做共享缓存(Public Cache)。比如下图的REST架构中,浏览器User Agent中的$符号($表示现金cash,与cache发音很接近,故常用来表示缓存)表示私有缓存,而Proxy正向代理、Gateway反向代理中的$符号表示共享缓存: Nginx上的缓存就是共享缓存,接下来我们看看如何配置共享缓存。 如何开启基础版的共享缓存功能?接下来,我将按照缓存的工作流程,介绍配置基础共享缓存的Nginx指令。 首先,我们要确定每个资源对应的key关键字。这里的原则是,既要确保能够区分开不同的资源(正确性),又要尽量被更多的同类用户共享使用(高效率)。 比如,若多个域名下不同的资源,使用了相同的URL,那么你仅用URL作为key,一定会导致缓存被错误地共享使用。再比如,如果你把客户端的IP地址也写进了key中,那么不同的客户端就无法共享缓存,导致缓存的利用率大幅下降。再比如,若你的服务器同时提供HTTP和 HTTPS服务,那么你将$scheme写入key中,就会导致HTTP和HTTPS不能使用对方产生的缓存。 在Nginx的HTTP反向代理中,可以使用proxy_cache_key指令配置缓存的key关键字,其中值可以使用变量,比如:proxy_cache_key $proxy_host$request_uri;其他反向代理也有类似的配置(比如python语言的ngx_http_uwsgi_module反向代理模块中,使用相似的uwsgi_cache_key指令完成这一功能)。 其次,我们要决定缓存对应的文件放置在磁盘中的哪个位置,这可以通过proxy_cache_path指令完成,例如:proxy_cache_path /data/nginx/cache levels=2:2 keys_zone=one:10m;这行指令要求Nginx把缓存文件放在/data/nginx/cache目录下。注意,1个目录中不能存放太多的文件(会影响性能),所以levels选项要求Nginx使用2级缓存子目录,其中每级目录用文件的MD5摘要中2个字符命名。 虽然缓存内容是存放在磁盘中的,但为了加快访问速度,Nginx会在共享内存中为缓存建立红黑树索引,这可以为多个worker进程加快处理速度。keys_zone选项定义了共享内存的大小和名称,其中每1MB的共享内存,大约可以存放8千个key关键字。 定义好了缓存及索引的存放方式后,还需要定义究竟缓存哪些资源。比如,404错误码表示找不到资源,302错误码表示临时重定向,这样的响应是否需要缓存呢?这就是具体情况具体分析,如果访问频率非常高,而且这些错误一时无法恢复,那么缓存它们也是个不错的选择。 通过proxy_cache_valid指令可以设置缓存哪些响应码,并可以设置缓存的有效时间,如下所示:proxy_cache_valid 200 302 10m;你可能在想,刚刚才说过源服务器会定义缓存的过期时间,为何代理服务器Nginx要多此一举,重新定义过期时间呢?这是因为Nginx的这个过期时间,是用于兜底的,它的优先级很低,仅针对不含有这些过期时间头部的响应。当上游源服务器的响应中含有过期时间时,会优先使用响应中的时间。 而且,响应头部中同时出现多个过期时间,Nginx还定义了不同的优先级,比如:Nginx私有的X-Accel-Expires头部优先级最高,其次才是RFC规范中定义的Expires和Cache-Control头部。另外,对于含有Set-Cookie头部的响应,出于安全性考虑,Nginx默认是不会缓存的,包括Vary: *头部也是如此,如果你希望缓存它们,那么可以通过proxy_ignore_headers指令实现。 这里还要注意,proxy_cache_valid指令中的时间是必填项,当只填写了时间时,proxy_cache_valid默认会缓存以下3个错误码的响应:200、301、302。 在定义好了缓存的处理方式后,最后还得通过proxy_cache指令,告诉Nginx针对哪些域名(在server{ }内配置)、URL(在location{ }内配置)才缓存请求。proxy_cache指令非常简单,指定proxy_cache指令中keys_zone选项的共享内存名即可,比如:proxy_cache one;至此,我们就配置好了可以基本工作的Nginx缓存。虽然此时的缓存已经能够大幅度提升性能,但Nginx在缓存失效后,还无法在回源请求中携带etag缓存摘要,自然也无法享受到源服务器返回304空包体响应带来的好处了。你还需要通过proxy_cache_revalidate指令打开这一功能,如下所示:proxy_cache_revalidate on;小结本文介绍了Nginx缓存的工作流程,以及如何配置一个基础版的共享缓存。 首先,Nginx作为代理服务器,它提供的是效率更高的共享缓存。因此,我们要通过proxy_cache_key指令,通过Nginx变量选定合适的缓存关键字,既要保证在功能上有正确的区分度,也要在效率上可以为同类用户共享使用。 其次,缓存内容存放在磁盘文件中,而为了加快处理速度,Nginx还在共享内存中建立了红黑树索引。通过proxy_pass指令,可以设定磁盘文件的存储方式,以及共享内存的名称及大小。 最后,服务器会通过响应头部,预估缓存的过期时间。而在Nginx中,proxy_cache_valid既可以设置通用的、优先级较低的过期时间,也可以缓存非200错误码的响应。当然,缓存过期后,打开proxy_cache_revalidate功能还可以减少源服务器的数据传输量。 篇幅所限,我将在下一篇文章中,再来讨论Nginx缓存的进阶功能。比如,如何设置缓存的最大容量,如何在Nginx重启后控制cache loader进程对CPU资源的消耗,如何控制回源请求的数量,如何使用过期缓存提供降级服务等等。 最后,留给你一道思考题,你知道Nginx的性能非常高,那么它在实现层面是怎样淘汰过期缓存的吗?欢迎你在留言中与大家一起分享你的看法。 感谢阅读,如果你觉得这篇文章对你有一些启发,也欢迎你把它分享给你的朋友。 
上一篇文章介绍了HTTP请求匹配server{ }配置块的过程,接着请求会继续匹配location{ }配置块,并最终决定哪些指令及Nginx模块处理请求。本文将介绍location的匹配规则,以及rewrite指令与location匹配顺序的关系。 生产环境中的nginx.conf往往含有上百条location,这是因为Nginx常常身兼多职:充当提供静态资源CDN、作为负载均衡为分布式集群提供扩展性、作为API gateway提供接口服务等等。location一旦配置错误,Nginx上巨大的并发连接数会将错误放大上万倍,很容易导致严重的线上事故。 而location也很容易配置错误,它既支持前缀匹配,也支持正则表达式匹配,当二者同时出现时,为了获得更高的性能,Nginx设计了复杂的location匹配优先级。这是因为前缀匹配是对静态的location多叉树检索完成的,它的性能要比正则表达式高得多,唯有搞清楚具体的匹配流程,我们才能设计出匹配速度更快的location。 而且rewrite指令修改URL的功能也让location匹配变得更为复杂。特别是rewrite出现在server{ }和location{ } 里,会导致完全不同的结果。设计location时,我们还需要考虑到rewrite的效率,以及它是否会导致循环重定向。 这篇文章将从底层讲清楚URL匹配location { }配置块的流程,以及rewrite指令修改URL后,Nginx又是怎样重新匹配location的。 如何匹配前缀location?location { }中定义了哪些Nginx模块会处理以及如何处理HTTP请求,因此,URL与location的匹配关系到功能的正确性,它是学好Nginx的必要条件。 location有两类匹配URL的方式,一类是前缀匹配,一类是正则表达式匹配。我们先来看前缀匹配。 URL通过/正斜杠符号分隔对象,因此URL从前至后具有天然的层级关系。比如,/wp-content/uploads/2019/07/test.jpg就具备以下意义:第1级wp-content说明它属于wordpress的内容,第2级uploads说明这是用户自行上传的文件,第3、4级2019/07描述了它的上传日期,第5级则是文件名称及格式。所以,从前至后进行前缀匹配最自然不过,像location /wp-content/uploads { } 就可以匹配wordpress中所有用户上传的文件。 当请求同时匹配上多个location时,Nginx会选择前缀最长的location { }处理请求。比如,location /wp-content/uploads { }和location /wp-content/uploads/2019 { }同时存在时,/wp-content/uploads/2019/07/test.jpg请求只会命中后者。最长前缀匹配,是location匹配的核心原则。 由于许多location处于包含关系,因此很容易出现重复匹配,那么,当数百个前缀location同时配置时,Nginx怎样基于最长前缀原则,最有效率的关联URL呢?事实上,Nginx会在启动过程中,将server{ }内的所有location基于前缀的包含关系,建立一颗多叉树。 比如,如下12个location将会构造出1颗4层的静态树,其中子树中的所有location,都是比父结节更长的前缀location;在同一层的结点中,它们互不相属,但却是基于字母表有序的(注意,同级location的排序与长度无关):        location /test {root html;}        location /res {root html/res;}        location /res/img {root html/res/img;}        location /res/video {root html/res/video;}        location / {root html/res;}        location /resource/js {root html/res;}        location /resource/image {root html/res;}        location /his {root html/res;}        location /his/20 {root html/res;}        location /his/2020 {root html/res;}        location /his/20/02 {root html/res;}        location = /50x.html {} 举个例子,location = /50x.html和location /res都是/结点的子结点,因此它们处于树的第2层。且因为首字母5的ASCII码比r要小,因此50x.html是res的左兄弟结点。为了提高检索效率,Nginx会在构造树的过程中,取每一层兄弟结点中间的那一个,作为父结点的直接子结点。就像50x.html、his、res、test四个结点并存时,res将作为/的直接子结点,这能够减少检索的时间复杂度。 我们以一个具体的例子来看下location树的匹配流程。比如/his/2001/test.jpg请求到达时,它的匹配顺序如下图蓝色箭头所示:  事实上,/his/2001/test.jpg请求的匹配共包含6步:1.      请求首先命中/,暂时/将被设置为最长前缀,再进入子树看看有没有更长的前缀;2.      未匹配上直接子结点res,由于h在字母表的顺序小于r,因此到左兄弟结点his中继续匹配;3.      匹配上his后,此时/his被设置为最长前缀;4.      匹配上直接子树/his/20,将其设为最长前缀,仍然进入子树尝试更长的前缀匹配;5.      未匹配上直接子树20,由于1在字母表的顺序中小于2,因此到左兄弟结点中去看看;6.      /20未匹配命中,且在字母表中/先于1,匹配到此结束。这时,最长匹配是/his/20,于是使用此location处理请求/his/2001/test.jpg。 这样我们搞清楚了最长前缀匹配的底层逻辑,接下来再来看正则表达式location的用法。 如何匹配正则表达式location? 当遇到前缀匹配无法覆盖的URL时,可以使用正则表达式匹配请求。当然,与上一篇介绍过的server_name类似,使用正则表达式的前提是将pcre开发库编译进Nginx。一次写对正则表达式很难,在Linux下我建议你用pcretest命令行工具提前测试正则表达式。关于正则表达式和pcretest工具的用法,你可以观看下我在极客时间上的视频课程《Nginx核心知识100讲》第46课《Nginx中的正则表达式》。 在location中使用正则表达式,只需要在表达式前加入~或者~*符号,其中前者表示字母大小写敏感,而后者对大小写不敏感,例如:location ~* *\.(gif|jpg|png|webp|)$ 它可以匹配各类图片,且忽略文件格式后缀的大小写。 多个正则表达式location之间的匹配次序很简单,按照它们在server{ }块中出现的位置,依次匹配,直接使用最先命中的location即可。所以使用正则表达式要小心,当上方的正则表达式匹配范围过大时,下方的正则表达式location可能永远也无法命中。 当正则表达式与前缀location同时出现时,事情就变得复杂起来。我们前面介绍过,前缀location构成的多叉树匹配效率很高,而正则表达式的匹配要慢得多。因此,Nginx会优先进行前缀location匹配,再进行正则表达式location的匹配,而且Nginx额外给前缀location提供了2个跳过正则表达式匹配的武器:=和^~。 在执行前缀匹配时,如果URL与location完全相等,那么Nginx不会再检索子树寻找更长的前缀匹配,但还会执行正则表达式匹配。如果你希望URL完全相等后,不必再匹配正则表达式location,那么可以在location前增加=号。比如,当location = / {}与location / {}同时出现时,前者是为了匹配访问首页的请求,而后者可以匹配任何请求,常用来兜底。因此,如果某些页面访问频率非常高,你应该用=号加快location的匹配速度。 另外,^~也可以跳过正则表达式匹配阶段,加快location的执行速度,而且它比=号的应用范围更广,^~不需要URL完全相等,只需要匹配上前缀即可跳过后续的正则表达式。注意,只有最长匹配上携带^~符号,才能够跳过正则表达式。比如,你觉得/res/blog/js/1.js访问下面3个location时会获得什么响应?        location ^~ /res/blog {return 200 ‘res blog‘;}        location /res/blog/js {return 200 ‘res blog js‘;}        location ~* .*\.js {return 200 ‘js‘;}  答案是’js’!虽然这个请求同时命中了3个location,但2个前缀location中,/res/blog虽然带有^~符号,可惜它却不是最长的前缀匹配;而/res/blog/js虽然是最长前缀,但又不能阻止正则表达式;最终第3个location ~* .*\.js匹配上了URL!  简单的总结下location匹配规则(见下图):1.      先对前缀location执行最长前缀匹配2.      若最长前缀location前,携带有=或者^~,那么使用此location配置块处理请求;3.      按server{ }中正则表达式的出现顺序,依次匹配。成功后就选中此location;4.      若所有正则表达式皆未匹配上,则使用第1步中检索出的最长前缀location处理请求。 你可能会问,如果第1步中就没有找到能匹配上的前缀location,那该怎么办?很简单,Nginx会直接返回404。当然,为了避免这种情况发生,通常我们都会添加location / { }兜底,它可以匹配任意URL。 注意:location中的正则表达式,就像server_name中一样,可以用小括号()提取变量,供后续其他Nginx模块的指令使用。 配置location时,还有一个技巧需要你掌握:由于客户端的URL中可能含有重复的正斜杠/,因此Nginx会自动合并连续的重复正斜杠/。比如,//res/blog///a.js会被合并成/res/blog/a.js。如果你想关闭这一功能,可以添加下面这行配置:merge_slashes off;  由于location的匹配规则相当复杂,所以Nginx会在debug级别的日志中,打印出最终选中了哪个location。比如:test location: "/"test location: "res"test location: "/blog"using configuration "/res/blog"  其中,using configuration指明了最终选择了哪个location。当然,要想开启debug日志,除了在nginx.conf里将error_log的日志级别设为debug外,还需要在configure时加入了---with-debug选项。 rewrite指令是如何工作的 虽然我们已经清楚了location的匹配规则,但是,匹配的URL未必是客户端的原始URL,因为rewrite指令可以修改URL!因此,我们还需要了解rewrite指令的用法,这样才能全面掌握location的匹配规则。 当系统升级、维护或者数据迁移时,往往需要重写URL后,再执行location匹配。rewrite指令就是用来重写URL的,它的用法非常简单,比如下面这行指令就可以将/reg1/a.js修改为/reg2/a.js:rewrite /reg1/(.*) /reg2/$1;  显然,rewrite可以反复地修改URL,并导致location被反复匹配命中。因此,为了防止不当的rewrite指令导致死循环,Nginx在代码层面将1个请求的rewrite次数限制为10次,超过后会直接返回500错误码:#define NGX_HTTP_MAX_URI_CHANGES           10  rewrite指令既可以直接出现在server{ }块中,也可以出现在location { }块中,但它们的工作流程却完全不同!比如,你觉得下面的rewrite会导致请求/reg1/a.js无限循环吗?        rewrite /reg1/(.*) /reg2/$1;        location /reg2 {rewrite /reg2/(.*) /reg1/$1;} 其实不会,因为server{ }中的rewrite指令只会执行1次。要说清楚rewrite、location的执行时机,我们得先清楚HTTP请求的11个执行阶段。 当Nginx接收完HTTP头部后,会让各Nginx模块基于Pipe And Filter模型依次处理请求。其中,为了让模块的处理次序更加可控,Nginx基于Web语义将其分为11个阶段,每个Nginx模块通常会选择1个阶段介入请求的处理流程。rewrite与location涉及到其中的4个阶段,下面看看它们究竟做了些什么: 我们依次分析这4个阶段:1.      server{ }块中的rewrite指令,将在NGX_HTTP_SERVER_REWRITE_PHASE阶段执行。从图中可以看到,它只会执行1次;2.      前2节介绍的location匹配流程,就发生在NGX_HTTP_FIND_CONFIG_PHASE阶段;3.      location{ }块中的rewrite指令,在NGX_HTTP_REWRITE_PHASE阶段执行;4.      NGX_HTTP_POST_REWRITE_PHASE阶段中,判断location中的rewrite指令是否重写了URL,如果是,那么跳转到NGX_HTTP_FIND_CONFIG_PHASE阶段再做1次location匹配,否则继续向下,由其他Nginx模块处理请求。 因此,不同于server{ }块,location中的rewrite指令是可能反复执行多次的。 其实,rewrite指令还可以携带4种不同的flag参数,它还将影响if、set等其他脚本类指令的执行。本文聚焦于location的匹配,后续我在脚本指令的介绍文章中,还会讲到rewrite指令的其他用法。 小结 本文介绍了HTTP请求匹配location的流程。 location支持URL按最长前缀进行location匹配。Nginx启动时会将所有前缀location构造出一颗静态的多叉树,其中子树中的结点都是父结点的更长前缀,而兄弟结点间则按字母表排序。这样,前缀URL的匹配效率就很高。 相比起来,正则表达式则按照在nginx.conf中的出现顺序进行匹配,效率要低得多。当二者同时出现时,虽然正则表达式优先级更高,但=号和^~号可以让前缀location跳过正则表达式匹配,提升性能。然而这样让location匹配更容易出错,如果你在开发环境中,可以借助debug级别的error.log日志,通过using configuration确认Nginx究竟选择了什么location来处理请求。 rewrite指令可以反复修改URL,其中server{ }块中的rewrite指令只会执行1次,而location中的rewrite则可能最多执行10次,超出后Nginx会返回500错误码。只有理解了11个HTTP阶段的执行顺序,才能掌握rewrite与location的匹配关系。 你可能知道,location { }配置块内可以嵌套location { },虽然这不是一种推荐的配置方式,但它确实是被语法规则支持的。那么,在嵌套发生时,基于本文的理论,location是如何匹配的?rewrite指令又是怎样工作的?欢迎你在帖子下方留言,与我一起探讨更好的热部署实现方案。