当上游出错时,作为负载均衡的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
负载均衡是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.
自2017年起HTTP3协议已发布了29个Draft,推出在即,Chrome、Nginx等软件都在跟进实现最新的草案。本文将介绍HTTP3协议规范、应用场景及实现原理。2015年HTTP2协议正式推出后,已经有接近一半的互联网站点在使用它: (图片来自https://w3techs.com/technologies/details/ce-http2)HTTP2协议虽然大幅提升了HTTP/1.1的性能,然而,基于TCP实现的HTTP2遗留下3个问题:l
上一篇文章中,我介绍了如何定制属于你自己的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配置文件。
谈到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操作,等等。可见,高性能既来自于架构,更来自于细节。
上一篇文章中,我介绍了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日志文件的方法。