陶辉 《深入理解Nginx:模块开发与架构解析》作者 极客时间《Nginx核心知识100讲》讲师
浏览 1.11 W+
文章 15
订阅 136
Nginx怎样隐藏上游错误? 陶辉 发表于 : 2021-02-23 10:04

当上游出错时,作为负载均衡的Nginx可以实时更换Server,在客户端无感知的情况下重新转发HTTP请求。这一功能在Nginx指令中称为next upstream,本文将详细介绍其用法及实现原理。在OSI网络模型中,传输层的TCP协议通过内核提供的系统调用向Nginx反馈错误,表示层的TLS/SSL协议通过openssl库向Nginx返回错误,而应用层的HTTP协议(或者uwsgi、gRPC、CGI、memcached等协议)通过Response的Decode解码流程返回错误。当Nginx能够通过重试解决这些错误时,我们可以使用next upstream机制对客户端隐藏个别上游Server由于宕机、网络异常产生的错误,这可以极大的提升整个分布式系统的可用性。如果我们不清楚它处理协议错误及重试转发的原理,就很容易在实际场景中发现nextupstream没有发挥作用,比如: l

点赞 2
0 条评论
深入剖析Nginx负载均衡算法 陶辉 发表于 : 2021-02-16 21:02

负载均衡是Nginx的核心应用场景,本文将介绍官方提供的5种负载均衡算法及其实现细节。Nginx提供的Scalability,主要由复制扩展(AKF X轴)和用户数据扩展(AKF Z轴)组成。所谓复制扩展,是指上游Server进程是完全相同的,因此可以采用最少连接数、Round Robin轮询、随机选择等算法来分发流量。所谓用户数据扩展,是指每个上游Server只处理特定用户的请求,对这种场景Nginx提供了支持权重的哈希算法,以及支持虚拟节点的一致性哈希算法。当上游集群规模巨大时,我们必须了解这些算法的细节,才能有效地均衡负载。比如,当上游server出错时,Weight权重会动态调整吗?调整策略又是什么?如果算法选出的上游server达到了max_fails限制的失败次数,或者max_conns限制的最大并发连接数,那么又该如何重新选择新路由呢?再比如,为了减少宕机、扩容时受影响的Key规模,同时让CRC32哈希值分布更均衡,Nginx为每个Weight权重配置了160个虚拟节点,为什么是这个数字?一致性哈希算法执行的时间复杂度又是多少呢?这一讲我将深入分析Nginx的负载均衡算法,同时围绕ngx_http_upstream_rr_peer_s这个核心数据结构,探讨这些HTTP负载均衡模块到底是怎样工作的。同时,本文也是Nginx开源社区基础培训系列课程第二季,即7月16日晚第2次直播课的文字总结。RoundRobin权重的实现算法在Nginx中,上游服务可以通过server指令声明其IP地址或者域名,并通过upstream块指令定义为一组。这一组server中,将使用同一种负载均衡算法,从请求信息(比如HTTP Header或者IP Header)或者上游服务状态(比如TCP并发连接数)中计算出请求的路由: 如果上游服务器是异构的,例如上图中server 1、3、4、5都是2核4G的服务器,而server 2则是8核16G,那么既可以在server 2上部署多个不同的服务,并把它配置到多个usptream组中,也可以通过server指令后的weight选项,设置算法权重:upstream rrBackend {  server localhost:8001 weight=1;  server localhost:8002 weight=2;  server localhost:8003 weight=3; }  location /rr {  proxy_pass http://rrBackend; } 在上面这段配置指令中,并没有显式指定负载均衡算法,此时将使用Nginx框架唯一自带的RoundRobin轮询算法。顾名思义,它将按照server在upstream配置块中的位置,有序访问上游服务。需要注意,加入weight权重后,Nginx并不会依照字面次序访问上游服务。仍然以上述配置为例,你可能认为Nginx应当这么访问:8001、8002、8002、8003、8003、8003(我在本机上启动了这3个HTTP端口,充当上游server,这样验证成本更低),但事实上,Nginx却是按照这个顺序访问的:8003、8002、8003、8001、8002、8003,为什么会这样呢?实际上这是为了动态权重的实现而设计的。我们先从实现层面谈起。Nginx为每个server设置了2个访问状态:struct ngx_http_upstream_rr_peer_s {  ...    ngx_int_tcurrent_weight;  ngx_int_t effective_weight;  ... }; 其中current_weight初始化为0,而effective_weight就是动态权重,它被初始化为server指令后的weight权重。我们先忽略转发失败的场景,此时RoundRobin算法可以简化为4步:1.

点赞 2
0 条评论
深入剖析HTTP3协议 陶辉 发表于 : 2021-02-04 15:04

自2017年起HTTP3协议已发布了29个Draft,推出在即,Chrome、Nginx等软件都在跟进实现最新的草案。本文将介绍HTTP3协议规范、应用场景及实现原理。2015年HTTP2协议正式推出后,已经有接近一半的互联网站点在使用它: (图片来自https://w3techs.com/technologies/details/ce-http2)HTTP2协议虽然大幅提升了HTTP/1.1的性能,然而,基于TCP实现的HTTP2遗留下3个问题:l

点赞 1
0 条评论
从通用规则中学习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配置文件。 

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

谈到Redis缓存,我们描述其性能时会这么说:支持1万并发连接,几万QPS。而我们描述Nginx的高性能时,则会宣示:支持C10M(1千万并发连接),百万级QPS。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操作,等等。可见,高性能既来自于架构,更来自于细节。 

点赞 8
3 条评论
如何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
1 条评论
TA的热门
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脚本的用法,配置文件的语法格式,以及如何配置出静态资源服务。 
Nginx是企业内网的对外入口,它常常同时对接许多应用,因此,Nginx上会同时监听多个端口、为多个域名提供服务。然而,匹配多级域名并不简单,Nginx为此准备了字符串精确匹配、前缀通配符、后缀通配符、正则表达式,当它们同时出现时,弄清楚HTTP请求会被哪个server{ }下的指令处理,就成了一件困难的事。这是因为基于域名规范,请求匹配server{ }配置块时,并不会按照它们在nginx.conf文件中的出现顺序作为选择依据。而且对于不支持Host头部、没有域名的HTTP/1.0请求和无法匹配到合适server{ }的异常请求,我们都要区别对待。另外,为了加快匹配速度,Nginx将字符串域名、前缀通配符、后缀通配符都放在了哈希表中,该设计充分使用了CPU的批量载入主存功能。如果不了解这些流程,既有可能导致请求没有被正确的server{ }块处理,也有可能降低了原本非常高效地哈希表查询性能。本文将沿着Nginx处理HTTP请求的流程,介绍一个请求是如何根据listen、server_name等配置关联到server{ }块的。我们将从TCP连接的建立、Nginx从哪些字段取出域名、域名是怎样与server_name匹配的,讲清楚Nginx如何为请求找到处理它的server{ }块。在实际运维中,大部分问题都是由于请求匹配指令错误造成的,搞清楚这一匹配流程,对我们掌握Nginx非常重要。listen指令对server{ }块的第1次关联为了让一台服务器可以处理访问多个域名的不同请求,我们用“虚拟主机”来定义一种域名的处理方式,在Nginx中这对应着一个server{ }块。因此,HTTP请求到达时,Nginx首先要找到处理它的server{ }配置块。请求关联server{ }块时主要依据listen和server_name这两个指令,其中listen指令发生在TCP连接建立完成时,它对server{ }块进行首次匹配,等到接收HTTP请求头部时,server_name再进行第二次匹配,这样就可以决定请求由哪个server{ }块中的指令处理。我们先来看listen指令是如何匹配请求的。Nginx启动时创建socket并监听listen指令告知的端口(包括绑定IP地址)。当运行在TCP协议之上的HTTP请求到达服务器时,操作系统首先收到了TCP三次握手请求。我们知道,TCP这种传输层协议是由内核实现的,因此,由内核完成TCP的三次握手后,就会通过“读事件”经由Linux的Epoll通知到Nginx的worker进程以及具体监听的socket。 比如,我们在nginx.conf中配置了以下两个server:server {    listen 192.168.1.5:80; }server {    listen 127.0.0.1:80; }如果是本机进程发来的HTTP请求(在Linux中可以用curl或者telnet发起请求),它的IP报文头部目的IP地址就是127.0.0.1,而TCP报文头部的目的端口就是80。这样,Linux内核就找到了相应的socket,进而通过epoll_wait函数唤醒Nginx进程,而Nginx也就找到了对应的listen指令以及其所属的server{ }块。版本号头部长度服务类型总长度标识标志位分片偏移TTL生存时间上层协议首部校验和源IP地址127.0.0.1源端口号80序列号确认序列号首部长度保留位UAPRSF窗口大小校验和紧急指针 你可能注意到,有些server{ }块没有listen指令也可以正常的工作。这是因为Nginx认为每个server{}都应该监听TCP端口,当你没有显式的配置listen指令时,Nginx会默认帮你打开80端口。 Nginx是怎样从HTTP请求中取出域名的?Nginx允许多个server{ }块监听相同的端口,所以当访问相同端口、不同域名的请求到达时,还需要根据请求中的域名做第2次匹配,以决定最终关联的server{ }块。这里我们先要搞清楚域名是怎么从HTTP请求中取出来的。在HTTP/1.0协议中并没有Host头部,这是因为互联网起步时,HTTP的设计者并没有考虑到域名的数量会远多于服务器。对于HTTP/1.0请求而言,只能从absolute URL中携带域名。举个例子,下面这个没有携带Host头部的请求可以取到www.taohui.pub域名:GET http://www.taohui.pub/index.html HTTP/1.0如果你不清楚HTTP协议的格式,建议你先观看下我在极客时间上的视频课程《Web协议详解与抓包实战》第12课《详解HTTP的请求行》。互联网业务的推动导致一台服务器必须要处理大量域名,于是HTTP/1.1协议推出了描述访问域名的Host头部。对于不含有Host头部的HTTP/1.1请求,RFC规范要求服务器必须返回400错误码(Nginx也正是这么做的)。当Host头部与上述absolute URL中的域名同时出现时,将会以后者为准。例如对于下面这个请求,Nginx会取出www.taohui.tech作为匹配域名:GET http://www.taohui.tech/index.html HTTP/1.0Host: www.taohui.pub另外,对于使用了TLS/SSL协议的HTTPS请求来说,还可以从TLS握手中获取到域名。关于TLS握手及相关插件我会在后续的文章中再详述。获取到请求的域名后,Nginx就会将其与上一节中listen指令匹配成功的server块进行第2次匹配,其中匹配依据就是server_name指令后的选项。我们暂且不谈server_name指令的匹配语法,先来看server_name匹配完成后的3种可能情况:1.      域名恰好与1个server{ }块中的server_name相匹配,选用该server{ }中的指令处理与请求;2.      有多个server{ }块匹配上了域名,此时按server_name规定的优先级选中一个server{ }块即可;3.      所有server{ }块都没有匹配上域名,此时必须有一个默认server { }块来处理这个请求。 其中在第3种情况里,Nginx是这么定义默认server { }的:1.      当listen指令后明确的跟着default_server选项时,它所属的server{ }就是默认server。2.      如果监听同一个端口的所有server{ }都没有通过listen指令显式设置default_server,那么这些server{ }配置块中,在nginx.conf配置文件里第1个出现的就是默认server。 注意,你不能把监听相同端口、地址对的两个server{ }块同时设为默认server,否则nginx将无法启动,并给出类似下方的错误输出:nginx: [emerg] a duplicate default server for 0.0.0.0:80 in /usr/local/nginx/conf/nginx.conf:40这就是请求匹配server{ }块的总体流程,下面我们来看server_name与域名的匹配,这也是最复杂的环节。 server_name指令对server{ }块的第2次关联 如果你购买过域名肯定清楚,虽然只买到一个域名,但你会有无数个子域名可以使用。比如我买到的是taohui.pub二级域名(pub是一级域名),我就可以配置出blog.taohui.pub这个三级域名,甚至自己搭建一个子域名解析服务,再配置出四级域名nginx.blog.taohui.pub,甚至五级、六级域名都能使用,如下图所示: 由于多级域名的存在,关联域名的server_name指令也相应地复杂起来,下面我们从3个层次看看server_name的选项种类。 首先,server_name支持精确地完全匹配,例如:server_name blog.taohui.pub;其次,server_name可以通过*符号作为通配符来匹配一类域名,比如:server_name *.taohui.pub;既可以匹配blog.taohui.pub,也可以匹配image.taohui.pub。由于*通配符在前方,所以我把它叫做前缀通配符。server_name还支持后缀通配符,例如:server_name www.taohui.*;它既可以匹配www.taohui.pub,也可以匹配www.taohui.tech域名。注意,server_name支持的通配符只能出现在最前方或者最后方,它不能出现在域名的中间,例如:server_name www.*.pub;就是非法的选项。 最后,当遇到通配符无法解决的场景时,可以使用正则表达式来匹配域名。当然,使用正则表达式的前提是将pcre开发库编译进Nginx(在CentOS下安装pcre开发库很简单,执行yum install pcre-devel -y即可。当有多个pcre版本并存时,可以通过configure --with-pcre=指定编译具体的pcre库)。 使用正则表达式时,需要在server_name选项前加入~符号,例如:server_name ~^ww\d.\w+.taohui.tech;$它可以匹配如ww3.blog.taohui.tech这样的域名。 当然,想一次写对正则表达式并不容易。pcre库提供的pcretest工具可以让我们提前测试正则表达式。注意,你用yum等工具安装pcre时,并不会自动安装pcretest工具,这需要你下载源代码(最新的pcre2-10.34下载地址参见这里,帮助文档参见这里)自行编译获得。本文不会讨论正则表达式的语法,也不会讨论pcretest工具的用法,关于Nginx中如何使用这两者,你可以观看下我在极客时间上的视频课程《Nginx核心知识100讲》第46课《Nginx中的正则表达式》。 Nginx中的正则表达式通常会提供提取变量的能力,server_name指令也不例外!我们可以通过小括号将域名中的信息取出来,交给后续的指令使用,例如:    server {        server_name ~^(ww\d).(?\w+).taohui.tech$;        return 200 ‘regular variable: $1 $domain‘;    }此时发起访问域名ww3.blog.taohui.tech的请求,由于第1个小括号我通过$1变量获取值为ww3,而第2个小括号我通过domain名称获得值为blog(通过$2也可以获得相同的内容),因此return指令发来的响应将会是regular variable: ww3 blog。 说完这3种域名选项后,我们再来看它们同时出现且匹配命中时,Nginx是怎样根据优先级来选择server{ }块的。域名的总体匹配优先级,与server{ }块在nginx.conf中的出现顺序无关,也与server_name指令在server{ }块中的出现顺序无关。事实上,对于监听同一地址、端口的server{ }块而言,Nginx会在进程启动时在收集所有server_name后,将精确匹配的字符串域名、前缀通配符、后缀通配符分别构建出3个哈希表,并将正则表达式构建为一个链表。我们看下请求到达时的匹配流程: 1.      匹配域名时,首先在字符串域名构成的哈希表上做精确查询,如果查询到了,就直接返回,因此,完全匹配的字符串域名优先级是最高的;2.      其次,将在前缀通配符哈希表上,按照每级域名的值分别查询哈希表,完成最长通配符的匹配。比如,blog.taohui.tech同时可以匹配以下2个前缀通配符:server_name *.tech;server_name *.taohui.tech;但Nginx会匹配命中*.taohui.tech。3.      其次,会在后缀通配符哈希表上做查询,完成最长通配符的匹配。4.      最后,会按照正则表达式在nginx.conf中出现的顺序,依次进行正则表达式匹配,这一步的性能比起前3步要慢许多。 这就是域名匹配的核心流程。 关于域名匹配你还需要了解的技巧 事实上,还有一些域名匹配的小技巧需要你掌握。首先,就像前面说过的HTTP/1.0协议是没有Host头部的,所以使用relative URL的HTTP/1.0请求并没有域名。按照之前的流程,它只能被默认server{ }块处理,这大大限制了默认server {}块的功能。在Nginx 0.7.11之后的版本,你可以通过server_name “”指定空字符串,来匹配没有域名的请求,这就解放了默认server { }的职责。其次,当Nginx对内网提供HTTP服务时,许多客户端会通过网络可达的主机名发起请求,这样客户端填写的域名就是主机名。如果必须由管理员先用hostname命令获取到主机名,再改写server_name指令,这就太不方便了。因此,server_name后还可以填写$hostname变量,这样Nginx启动时,会自动把$hostname替换为真正的主机名。server_name $hostname;最后一点,上文说过非正则表达式的server_name选项都会存放在哈希表中,这样哈希表中每个bucket桶大小就限制了域名的最大长度。当我们使用长域名或者多级域名时,默认的桶大小很可能就不够了,这时需要提升server_name的桶大小。桶大小由server_names_hash_bucket_size配置控制,由于CPU从内存中读入数据时是按批进行的,其中每批字节数是cpu cache line,因此为了一次可以载入一个哈希桶,server_names_hash_bucket_size的默认值被定为cpu cache line。目前多数CPU的cache line值是64字节,所以若域名较长时需要增加桶的大小。当你增大桶大小时,需要保证server_names_hash_bucket_size是cpu cache line的整数倍,这样读取哈希桶时,会尽量少地读取主存。毕竟操作主存的速度通常在100纳秒左右,这比CPU的速度慢得多!小结最后对本文做个小结。当TCP三次握手完成后,Linux内核就会按照内核的负载均衡算法,唤醒监听相应端口的某个Nginx worker进程。而从读事件及socket句柄上,Nginx可以找到对应的listen指令及所属的server{ }块,这完成了初次匹配。接着,Nginx会接收HTTP请求,从absolute URL、 Host域名或者TLS插件中取出域名,再将域名与server_name进行匹配。其中匹配优先级是这样的:精确的字符串匹配优先级最高,其次是前缀通配符和后缀通配符匹配(这两者匹配时,如果多个通配符命中,会选择最长的server_name),最后才是正则表达式匹配。如果以上情况皆未匹配上,请求会被默认server{ }处理。其中默认server {}是监听同一端口、地址的一系列server{ }块中,第1个在nginx.conf中出现的那个server{ }。当然,通过listen default_server也可以显示地定义默认server{ }。最后留给你一个思考题,为什么有人用server_name _;来处理未匹配上的请求?欢迎你留言一起探讨。  
分布式系统提升可用性时,最有效的方案就是在空间维度上,将资源复制一份作为缓存,并把缓存放在离用户更近的地方。这样,通过缩短用户的访问路径,不只可以降低请求的时延,多份资源还能提升系统的健壮性。比如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指令又是怎样工作的?欢迎你在帖子下方留言,与我一起探讨更好的热部署实现方案。