深入理解Nginx
研究Nginx前的准备工作
2012年,Nginx荣获年度云计算开发奖(2012 Cloud Award for Developer ofthe Year),并成长为世界第二大Web服务器。全世界流量最高的前1000名网站中,超过25%都使用Nginx来处理海量的互联网请求。Nginx已经成为业界高性能Web服务器的代名词。
那么,什么是Nginx?它有哪些特点?我们选择Nginx的理由是什么?如何编译安装Nginx?这种安装方式背后隐藏的又是什么样的思想呢?本章将会回答上述问题。
Nginx是什么
人们在了解新事物时,往往习惯通过类比来帮助自己理解事物的概貌。那么,我们在学习Nginx时也采用同样的方式,先来看看Nginx的竞争对手——Apache、Lighttpd、Tomcat、Jetty、IIS,它们都是Web服务器,或者叫做WWW(World WideWeb)服务器,相应地也都具备Web服务器的基本功能:基于REST架构风格,以统一资源描述符(Uniform Resource Identifier,URI)或者统一资源定位符(UniformResource Locator,URL)作为沟通依据,通过HTTP为浏览器等客户端程序提供各种网络服务。然而,由于这些Web服务器在设计阶段就受到许多局限,例如当时的互联网用户规模、网络带宽、产品特点等局限,并且各自的定位与发展方向都不尽相同,使得每一款Web服务器的特点与应用场合都很鲜明。
Tomcat和Jetty面向Java语言,先天就是重量级的Web服务器,它的性能与Nginx没有可比性,这里略过。
IIS只能在Windows操作系统上运行。Windows作为服务器在稳定性与其他一些性能上都不如类UNIX操作系统,因此,在需要高性能Web服务器的场合下,IIS可能会被“冷落”。
Apache的发展时期很长,而且是目前毫无争议的世界第一大Web服务器,图1-1中是12年来(2000~2012年)世界Web服务器的使用排名情况。
从图1-1中可以看出,Apache目前处于领先地位。
Apache有许多优点,如稳定、开源、跨平台等,但它出现的时间太长了,在它兴起的年代,互联网的产业规模远远比不上今天,所以它被设计成了一个重量级的、不支持高并发的Web服务器。在Apache服务器上,如果有数以万计的并发HTTP请求同时访问,就会导致服务器上消耗大量内存,操作系统内核对成百上千的Apache进程做进程间切换也会消耗大量CPU资源,并导致HTTP请求的平均响应速度降低,这些都决定了Apache不可能成为高性能Web服务器,这也促使了Lighttpd和Nginx的出现。观察图1-1中Nginx成长的曲线,体会一下Nginx抢占市场时的“咄咄逼人”吧。
Lighttpd和Nginx一样,都是轻量级、高性能的Web服务器,欧美的业界开发者比较钟爱Lighttpd,而国内的公司更青睐Nginx,Lighttpd使用得比较少。
在了解了Nginx的竞争对手之后,相信大家对Nginx也有了直观感受,下面让我们来正式地认识一下Nginx吧。
来自俄罗斯的Igor Sysoev在为Rambler Media(http://www.rambler.ru/)工作期间,使用C语言开发了Nginx。Nginx作为Web服务器,一直为俄罗斯著名的门户网站Rambler Media提供着出色、稳定的服务。
Igor Sysoev将Nginx的代码开源,并且赋予其最自由的2-clause BSD-like license许可证。由于Nginx使用基于事件驱动的架构能够并发处理百万级别的TCP连接,高度模块化的设计和自由的许可证使得扩展Nginx功能的第三方模块层出不穷,而且优秀的设计带来了极佳的稳定性,因此其作为Web服务器被广泛应用到大流量的网站上,包括腾讯、新浪、网易、淘宝等访问量巨大的网站。
Nginx是一个跨平台的Web服务器,可运行在Linux、FreeBSD、Solaris、AIX、Mac OS、Windows等操作系统上,并且它还可以使用当前操作系统特有的一些高效API来提高自己的性能。
例如,对于高效处理大规模并发连接,它支持Linux上的epoll(epoll是Linux上处理大并发网络连接的利器,9.6.1节中将会详细说明epoll的工作原理)、Solaris上的eventports和FreeBSD上的kqueue等。
又如,对于Linux,Nginx支持其独有的sendfile系统调用,这个系统调用可以高效地把硬盘中的数据发送到网络上(不需要先把硬盘数据复制到用户态内存上再发送),这极大地减少了内核态与用户态数据间的复制动作。
种种迹象都表明,Nginx以性能为王。
2011年7月,Nginx正式成立公司,由Igor Sysoev担任CTO,立足于提供商业级的Web服务器。
为什么选择Nginx
为什么选择Nginx?因为它具有以下特点:
(1)更快
这表现在两个方面:一方面,在正常情况下,单次请求会得到更快的响应;另一方面,在高峰期(如有数以万计的并发请求),Nginx可以比其他Web服务器更快地响应请求。
(2)高扩展
inx的设计极具扩展性,它完全是由多个不同功能、不同层次、不同类型且耦合度极低的模块组成。因此,当对某一个模块修复Bug或进行升级时,可以专注于模块自身,无须在意其他。而且在HTTP模块中,还设计了HTTP过滤器模块:一个正常的HTTP模块在处理完请求后,会有一串HTTP过滤器模块对请求的结果进行再处理。这样,当我们开发一个新的HTTP模块时,不但可以使用诸如HTTP核心模块、events模块、log模块等不同层次或者不同类型的模块,还可以原封不动地复用大量已有的HTTP过滤器模块。这种低耦合度的优秀设计,造就了Nginx庞大的第三方模块,当然,公开的第三方模块也如官方发布的模块一样容易使用。
Nginx的模块都是嵌入到二进制文件中执行的,无论官方发布的模块还是第三方模块都是如此。这使得第三方模块一样具备极其优秀的性能,充分利用Nginx的高并发特性,因此,许多高流量的网站都倾向于开发符合自己业务特性的定制模块。
(3)高可靠性
高可靠性是我们选择Nginx的最基本条件,因为Nginx的可靠性是大家有目共睹的,很多家高流量网站都在核心服务器上大规模使用Nginx。Nginx的高可靠性来自于其核心框架代码的优秀设计、模块设计的简单性;另外,官方提供的常用模块都非常稳定,每个worker进程相对独立,master进程在1个worker进程出错时可以快速“拉起”新的worker子进程提供服务。
(4)低内存消耗
般情况下,10000个非活跃的HTTP Keep-Alive连接在Nginx中仅消耗2.5MB的内存,这是Nginx支持高并发连接的基础。
(5)单机支持10万以上的并发连接
这是一个非常重要的特性!随着互联网的迅猛发展和互联网用户数量的成倍增长,各大公司、网站都需要应付海量并发请求,一个能够在峰值期顶住10万以上并发请求的Server,无疑会得到大家的青睐。理论上,Nginx支持的并发连接上限取决于内存,10万远未封顶。当然,能够及时地处理更多的并发请求,是与业务特点紧密相关的。
(6)热部署
master管理进程与worker工作进程的分离设计,使得Nginx能够提供热部署功能,即可以在7×24小时不间断服务的前提下,升级Nginx的可执行文件。当然,它也支持不停止服务就更新配置项、更换日志文件等功能。
(7)最自由的BSD许可协
这是Nginx可以快速发展的强大动力。BSD许可协议不只是允许用户免费使用Nginx,它还允许用户在自己的项目中直接使用或修改Nginx源码,然后发布。这吸引了无数开发者继续为Nginx贡献自己的智慧。
以上7个特点当然不是Nginx的全部,拥有无数个官方功能模块、第三方功能模块使得Nginx能够满足绝大部分应用场景,这些功能模块间可以叠加以实现更加强大、复杂的功能,有些模块还支持Nginx与Perl、Lua等脚本语言集成工作,大大提高了开发效率。这些特点促使用户在寻找一个Web服务器时更多考虑Nginx。
当然,选择Nginx的核心理由还是它能在支持高并发请求的同时保持高效的服务。
如果Web服务器的业务访问量巨大,就需要保证在数以百万计的请求同时访问服务时,用户可以获得良好的体验,不会出现并发访问量达到一个数字后,新的用户无法获取服务,或者虽然成功地建立起了TCP连接,但大部分请求却得不到响应的情况。
通常,高峰期服务器的访问量可能是正常情况下的许多倍,若有热点事件的发生,可能会导致正常情况下非常顺畅的服务器直接“挂死”。然而,如果在部署服务器时,就预先针对这种情况进行扩容,又会使得正常情况下所有服务器的负载过低,这会造成大量的资源浪费。因此,我们会希望在这之间取得平衡,也就是说,在低并发压力下,用户可以获得高速体验,而在高并发压力下,更多的用户都能接入,可能访问速度会下降,但这只应受制于带宽和处理器的速度,而不应该是服务器设计导致的软件瓶颈。
事实上,由于中国互联网用户群体的数量巨大,致使对Web服务器的设计往往要比欧美公司更加困难。例如,对于全球性的一些网站而言,欧美用户分布在两个半球,欧洲用户活跃时,美洲用户通常在休息,反之亦然。而国内巨大的用户群体则对业界的程序员提出更高的挑战,早上9点和晚上20点到24点这些时间段的并发请求压力是非常巨大的。尤其节假日、寒暑假到来之时,更会对服务器提出极高的要求。
另外,国内业务上的特性,也会引导用户在同一时间大并发地访问服务器。例如,许多SNS网页游戏会在固定的时间点刷新游戏资源或者允许“偷菜”等好友互动操作。这些会导致服务器处理高并发请求的压力增大。
上述情形都对我们的互联网服务在大并发压力下是否还能够给予用户良好的体验提出了更高的要求。若要提供更好的服务,那么可以从多方面入手,例如,修改业务特性、引导用户从高峰期分流或者把服务分层分级、对于不同并发压力给用户提供不同级别的服务等。但最根本的是,Web服务器要能支持大并发压力下的正常服务,这才是关键。
快速增长的互联网用户群以及业内所有互联网服务提供商越来越好的用户体验,都促使我们在大流量服务中用Nginx取代其他Web服务器。Nginx先天的事件驱动型设计、全异步的网络I/O处理机制、极少的进程间切换以及许多优化设计,都使得Nginx天生善于处理高并发压力下的互联网请求,同时Nginx降低了资源消耗,可以把服务器硬件资源“压榨”到极致。
Nginx的配置
Nginx拥有大量官方发布的模块和第三方模块,这些已有的模块可以帮助我们实现Web服务器上很多的功能。使用这些模块时,仅仅需要增加、修改一些配置项即可。
运行中的Nginx进程间的关系
在正式提供服务的产品环境下,部署Nginx时都是使用一个master进程来管理多个worker进程,一般情况下,worker进程的数量与服务器上的CPU核心数相等。每一个worker进程都是繁忙的,它们在真正地提供互联网服务,master进程则很“清闲”,只负责监控管理worker进程。worker进程之间通过共享内存、原子操作等一些进程间通信机制来实现负载均衡等功能
部署后Nginx进程间的关系如下图:
Nginx是支持单进程(master进程)提供服务的,那么为什么产品环境下要按照master-worker方式配置同时启动多个进程呢?这样做的好处主要有以下两点:
- 由于master进程不会对用户请求提供服务,只用于管理真正提供服务的worker进程,所以master进程可以是唯一的,它仅专注于自己的纯管理工作,为管理员提供命令行服务,包括诸如启动服务、停止服务、重载配置文件、平滑升级程序等。master进程需要拥有较大的权限,例如,通常会利用root用户启动master进程。worker进程的权限要小于或等于master进程,这样master进程才可以完全地管理worker进程。当任意一个worker进程出现错误从而导致coredump时,master进程会立刻启动新的worker进程继续服务。
- 多个worker进程处理互联网请求不但可以提高服务的健壮性(一个worker进程出错后,其他worker进程仍然可以正常提供服务),最重要的是,这样可以充分利用现在常见的SMP多核架构,从而实现微观上真正的多核并发处理。因此,用一个进程(master进程)来处理互联网请求肯定是不合适的。另外,为什么要把worker进程数量设置得与CPU核心数量一致呢?这正是Nginx与Apache服务器的不同之处。在Apache上每个进程在一个时刻只处理一个请求,因此,如果希望Web服务器拥有并发处理的请求数更多,就要把Apache的进程或线程数设置得更多,通常会达到一台服务器拥有几百个工作进程,这样大量的进程间切换将带来无谓的系统资源消耗。而Nginx则不然,一个worker进程可以同时处理的请求数只受限于内存大小,而且在架构设计上,不同的worker进程之间处理并发请求时几乎没有同步锁的限制,worker进程通常不会进入睡眠状态,因此,当Nginx上的进程数与CPU核心数相等时(最好每一个worker进程都绑定特定的CPU核心),进程间切换的代价是最小的。(这里说的Apache每个进程一个时刻只处理一个请求应该说的是,在这个请求处理完之前,其它请求不会被处理;而使用Nginx的话就不会出现这种问题了,CPU每一时刻虽然都调用一个线程,但是这个线程却可以在当前请求没处理完之前切换成其他线程执行其它请求,因此Nginx能同时在一段时间间隔内处理多个请求;)
举例来说,如果产品中的服务器CPU核心数为8,那么就需要配置8个worker进程,如下图:
如果对路径部分都使用默认配置,那么Nginx运行目录为/usr/local/nginx,其目录结构如下。
Nginx配置的通用语
Nginx的配置文件其实是一个普通的文本文件。如下图:
块配置项
块配置项由一个块配置项名和一对大括号组成。具体示例如下:
上面代码段中的events、http、server、location、upstream等都是块配置项,块配置项之后是否如“location/webstatic{…}”那样在后面加上参数,取决于解析这个块配置项的模块,不能一概而论,但块配置项一定会用大括号把一系列所属的配置项全包含进来,表示大括号内的配置项同时生效。所有的事件类配置都要在events块中,http、server等配置也遵循这个规定。
块配置项可以嵌套。内层块直接继承外层块,例如,上例中,server块里的任意配置都是基于http块里的已有配置的。当内外层块中的配置发生冲突时,究竟是以内层块还是外层块的配置为准,取决于解析这个配置项的模块。例如,上例在http模块中已经打开了“gzip on;”,但其下的location/webstatic又把gzip关闭了:gzip off;,最终,在/webstatic的处理模块中,gzip模块是按照gzip off来处理请求的。
配置项得到语法格式
从上文的示例可以看出,最基本的配置项语法格式如下:
配置项名 配置项值1 配置项值2 …;
下面解释一下配置项的构成部分。
首先,在行首的是配置项名,这些配置项名必须是Nginx的某一个模块想要处理的,否则Nginx会认为配置文件出现了非法的配置项名。配置项名输入结束后,将以空格作为分隔符。
其次是配置项值,它可以是数字或字符串(当然也包括正则表达式)。针对一个配置项,既可以只有一个值,也可以包含多个值,配置项值之间仍然由空格符来分隔。当然,一个配置项对应的值究竟有多少个,取决于解析这个配置项的模块。我们必须根据某个Nginx模块对一个配置项的约定来更改配置项.
最后,每行配置的结尾需要加上分号。
**注意:
如果配置项值中包括语法符号,比如空格符,那么需要使用单引号或双引号括住配置项值,否则Nginx会报语法错误。**例如:
og_format main '$remote_addr - $remote_user [$time_local] "$request" ';
配置项的注释
果有一个配置项暂时需要注释掉,那么可以加“#”注释掉这一行配置。例如:
#pid logs/nginx.pid
配置项的单位
部分模块遵循一些通用的规定,如指定空间大小时不用每次都定义到字节、指定时间时不用精确到毫秒。
当指定空间大小时,可以使用的单位包括:
- K或者k千字节(KiloByte,KB)。
- M或者m兆字节(MegaByte,MB)。
例如:
gzip_buffers 4 8k;
client_max_body_size 64M;
当指定时间时,可以使用的单位包括:
- ms(毫秒),s(秒),m(分钟),h(小时),d(天),w(周,包含7天),M(月,包含30天),y(年,包含365天)。
例如:
expires 10y;
proxy_read_timeout 600;
client_body_timeout 2m;
注意,配置项后的值究竟是否可以使用这些单位,取决于解析该配置项的模块。如果这个模块使用了Nginx框架提供的相应解析配置项方法,那么配置项值才可以携带单位。
在配置中使用变量
有些模块允许在配置项中使用变量,如在日志记录部分,具体示例如下。
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $bytes_sent "$http_referer" ' '"$http_user_agent" "$http_x_forwarded_for"';
其中,remote_addr是一个变量,使用它的时候前面要加上$符号。需要注意的是,这种变量只有少数模块支持,并不是通用的。
许多模块在解析请求时都会提供多个变量(如本章后面提到的http core module、http proxy module、http upstream module等),以使其他模块的配置可以即时使用。我们在学习某个模块提供的配置说明时可以关注它是否提供变量。
提示 在执行configure命令时,我们已经把许多模块编译进Nginx中,但是否启用这些模块,一般取决于配置文件中相应的配置项。换句话说,每个Nginx模块都有自己感兴趣的配置项,大部分模块都必须在nginx.conf中读取某个配置项后才会在运行时启用。例如,只有当配置http{…}这个配置项时,ngx_http_module模块才会在Nginx中启用,其他依赖ngx_http_module的模块也才能正常使用。
Nginx服务的基本配置
Nginx在运行时,至少必须加载几个核心模块和一个事件类模块。这些模块运行时所支持的配置项称为基本配置——所有其他模块执行时都依赖的配置项。
下面详述基本配置项的用法。由于配置项较多,所以把它们按照用户使用时的预期功能分成了以下4类:
- 用于调试、定位问题的配置项。
- 正常运行的必备配置项。
- 优化性能的配置项。
- 事件类配置项(有些事件类配置项归纳到优化性能类,这是因为它们虽然也属于events{}块,但作用是优化性能)。
有这么一些配置项,即使没有显式地进行配置,它们也会有默认的值,如daemon,即使在nginx.conf中没有对它进行配置,也相当于打开了这个功能,这点需要注意。对于这样的配置项,作者会在下面相应的配置项描述上加入一行“默认:”来进行说明。
用于调试进程和定位问题的配置项
先来看一下用于调试进程、定位问题的配置项,如下所示。
(1)是否以守护进程方式运行Nginx
语法:daemon on|off;
默认:daemon on;
护进程(daemon)是脱离终端并且在后台运行的进程。它脱离终端是为了避免进程执行过程中的信息在任何终端上显示,这样一来,进程也不会被任何终端所产生的信息所打断。Nginx毫无疑问是一个需要以守护进程方式运行的服务,因此,默认都是以这种方式运行的。
不过Nginx还是提供了关闭守护进程的模式,之所以提供这种模式,是为了方便跟踪调试Nginx,毕竟用gdb调试进程时最烦琐的就是如何继续跟进fork出的子进程了。这在第三部分研究Nginx架构时很有用。
(2)是否以master/worker方式工作
语法:master_process on|off;
默认:master_process on;
可以看到,在如图2-1所示的产品环境中,是以一个master进程管理多个worker进程的方式运行的,几乎所有的产品环境下,Nginx都以这种方式工作。
与daemon配置相同,提供master_process配置也是为了方便跟踪调试Nginx。如果用off关闭了master_process方式,就不会fork出worker子进程来处理请求,而是用master进程自身来处理请求。
(3)error日志的设置
语法:error_log/path/file level;
默认:error_log logs/error.log error;
error日志是定位Nginx问题的最佳工具,我们可以根据自己的需求妥善设置error日志的路径和级别。
/path/file参数可以是一个具体的文件,例如,默认情况下是logs/error.log文件,最好将它放到一个磁盘空间足够大的位置;/path/file也可以是/dev/null,这样就不会输出任何日志了,这也是关闭error日志的唯一手段;/path/file也可以是stderr,这样日志会输出到标准错误文件中。
level是日志的输出级别,取值范围是debug、info、notice、warn、error、crit、alert、emerg,从左至右级别依次增大。当设定为一个级别时,大于或等于该级别的日志都会被输出到/path/file文件中,小于该级别的日志则不会输出。例如,当设定为error级别时,error、crit、alert、emerg级别的日志都会输出。
如果设定的日志级别是debug,则会输出所有的日志,这样数据量会很大,需要预先确保/path/file所在磁盘有足够的磁盘空间。
注意 如果日志级别设定到debug,必须在configure时加入–with-debug配置项。
(4)是否处理几个特殊的调试点
语法:debug_points[stop|abort]
这个配置项也是用来帮助用户跟踪调试Nginx的。它接受两个参数:stop和abort。Nginx在一些关键的错误逻辑中(Nginx 1.0.14版本中有8处)设置了调试点。如果设置了debug_points为stop,那么Nginx的代码执行到这些调试点时就会发出SIGSTOP信号以用于调试。如果debug_points设置为abort,则会产生一个coredump文件,可以使用gdb来查看Nginx当时的各种信息。
通常不会使用这个配置项。
(5)仅对指定的客户端输出debug级别的日志
语法:debug_connection[IP|CIDR]
这个配置项实际上属于事件类配置,因此,它必须放在events{…}中才有效。它的值可以是IP地址或CIDR地址,例如:
events {
debug_connection 10.224.66.14;
debug_connection 10.224.57.0/24;}
这样,仅仅来自以上IP地址的请求才会输出debug级别的日志,其他请求仍然沿用error_log中配置的日志级别。
上面这个配置对修复Bug很有用,特别是定位高并发请求下才会发生的问题。
注意 使用debug_connection前,需确保在执行configure时已经加入了–with-debug参数,否则不会生效。
(6)限制coredump核心转储文件的大小
语法:worker_rlimit_core size;
在Linux系统中,当进程发生错误或收到信号而终止时,系统会将进程执行时的内存内容(核心映像)写入一个文件(core文件),以作为调试之用,这就是所谓的核心转储(core dumps)。当Nginx进程出现一些非法操作(如内存越界)导致进程直接被操作系统强制结束时,会生成核心转储core文件,可以从core文件获取当时的堆栈、寄存器等信息,从而帮助我们定位问题。但这种core文件中的许多信息不一定是用户需要的,如果不加以限制,那么可能一个core文件会达到几GB,这样随便coredumps几次就会把磁盘占满,引发严重问题。通过worker_rlimit_core配置可以限制core文件的大小,从而有效帮助用户定位问题。
(7)指定coredump文件生成目录
语法:working_directory path;
worker进程的工作目录。这个配置项的唯一用途就是设置coredump文件所放置的目录,协助定位问题。因此,需确保worker进程有权限向working_directory指定的目录中写入文件。
正常运行的配置项
下面是正常运行的配置项的相关介绍。
(1)定义环境变量
语法:env VAR|VAR=VALUE
这个配置项可以让用户直接设置操作系统上的环境变量。例如:
env TESTPATH=/tmp/;
(2)嵌入其他配置文件
语法:include/path/file;
include配置项可以将其他配置文件嵌入到当前的nginx.conf文件中,它的参数既可以是绝对路径,也可以是相对路径(相对于Nginx的配置目录,即nginx.conf所在的目录),例如:
include mime.types;
include vhost/*.conf
可以看到,参数的值可以是一个明确的文件名,也可以是含有通配符*的文件名,同时可以一次嵌入多个配置文件。
(3)pid文件的路径
语法:pid path/file;
默认:pid logs/nginx.pid;
保存master进程ID的pid文件存放路径。默认与configure执行时的参数“–pid-path”所指定的路径是相同的,也可以随时修改,但应确保Nginx有权在相应的目标中创建pid文件,该文件直接影响Nginx是否可以运行。
(4)Nginx worker进程运行的用户及用户组
语法:user username[groupname];
默认:user nobody nobody;
user用于设置master进程启动后,fork出的worker进程运行在哪个用户和用户组下。当按照“user username;”设置时,用户组名与用户名相同。
若用户在configure命令执行时使用了参数–user=username和–group=groupname,此时nginx.conf将使用参数中指定的用户和用户组。
(5)指定Nginx worker进程可以打开的最大句柄描述符个数
语法:worker_rlimit_nofile limit
设置一个worker进程可以打开的最大文件句柄数。
(6)限制信号队列
语法:worker_rlimit_sigpending limit;
设置每个用户发往Nginx的信号队列的大小。也就是说,当某个用户的信号队列满了,这个用户再发送的信号量会被丢掉。
优化性能的配置项
下面是优化性能的配置项的相关介绍。
(1)Nginx worker进程个数
语法:worker_processes number;
默认:worker_processes 1;
在master/worker运行方式下,定义worker进程的个数。
worker进程的数量会直接影响性能。那么,用户配置多少个worker进程才好呢?这实际上与业务需求有关。
每个worker进程都是单线程的进程,它们会调用各个模块以实现多种多样的功能。如果这些模块确认不会出现阻塞式的调用,那么,有多少CPU内核就应该配置多少个进程;反之,如果有可能出现阻塞式调用,那么需要配置稍多一些的worker进程。
例如,如果业务方面会致使用户请求大量读取本地磁盘上的静态资源文件,而且服务器上的内存较小,以至于大部分的请求访问静态资源文件时都必须读取磁盘(磁头的寻址是缓慢的),而不是内存中的磁盘缓存,那么磁盘I/O调用可能会阻塞住worker进程少量时间,进而导致服务整体性能下降。
多worker进程可以充分利用多核系统架构,但若worker进程的数量多于CPU内核数,那么会增大进程间切换带来的消耗(Linux是抢占式内核)。一般情况下,用户要配置与CPU内核数相等的worker进程,并且使用下面的worker_cpu_affinity配置来绑定CPU内核。
(2)绑定Nginx worker进程到指定的CPU内核
语法:worker_cpu_affinity cpumask[cpumask…]
为什么要绑定worker进程到指定的CPU内核呢?假定每一个worker进程都是非常繁忙的,如果多个worker进程都在抢同一个CPU,那么这就会出现同步问题。反之,如果每一个worker进程都独享一个CPU,就在内核的调度策略上实现了完全的并发。
例如,如果有4颗CPU内核,就可以进行如下配置:
worker_processes 4;
worker_cpu_affinity 1000 0100 0010 0001;
注意 worker_cpu_affinity配置仅对Linux操作系统有效。Linux操作系统使用sched_setaffinity()系统调用实现这个功能。
(3)SSL硬件加速
语法:ssl_engine device;
如果服务器上有SSL硬件加速设备,那么就可以进行配置以加快SSL协议的处理速度。用户可以使用OpenSSL提供的命令来查看是否有SSL硬件加速设备:
openssl engine -t
事件类配置
下面是事件类配置项的相关介绍。
(1)是否打开accept锁
语法:accept_mutex[on|off]
默认:accept_mutext on;
accept_mutex是Nginx的负载均衡锁,本书会在第9章事件处理框架中详述Nginx是如何实现负载均衡的。这里,读者仅需要知道accept_mutex这把锁可以让多个worker进程轮流地、序列化地与新的客户端建立TCP连接。当某一个worker进程建立的连接数量达到worker_connections配置的最大连接数的7/8时,会大大地减小该worker进程试图建立新TCP连接的机会,以此实现所有worker进程之上处理的客户端请求数尽量接近。
accept锁默认是打开的,如果关闭它,那么建立TCP连接的耗时会更短,但worker进程之间的负载会非常不均衡,因此不建议关闭它。
(2)lock文件的路径
语法:lock_file path/file;
默认:lock_file logs/nginx.lock;
accept锁可能需要这个lock文件,如果accept锁关闭,lock_file配置完全不生效。如果打开了accept锁,并且由于编译程序、操作系统架构等因素导致Nginx不支持原子锁,这时才会用文件锁实现accept锁,这样lock_file指定的lock文件才会生效。
注意 在基于i386、AMD64、Sparc64、PPC64体系架构的操作系统上,若使用GCC、Intel C++、SunPro C++编译器来编译Nginx,则可以肯定这时的Nginx是支持原子锁的,因为Nginx会利用CPU的特性并用汇编语言来实现它。这时的lock_file配置是没有意义的。
(3)使用accept锁后到真正建立连接之间的延迟时间
语法:accept_mutex_delay Nms;
默认:accept_mutex_delay 500ms;
在使用accept锁后,同一时间只有一个worker进程能够取到accept锁。这个accept锁不是阻塞锁,如果取不到会立刻返回。如果有一个worker进程试图取accept锁而没有取到,它至少要等accept_mutex_delay定义的时间间隔后才能再次试图取锁。
(4)批量建立新连
语法:multi_accept[on|off];
默认:multi_accept off;
当事件模型通知有新连接时,尽可能地对本次调度中客户端发起的所有TCP请求都建立连接。
(5)选择事件模型
语法:use[kqueue|rtsig|epoll|/dev/poll|select|poll|eventport];
默认:Nginx会自动使用最适合的事件模型。
对于Linux操作系统来说,可供选择的事件驱动模型有poll、select、epoll三种。epoll当然是性能最高的一种。
(6)每个worker的最大连接数
语法:worker_connections number;
定义每个worker进程可以同时处理的最大连接数。
用HTTP核心模块配置一个静态Web服务器
静态Web服务器的主要功能由ngx_http_core_module模块(HTTP框架的主要成员)实现,当然,一个完整的静态Web服务器还有许多功能是由其他的HTTP模块实现的。本节主要讨论如何配置一个包含基本功能的静态Web服务器,文中会完整地说明ngx_http_core_module模块提供的配置项及变量的用法,但不会过多说明其他HTTP模块的配置项。在阅读完本节内容后,读者应当可以通过简单的查询相关模块(如ngx_http_gzip_filter_module、ngx_http_image_filter_module等)的配置项说明,方便地在nginx.conf配置文件中加入新的配置项,从而实现更多的Web服务器功能。
除了2.3节提到的基本配置项外,一个典型的静态Web服务器还会包含多个server块和location块,例如:
所有的HTTP配置项都必须直属于http块、server块、location块、upstream块或if块等(HTTP配置项自然必须全部在http{}块之内,这里的“直属于”是指配置项直接所属的大括号对应的配置块),同时,在描述每个配置项的功能时,会说明它可以在上述的哪个块中存在,因为有些配置项可以任意地出现在某一个块中,而有些配置项只能出现在特定的块中,在第4章介绍自定义配置项的读取时,相信读者就会体会到这种设计思路。
Nginx为配置一个完整的静态Web服务器提供了非常多的功能,下面会把这些配置项分为以下8类进行详述:虚拟主机与请求的分发、文件路径的定义、内存及磁盘资源的分配、网络连接的设置、MIME类型的设置、对客户端请求的限制、文件操作的优化、对客户端请求的特殊处理。这种划分只是为了帮助大家从功能上理解这些配置项。
在这之后会列出ngx_http_core_module模块提供的变量,以及简单说明它们的意义。
虚拟主机与请求的分发
由于IP地址的数量有限,因此经常存在多个主机域名对应着同一个IP地址的情况,这时在nginx.conf中就可以按照server_name(对应用户请求中的主机域名)并通过server块来定义虚拟主机,每个server块就是一个虚拟主机,它只处理与之相对应的主机域名请求。这样,一台服务器上的Nginx就能以不同的方式处理访问不同主机域名的HTTP请求了。
(1)监听端口
语法:listen address:port[default(deprecated in 0.8.21)|default_server[backlog=num|rcvbuf=size|sndbuf=size|accept_filter=filter|deferred|bind|ipv6only=[on|off]|ssl]];
默认:listen 80;
配置块:server
listen参数决定Nginx服务如何监听端口。在listen后可以只加IP地址、端口或主机名,非常灵活,例如:
listen 127.0.0.1:8000;
listen 127.0.0.1; #注意:不加端口时,默认监听80端口
listen 8000;
listen *:8000;
listen localhost:8000;
如果服务器使用IPv6地址,那么可以这样使用:
listen [::]:8000;
listen [fe80::1];
listen [:::a8c9:1234]:80;
在地址和端口后,还可以加上其他参数,例如:
listen 443 default_server ssl;
listen 127.0.0.1 default_server accept_filter=dataready backlog=1024;
下面说明listen可用参数的意义。
- default:将所在的server块作为整个Web服务的默认server块。如果没有设置这个参数,那么将会以在nginx.conf中找到的第一个server块作为默认server块。为什么需要默认虚拟主机呢?当一个请求无法匹配配置文件中的所有主机域名时,就会选用默认的虚拟主机(在11.3节介绍默认主机的使用)
- default_server:同上。
- backlog=num:表示TCP中backlog队列的大小。默认为–1,表示不予设置。在TCP建立三次握手过程中,进程还没有开始处理监听句柄,这时backlog队列将会放置这些新连接。可如果backlog队列已满,还有新的客户端试图通过三次握手建立TCP连接,这时客户端将会建立连接失败。
- rcvbuf=size:设置监听句柄的SO_RCVBUF参数。
- sndbuf=size:设置监听句柄的SO_SNDBUF参数。
- accept_filter:设置accept过滤器,只对FreeBSD操作系统有用。
- deferred:在设置该参数后,若用户发起建立连接请求,并且完成了TCP的三次握手,内核也不会为了这次的连接调度worker进程来处理,只有用户真的发送请求数据时(内核已经在网卡中收到请求数据包),内核才会唤醒worker进程处理这个连接。这个参数适用于大并发的情况下,它减轻了worker进程的负担。当请求数据来临时,worker进程才会开始处理这个连接。只有确认上面所说的应用场景符合自己的业务需求时,才可以使用deferred配置。
- bind:绑定当前端口/地址对,如127.0.0.1:8000。只有同时对一个端口监听多个地址时才会生效。
- ssl:在当前监听的端口上建立的连接必须基于SSL协议。
(2)主机名称
语法:server_name name[…];
默认:server_name"";
配置块:server
server_name后可以跟多个主机名称,如server_name www.testweb.com、download.testweb.com;。
在开始处理一个HTTP请求时,Nginx会取出header头中的Host,与每个server中的server_name进行匹配,以此决定到底由哪一个server块来处理这个请求。有可能一个Host与多个server块中的server_name都匹配,这时就会根据匹配优先级来选择实际处理的server块。server_name与Host的匹配优先级如下:
- 首先选择所有字符串完全匹配的server_name,如www.testweb.com。
- 其次选择通配符在前面的server_name,如*.testweb.com。
- 再次选择通配符在后面的server_name,如www.testweb.*。
- 最后选择使用正则表达式才匹配的server_name,如~^.testweb.com$。
如果Host与所有的server_name都不匹配,这时将会按下列顺序选择处理的server块。
1.优先选择在listen配置项后加入[default|default_server]的server块。
2.找到匹配listen端口的第一个server块。如果server_name后跟着空字符串(如server_name"";),那么表示匹配没有Host这个HTTP头部的请求。
3.server_names_hash_bucket_size
语法:server_names_hash_bucket_size size;
默认:server_names_hash_bucket_size 32|64|128;
配置块:http、server、location
为了提高快速寻找到相应server name的能力,Nginx使用散列表来存储servername。server_names_hash_bucket_size设置了每个散列桶占用的内存大小。
4.server_names_hash_max_size
语法:server_names_hash_max_size size;
默认:server_names_hash_max_size 512;
配置块:http、server、location
server_names_hash_max_size会影响散列表的冲突率。server_names_hash_max_size越大,消耗的内存就越多,但散列key的冲突率则会降低,检索速度也更快。server_names_hash_max_size越小,消耗的内存就越小,但散列key的冲突率可能增高。
5.重定向主机名称的处理
语法:server_name_in_redirect on|off;
默认:server_name_in_redirect on;
配置块:http、server或者location
该配置需要配合server_name使用。在使用on打开时,表示在重定向请求时会使用server_name里配置的第一个主机名代替原先请求中的Host头部,而使用off关闭时,表示在重定向请求时使用请求本身的Host头部。
6.location
语法:location[=||*|^~|@]/uri/{…}
配置块:server
location会尝试根据用户请求中的URI来匹配上面的/uri表达式,如果可以匹配,就选择location{}块中的配置来处理用户请求。当然,匹配方式是多样的,下面介绍location的匹配规则。
1)=表示把URI作为字符串,以便与参数中的uri做完全匹配。例如:
location = / { #只有当用户请求是
/时,才会使用该
location下的配置
…
}
2)~表示匹配URI时是字母大小写敏感的。
3)~*表示匹配URI时忽略字母大小写问题。
4)^~表示匹配URI时只需要其前半部分与uri参数匹配即可。例如:
location ^~ /images/ { # 以
/images/开始的请求都会匹配上
…
}
5)@表示仅用于Nginx服务内部请求之间的重定向,带有@的location不直接处理用户请求。
当然,在uri参数里是可以用正则表达式的,例如:
location ~* \.(gif|jpg|jpeg)$ { # 匹配以
.gif、
.jpg、
.jpeg结尾的请求
…
}
注意,location是有顺序的,当一个请求有可能匹配多个location时,实际上这个请求会被第一个location处理。
在以上各种匹配方式中,都只能表达为“如果匹配…则…”。如果需要表达“如果不匹配…则…”,就很难直接做到。有一种解决方法是在最后一个location中使用/作为参数,它会匹配所有的HTTP请求,这样就可以表示如果不能匹配前面的所有location,则由“/”这个location处理。例如:
location / { # /可以匹配所有请求
…
}
文件路径的定义
下面介绍一下文件路径的定义配置项。
(1)以root方式设置资源路径
语法:root path;
默认:root html;
配置块:http、server、location、if
例如,定义资源文件相对于HTTP请求的根目录。
location /download/ {
root /opt/web/html/;
}
在上面的配置中,如果有一个请求的URI是/download/index/test.html,那么Web服务器将会返回服务器上/opt/web/html/download/index/test.html文件的内容。
(2)以alias方式设置资源路径
语法:alias path;
配置块:location
alias也是用来设置文件资源路径的,它与root的不同点主要在于如何解读紧跟location后面的uri参数,这将会致使alias与root以不同的方式将用户请求映射到真正的磁盘文件上。例如,如果有一个请求的URI是/conf/nginx.conf,而用户实际想访问的文件在/usr/local/nginx/conf/nginx.conf,那么想要使用alias来进行设置的话,可以采用如下方式:
location /conf {
alias /usr/local/nginx/conf/;
}
如果用root设置,那么语句如下所示:
location /conf {
root /usr/local/nginx/;
}
使用alias时,在URI向实际文件路径的映射过程中,已经把location后配置的/conf这部分字符串丢弃掉,因此,/conf/nginx.conf请求将根据alias path映射为path/nginx.conf。root则不然,它会根据完整的URI请求来映射,因此,/conf/nginx.conf请求会根据root path映射为path/conf/nginx.conf。这也是root可以放置到http、server、location或if块中,而alias只能放置到location块中的原因。
alias后面还可以添加正则表达式,例如:
location ~ ^/test/(\w+)\.(\w+)$ {
alias /usr/local/nginx/$2/$1.$2;
}
这样,请求在访问/test/nginx.conf时,Nginx会返回/usr/local/nginx/conf/nginx.conf文件中的内容。
(3)访问首页
语法:index file…;
默认:index index.html;
配置块:http、server、location
有时,访问站点时的URI是/,这时一般是返回网站的首页,而这与root和alias都不同。这里用ngx_http_index_module模块提供的index配置实现。index后可以跟多个文件参数,Nginx将会按照顺序来访问这些文件,例如:
location / {
root path;
index /index.html /html/index.php /index.php;
}
接收到请求后,Nginx首先会尝试访问path/index.php文件,如果可以访问,就直接返回文件内容结束请求,否则再试图返回path/html/index.php文件的内容,依此类推。
(4)根据HTTP返回码重定向页面
语法:error_page code[code…][=|=answer-code]uri|@named_location
配置块:http、server、location、if
当对于某个请求返回错误码时,如果匹配上了error_page中设置的code,则重定向到新的URI中。例如:
error_page 404 /404.html;
error_page 502 503 504 /50x.html;
error_page 403 http://exa
mple.com/forbidden.html;error_page 404 = @fetch;
注意,虽然重定向了URI,但返回的HTTP错误码还是与原来的相同。用户可以通过“=”来更改返回的错误码,例如:
error_page 404 =200 /empty.gif;
error_page 404 =403 /forbidden.gif;
也可以不指定确切的返回错误码,而是由重定向后实际处理的真实结果来决定,这时,只要把“=”后面的错误码去掉即可,例如:
error_page 404 = /empty.gif;
如果不想修改URI,只是想让这样的请求重定向到另一个location中进行处理,那么可以这样设置:
location / (
error_page 404 @fallback;
)
location @fallback (
proxy_pass http://backend;
)
这样,返回404的请求会被反向代理到http://backend上游服务器中处理。
(5)是否允许递归使用error_page
语法:recursive_error_pages[on|off];
默认:recursive_error_pages off;
配置块:http、server、location
确定是否允许递归地定义error_page。
(6)try_files
语法:try_files path1[path2]uri;
配置块:server、location
try_files后要跟若干路径,如path1 path2…,而且最后必须要有uri参数,意义如下:尝试按照顺序访问每一个path,如果可以有效地读取,就直接向用户返回这个path对应的文件结束请求,否则继续向下访问。如果所有的path都找不到有效的文件,就重定向到最后的参数uri上。因此,最后这个参数uri必须存在,而且它应该是可以有效重定向的。例如:
try_files /system/maintenance.html $uri $uri/index.html $uri.html @other;
location @other {
proxy_pass http://backend;
}
上面这段代码表示如果前面的路径,如/system/maintenance.html等,都找不到,就会反向代理到http://backend服务上。还可以用指定错误码的方式与error_page配合使用,例如:
location / {
try_files $uri $uri/ /error.phpc=404 =404;
}
内存及磁盘资源的分配
(1)HTTP包体只存储到磁盘文件中
语法:client_body_in_file_only on|clean|off;
默认:client_body_in_file_only off;
配置块:http、server、location
当值为非off时,用户请求中的HTTP包体一律存储到磁盘文件中,即使只有0字节也会存储为文件。当请求结束时,如果配置为on,则这个文件不会被删除(该配置一般用于调试、定位问题),但如果配置为clean,则会删除该文件。
(2)HTTP包体尽量写入到一个内存buffer中
语法:client_body_in_single_buffer on|off;
默认:client_body_in_single_buffer off;
配置块:http、server、location
用户请求中的HTTP包体一律存储到内存buffer中。当然,如果HTTP包体的大小超过了下面client_body_buffer_size设置的值,包体还是会写入到磁盘文件中。
(3)存储HTTP头部的内存buffer大小
语法:client_header_buffer_size size;
默认:client_header_buffer_size 1k;
配置块:http、server
上面配置项定义了正常情况下Nginx接收用户请求中HTTP header部分(包括HTTP行和HTTP头部)时分配的内存buffer大小。有时,请求中的HTTP header部分可能会超过这个大小,这时large_client_header_buffers定义的buffer将会生效。
(4)存储HTTP包体的内存buffer大小
语法:client_body_buffer_size size;
默认:client_body_buffer_size 8k/16k;
配置块:http、server、location
上面配置项定义了Nginx接收HTTP包体的内存缓冲区大小。也就是说,HTTP包体会先接收到指定的这块缓存中,之后才决定是否写入磁盘。
注意:如果用户请求中含有HTTP头部Content-Length,并且其标识的长度小于定义的buffer大小,那么Nginx会自动降低本次请求所使用的内存buffer,以降低内存消耗。
(5)connection_pool_size
语法:connection_pool_size size;
默认:connection_pool_size 256;
配置块:http、server
Nginx对于每个建立成功的TCP连接会预先分配一个内存池,上面的size配置项将指定这个内存池的初始大小(即ngx_connection_t结构体中的pool内存池初始大小,9.8.1节将介绍这个内存池是何时分配的),用于减少内核对于小块内存的分配次数。需慎重设置,因为更大的size会使服务器消耗的内存增多,而更小的size则会引发更多的内存分配次数。
(6)request_pool_size
语法:request_pool_size size;
默认:request_pool_size 4k;
配置块:http、server
Nginx开始处理HTTP请求时,将会为每个请求都分配一个内存池,size配置项将指定这个内存池的初始大小(即ngx_http_request_t结构体中的pool内存池初始大小,11.3节将介绍这个内存池是何时分配的),用于减少内核对于小块内存的分配次数。TCP连接关闭时会销毁connection_pool_size指定的连接内存池,HTTP请求结束时会销毁request_pool_size指定的HTTP请求内存池,但它们的创建、销毁时间并不一致,因为一个TCP连接可能被复用于多个HTTP请求。
网络连接的设置
(1)读取HTTP头部的超时时间
语法:client_header_timeout time(默认单位:秒);
默认:client_header_timeout 60;
配置块:http、server、location
客户端与服务器建立连接后将开始接收HTTP头部,在这个过程中,如果在一个时间间隔(超时时间)内没有读取到客户端发来的字节,则认为超时,并向客户端返回408(“Request timed out”)响应。
(2)读取HTTP包体的超时时间
语法:client_body_timeout time(默认单位:秒);
默认:client_body_timeout 60;
配置块:http、server、location
此配置项与client_header_timeout相似,只是这个超时时间只在读取HTTP包体时才有效。
(3)发送响应的超时时间
语法:send_timeout time;
默认:send_timeout 60;
配置块:http、server、location
这个超时时间是发送响应的超时时间,即Nginx服务器向客户端发送了数据包,但客户端一直没有去接收这个数据包。如果某个连接超过send_timeout定义的超时时间,那么Nginx将会关闭这个连接。
(4)reset_timeout_connection
语法:reset_timeout_connection on|off;
默认:reset_timeout_connection off;
配置块:http、server、location
连接超时后将通过向客户端发送RST包来直接重置连接。这个选项打开后,Nginx会在某个连接超时后,不是使用正常情形下的四次握手关闭TCP连接,而是直接向用户发送RST重置包,不再等待用户的应答,直接释放Nginx服务器上关于这个套接字使用的所有缓存(如TCP滑动窗口)。相比正常的关闭方式,它使得服务器避免产生许多处于FIN_WAIT_1、FIN_WAIT_2、TIME_WAIT状态的TCP连接。
注意,使用RST重置包关闭连接会带来一些问题,默认情况下不会开启。
(5)对某些浏览器禁用keepalive功能
语法:keepalive_disable[msie6|safari|none]…
默认:keepalive_disablemsie6 safari
配置块:http、server、location
HTTP请求中的keepalive功能是为了让多个请求复用一个HTTP长连接,这个功能对服务器的性能提高是很有帮助的。但有些浏览器,如IE 6和Safari,它们对于使用keepalive功能的POST请求处理有功能性问题。因此,针对IE 6及其早期版本、Safari浏览器默认是禁用keepalive功能的。
(6)keepalive超时时间
语法:keepalive_timeout time(默认单位:秒);
默认:keepalive_timeout 75;
配置块:http、server、location
一个keepalive连接在闲置超过一定时间后(默认的是75秒),服务器和浏览器都会去关闭这个连接。当然,keepalive_timeout配置项是用来约束Nginx服务器的,Nginx也会按照规范把这个时间传给浏览器,但每个浏览器对待keepalive的策略有可能是不同的。
(7)一个keepalive长连接上允许承载的请求最大数
语法:keepalive_requests n;
默认:keepalive_requests 100;
配置块:http、server、location
一个keepalive连接上默认最多只能发送100个请求。
对客户端请求的限制
(1)按HTTP方法名限制用户请求
语法:limit_except method…{…}
配置块:location
Nginx通过limit_except后面指定的方法名来限制用户请求。方法名可取值包括:GET、HEAD、POST、PUT、DELETE、MKCOL、COPY、MOVE、OPTIONS、PROPFIND、PROPPATCH、LOCK、UNLOCK或者PATCH。例如:
limit_except GET {
allow 192.168.1.0/32;
deny all;
}
注意,允许GET方法就意味着也允许HEAD方法。因此,上面这段代码表示的是禁止GET方法和HEAD方法,但其他HTTP方法是允许的。
(2)HTTP请求包体的最大值
语法:client_max_body_size size;
默认:client_max_body_size 1m;
配置块:http、server、location
浏览器在发送含有较大HTTP包体的请求时,其头部会有一个Content-Length字段,client_max_body_size是用来限制Content-Length所示值的大小的。因此,这个限制包体的配置非常有用处,因为不用等Nginx接收完所有的HTTP包体——这有可能消耗很长时间——就可以告诉用户请求过大不被接受。例如,用户试图上传一个10GB的文件,Nginx在收完包头后,发现Content-Length超过client_max_body_size定义的值,就直接发送413(“Request Entity Too Large”)响应给客户端。
(3)对请求的限速
语法:limit_rate speed;
默认:limit_rate 0;
配置块:http、server、location、if
此配置是对客户端请求限制每秒传输的字节数。speed可以使用2.2.4节中提到的多种单位,默认参数为0,表示不限速。
针对不同的客户端,可以用$limit_rate参数执行不同的限速策略。例如:
server {
if ($slow) {
set $limit_rate 4k;
}
}
用HTTP proxy module配置一个反向代理服务
反向代理(reverse proxy)方式是指用代理服务器来接受Internet上的连接请求,然后将请求转发给内部网络中的上游服务器,并将从上游服务器上得到的结果返回给Internet上请求连接的客户端,此时代理服务器对外的表现就是一个Web服务器。充当反向代理服务器也是Nginx的一种常见用法(反向代理服务器必须能够处理大量并发请求),本节将介绍Nginx作为HTTP反向代理服务器的基本用法。
由于Nginx具有“强悍”的高并发高负载能力,因此一般会作为前端的服务器直接向客户端提供静态文件服务。但也有一些复杂、多变的业务不适合放到Nginx服务器上,这时会用Apache、Tomcat等服务器来处理。于是,Nginx通常会被配置为既是静态Web服务器也是反向代理服务器(如图2-3所示),不适合Nginx处理的请求就会直接转发到上游服务器中处理。
(作为静态Web服务器与反向代理服务器的Nginx)
Squid等其他反向代理服务器相比,Nginx的反向代理功能有自己的特点,如下图所示。
(Nginx作为反向代理服务器时转发请求的流程)
当客户端发来HTTP请求时,Nginx并不会立刻转发到上游服务器,而是先把用户的请求(包括HTTP包体)完整地接收到Nginx所在服务器的硬盘或者内存中,然后再向上游服务器发起连接,把缓存的客户端请求转发到上游服务器。而Squid等代理服务器则采用一边接收客户端请求,一边转发到上游服务器的方式。
Nginx的这种工作方式有什么优缺点呢?很明显,缺点是延长了一个请求的处理时间,并增加了用于缓存请求内容的内存和磁盘空间。而优点则是降低了上游服务器的负载,尽量把压力放在Nginx服务器上。
Nginx的这种工作方式为什么会降低上游服务器的负载呢?通常,客户端与代理服务器之间的网络环境会比较复杂,多半是“走”公网,网速平均下来可能较慢,因此,一个请求可能要持续很久才能完成。而代理服务器与上游服务器之间一般是“走”内网,或者有专线连接,传输速度较快。Squid等反向代理服务器在与客户端建立连接且还没有开始接收HTTP包体时,就已经向上游服务器建立了连接。例如,某个请求要上传一个1GB的文件,那么每次Squid在收到一个TCP分包(如2KB)时,就会即时地向上游服务器转发。在接收客户端完整HTTP包体的漫长过程中,上游服务器始终要维持这个连接,这直接对上游服务器的并发处理能力提出了挑战。
Nginx则不然,它在接收到完整的客户端请求(如1GB的文件)后,才会与上游服务器建立连接转发请求,由于是内网,所以这个转发过程会执行得很快。这样,一个客户端请求占用上游服务器的连接时间就会非常短,也就是说,Nginx的这种反向代理方案主要是为了降低上游服务器的并发压力。
Nginx将上游服务器的响应转发到客户端有许多种方法,第12章将介绍其中常见的两种方式。
负载均衡的基本配
作为代理服务器,一般都需要向上游服务器的集群转发请求。这里的负载均衡是指选择一种策略,尽量把请求平均地分布到每一台上游服务器上。下面介绍负载均衡的配置项。
(1)upstream块
语法:upstream name{…}
配置块:http
upstream块定义了一个上游服务器的集群,便于反向代理中的proxy_pass使用。例如:
upstream backend {
server backend1.example.com;
server backend2.example.com;
server backend3.example.com;
}
server {
location / {
proxy_pass http://backend;
}
}
(2)server
语法:server name[parameters];
配置块:upstream
server配置项指定了一台上游服务器的名字,这个名字可以是域名、IP地址端口、UNIX句柄等,在其后还可以跟下列参数。
- weight=number:设置向这台上游服务器转发的权重,默认为1。
- max_fails=number:该选项与fail_timeout配合使用,指在fail_timeout时间段内,如果向当前的上游服务器转发失败次数超过number,则认为在当前的fail_timeout时间段内这台上游服务器不可用。max_fails默认为1,如果设置为0,则表示不检查失败次数.
- fail_timeout=time:fail_timeout表示该时间段内转发失败多少次后就认为上游服务器暂时不可用,用于优化反向代理功能。它与向上游服务器建立连接的超时时间、读取上游服务器的响应超时时间等完全无关。fail_timeout默认为10秒。
- down:表示所在的上游服务器永久下线,只在使用ip_hash配置项时才有用。
- backup:在使用ip_hash配置项时它是无效的。它表示所在的上游服务器只是备份服务器,只有在所有的非备份上游服务器都失效后,才会向所在的上游服务器转发请求。
例如:
upstream backend {
server backend1.example.com weight=5;
server 127.0.0.1:8080 max_fails=3 fail_timeout=30s;
server unix:/tmp/backend3;
}
(3)ip_hash
语法:ip_hash;
配置块:upstream
在有些场景下,我们可能会希望来自某一个用户的请求始终落到固定的一台上游服务器中。例如,假设上游服务器会缓存一些信息,如果同一个用户的请求任意地转发到集群中的任一台上游服务器中,那么每一台上游服务器都有可能会缓存同一份信息,这既会造成资源的浪费,也会难以有效地管理缓存信息。ip_hash就是用以解决上述问题的,它首先根据客户端的IP地址计算出一个key,将key按照upstream集群里的上游服务器数量进行取模,然后以取模后的结果把请求转发到相应的上游服务器中。这样就确保了同一个客户端的请求只会转发到指定的上游服务器中。
ip_hash与weight(权重)配置不可同时使用。如果upstream集群中有一台上游服务器暂时不可用,不能直接删除该配置,而是要down参数标识,确保转发策略的一贯性。例如:
upstream backend {
ip_hash;
server backend1.example.com;
server backend2.example.com;
server backend3.example.com down;
server backend4.example.com;
}
反向代理的基本配置
下面介绍反向代理的基本配置项。
(1)proxy_pass
语法:proxy_pass URL;
配置块:location、if
此配置项将当前请求反向代理到URL参数指定的服务器上,URL可以是主机名或IP地址加端口的形式,例如:
proxy_pass http://localhost:8000/uri/;
也可以是UNIX句柄:
proxy_pass http://unix:/path/to/backend.socket:/uri/;
还可以如上节负载均衡中所示,直接使用upstream块,例如:
upstream backend {
…
}
server {
location / {
proxy_pass http://backend;
}}
用户可以把HTTP转换成更安全的HTTPS,例如:
proxy_pass https://192.168.0.1;
默认情况下反向代理是不会转发请求中的Host头部的。如果需要转发,那么必须加上配置:
proxy_set_header Host $host;
(2)proxy_method
语法:proxy_method method;
配置块:http、server、location
此配置项表示转发时的协议方法名。例如设置为:
proxy_method POST;
那么客户端发来的GET请求在转发时方法名也会改为POST。
(3)proxy_hide_header
语法:proxy_hide_header the_header;
配置块:http、server、location
Nginx会将上游服务器的响应转发给客户端,但默认不会转发以下HTTP头部字段:Date、Server、X-Pad和X-Accel-*。使用proxy_hide_header后可以任意地指定哪些HTTP头部字段不能被转发。例如:
proxy_hide_header Cache-Control;
proxy_hide_header MicrosoftOfficeWebServer;
(4)proxy_pass_header
语法:proxy_pass_header the_header;
配置块:http、server、location
与proxy_hide_header功能相反,proxy_pass_header会将原来禁止转发的header设置为允许转发。例如:
proxy_pass_header X-Accel-Redirect;
(5)proxy_pass_request_body
语法:proxy_pass_request_body on|off;
默认:proxy_pass_request_body on;
配置块:http、server、location
作用为确定是否向上游服务器发送HTTP包体部分。
(6)proxy_pass_request_headers
语法:proxy_pass_request_headers on|off;
默认:proxy_pass_request_headers on;
配置块:http、server、location
作用为确定是否转发HTTP头部。
(7)proxy_redirect
语法:proxy_redirect[default|off|redirect replacement];
默认:proxy_redirect default;
配置块:http、server、location
当上游服务器返回的响应是重定向或刷新请求(如HTTP响应码是301或者302)时,proxy_redirect可以重设HTTP头部的location或refresh字段。例如,如果上游服务器发出的响应是302重定向请求,location字段的URI是http://localhost:8000/two/some/uri/,那么在下面的配置情况下,实际转发给客户端的location是http://frontend/one/some/uri/。
proxy_redirect http://localhost:8000/two/ http://frontend/one/;
(这里其实就是把客户端过来的请求中的第一部分http://localhost:8000/two换成是第二部分http://frontend/one/)
这里还可以使用ngx-http-core-module提供的变量来设置新的location字段。例如:
proxy_redirect http://localhost:8000/ http://$host:$server_port/;
也可以省略replacement参数中的主机名部分,这时会用虚拟主机名称来填充。例如:
proxy_redirect http://localhost:8000/two//one/;
使用off参数时,将使location或者refresh字段维持不变。例如:
proxy_redirect off;
使用默认的default参数时,会按照proxy_pass配置项和所属的location配置项重组发往客户端的location头部。例如,下面两种配置效果是一样的:
location /one/ {
proxy_pass http://upstream:port/two/;
proxy_redirect default;
}
location /one/ {
proxy_pass http://upstream:port/two/;
proxy_redirect http://upstream:port/two//one/;
}
(8)proxy_next_upstream
语法:proxy_next_upstream[error|timeout|invalid_header|http_500|http_502|http_503|http_504|http_404|off];
默认:proxy_next_upstream error timeout;
配置块:http、server、location
此配置项表示当向一台上游服务器转发请求出现错误时,继续换一台上游服务器处理这个请求。前面已经说过,上游服务器一旦开始发送应答,Nginx反向代理服务器会立刻把应答包转发给客户端。因此,一旦Nginx开始向客户端发送响应包,之后的过程中若出现错误也是不允许换下一台上游服务器继续处理的。这很好理解,这样才可以更好地保证客户端只收到来自一个上游服务器的应答。proxy_next_upstream的参数用来说明在哪些情况下会继续选择下一台上游服务器转发请求。
- error:当向上游服务器发起连接、发送请求、读取响应时出错。
- timeout:发送请求或读取响应时发生超时。
- invalid_header:上游服务器发送的响应是不合法的
- http_500:上游服务器返回的HTTP响应码是500。
- http_502:上游服务器返回的HTTP响应码是502。
- http_503:上游服务器返回的HTTP响应码是503。
- http_504:上游服务器返回的HTTP响应码是504。
- http_404:上游服务器返回的HTTP响应码是404。
- off:关闭proxy_next_upstream功能—出错就选择另一台上游服务器再次转发。
Nginx的反向代理模块还提供了很多种配置,如设置连接的超时时间、临时文件如何存储,以及最重要的如何缓存上游服务器响应等功能。这些配置可以通过阅读ngx_http_proxy_module模块的说明了解,只有深入地理解,才能实现一个高性能的反向代理服务器。本节只是介绍反向代理服务器的基本功能,在第12章中我们将会深入地探索upstream机制,到那时,读者也许会发现ngx_http_proxy_module模块只是使用upstream机制实现了反向代理功能而已。
小结
Nginx由少量的核心框架代码和许多模块组成,每个模块都有它独特的功能。因此,读者可以通过查看每个模块实现了什么功能,来了解Nginx可以帮我们做些什么。
Nginx的Wiki网站(http://wiki.nginx.org/Modules)上列出了官方提供的所有模块及配置项,仔细观察就会发现,这些配置项的语法与本章的内容都是很相近的,读者只需要弄清楚模块说明中每个配置项的意义即可。另外,网页http://wiki.nginx.org/3rdPartyModules中列出了Wiki上已知的几十个第三方模块,同时读者还可以从搜索引擎上搜索到更多的第三方模块。了解每个模块的配置项用法,并在Nginx中使用这些模块,可以让Nginx做到更多。
随着对本书的学习,读者会对Nginx模块的设计思路有深入的了解,也会渐渐熟悉如何编写一个模块。如果某个模块的实现与你的想法有出入,可以更改这个模块的源码,实现你期望的业务功能。如果所有的模块都没有你想要的功能,不妨自己重写一个定制的模块,也可以申请发布到Nginx网站上供大家分享。
开发一个简单的HTTP模块
当通过开发HTTP模块来实现产品功能时,是可以完全享用Nginx的优秀设计所带来的、与官方模块相同的高并发特性的。不过,如何开发一个充满异步调用、无阻塞的HTTP模块呢?首先,需要把程序嵌入到Nginx中,也就是说,最终编译出的二进制程序Nginx要包含我们的代码;其次,这个全新的HTTP模块要能介入到HTTP请求的处理流程中。满足上述两个前提后,我们的模块才能开始处理HTTP请求,但在开始处理请求前还需要先了解一些Nginx框架定义的数据结构,这是后面必须要用到的;正式处理请求时,还要可以获得Nginx框架接收、解析后的用户请求信息;业务执行完毕后,则要考虑发送响应给用户,包括将磁盘中的文件以HTTP包体的形式发送给用户。
如何调用HTTP模
在开发HTTP模块前,首先需要了解典型的HTTP模块是如何介入Nginx处理用户请求流程的。图3-1是一个简化的时序图,这里省略了许多异步调用,忽略了多个不同的HTTP处理阶段,仅标识了在一个典型请求的处理过程中主要模块被调用的流程,以此帮助读者理解HTTP模块如何处理用户请求。完整的流程将在第11章中详细介绍。
(图3-1 Nginx HTTP模块调用的简化流)
从图3-1中看到,worker进程会在一个for循环语句里反复调用事件模块检测网络事件。当事件模块检测到某个客户端发起的TCP请求时(接收到SYN包),将会为它建立TCP连接,成功建立连接后根据nginx.conf文件中的配置会交由HTTP框架处理。HTTP框架会试图接收完整的HTTP头部,并在接收到完整的HTTP头部后将请求分发到具体的HTTP模块中处理。这种分发策略是多样化的,其中最常见的是根据请求的URI和nginx.conf里location配置项的匹配度来决定如何分发(本章的例子正是应用这种分发策略,在第10章中会介绍其他分发策略)。HTTP模块在处理请求的结束时,大多会向客户端发送响应,此时会自动地依次调用所有的HTTP过滤模块,每个过滤模块可以根据配置文件决定自己的行为。例如,gzip过滤模块根据配置文件中的gzip on|off来决定是否压缩响应。HTTP处理模块在返回时会将控制权交还给HTTP框架,如果在返回前设置了subrequest,那么HTTP框架还会继续异步地调用适合的HTTP模块处理子请求。
开发HTTP模块时,首先要注意的就是HTTP框架到具体的HTTP模块间数据流的传递,以及开发的HTTP模块如何与诸多的过滤模块协同工作(第10章、第11章会详细介绍HTTP框架)。下面正式进入HTTP模块的开发环节。
整型的封装
Nginx使用ngx_int_t封装有符号整型,使用ngx_uint_t封装无符号整型。Nginx各模块的变量定义都是如此使用的,建议读者沿用Nginx的习惯,以此替代int和unsingedint。
在Linux平台下,Nginx对ngx_int_t和ngx_uint_t的定义如下:
typedef intptr_t ngx_int_t;
typedef uintptr_t ngx_uint_t;
ngx_str_t数据结构
Nginx的领域中,ngx_str_t结构就是字符串。ngx_str_t的定义如下:
typedef struct {
size_t len;
u_char *data;
} ngx_str_t;
ngx_str_t只有两个成员,其中data指针指向字符串起始地址,len表示字符串的有效长度。
ngx_table_elt_t数据结构
ngx_table_elt_t数据结构如下所示:
typedef struct {
ngx_uint_t hash;
ngx_str_t key;
ngx_str_t value;
u_char *lowcase_key;
} ngx_table_elt_t;
可以看到,ngx_table_elt_t就是一个key/value对,ngx_str_t类型的key、value成员分别存储的是名字、值字符串。hash成员表明ngx_table_elt_t也可以是某个散列表数据结构(ngx_hash_t类型)中的成员。ngx_uint_t类型的hash成员可以在ngx_hash_t中更快地找到相同key的ngx_table_elt_t数据。lowcase_key指向的是全小写的key字符串。
显而易见,ngx_table_elt_t是为HTTP头部“量身订制”的,其中key存储头部名称(如Content-Length),value存储对应的值(如“1024”),lowcase_key是为了忽略HTTP头部名称的大小写(例如,有些客户端发来的HTTP请求头部是content-length,Nginx希望它与大小写敏感的Content-Length做相同处理,有了全小写的lowcase_key成员后就可以快速达成目的了),hash用于快速检索头部。
如何将自己的HTTP模块编译进Nginx
Nginx提供了一种简单的方式将第三方的模块编译到Nginx中。首先把源代码文件全部放到一个目录下,同时在该目录中编写一个文件用于通知Nginx如何编译本模块,这个文件名必须为config。
这样,只要在configure脚本执行时加入参数–add-module=PATH(PATH就是上面我们给定的源代码、config文件的保存目录),就可以在执行正常编译安装流程时完成Nginx编译工作。
有时,Nginx提供的这种方式可能无法满足我们的需求,其实,在执行完configure脚本后Nginx会生成objs/Makefile和objs/ngx_modules.c文件,完全可以自己去修改这两个文件,这是一种更强大也复杂得多的方法。
配置、error日志和请求上下文
在开发功能灵活的Nginx模块时,需要从配置文件中获取特定的信息,不过,不需要再编写一套读取配置的系统,Nginx已经为用户提供了强大的配置项解析机制,同时它还支持“-s reload”命令——在不重启服务的情况下可使配置生效。
http配置项的使用场景
在第2章中通过多样化修改nginx.conf文件中的配置项,实现了复杂的Web服务器功能。其中,http{…}内的配置项最为复杂,在http配置块内还有server块、location块等,同一个配置项可以同时出现在多个http块、server块或location块内。
那么,如何解析这样的配置项呢?在第3章中的mytest例子中,又是怎样获取nginx.conf中的配置的呢?当同一个配置在http块、server块、location块中同时出现时,应当选择哪一个块下的配置呢?当多个不同URI表达式下的location都配置了mytest这个配置项,然而后面的参数值却不同时,Nginx是如何处理的呢?这些就是本章将要回答的问题。
我们先来看一个例子,有一个配置项test_str,它在多个块内都出现了,如下所示。
在上面的配置文件中,test_str这个配置项在http块内出现的值为main,在监听80端口的server块内test_str值为server80,该server块内有两个location都是由第3章中定义的mytest模块处理的,而且每个location中又重新设置了test_str的值,分别为loc1和loc2。在这之后又定义了监听8080端口的server块,并重定义test_str的值为server8080,这个server块内定义的一个location也是由mytest模块处理的,而且这个location内再次重定义了test_str的值为loc3。(事实上不只是例子中的server块可以嵌套location块,location块之间还可以继续嵌套,这样test_str的值就更复杂了,上例中没有出现location中进一步反复嵌套location的场景。在4.3.3节讨论HTTP框架如何合并配置项时涉及了location块的反复嵌套问题,请读者注意。)
在这段很短的配置中,mytest模块将会处理两个监听端口上建立的TCP连接,以及3种HTTP请求,请求的URL分别对应着/url1、/url2、/url3。假设mytest模块必须取出test_str配置项的参数,可是在以上的例子中test_str出现了6个不同的参数值,分别为main、server80、server8080、loc1、loc2、loc3,那么在mytest模块中我们取到的test_str值以哪一个为准呢?
事实上,Nginx的设计是非常灵活的(实际上这是第10章将要介绍的HTTP框架设计的),它在每一个http块、server块或location块下,都会生成独立的数据结构来存放配置项。因此,我们允许当用户访问的请求不同时(如请求的URL分别是/url1、/url2、/url3),配置项test_str可以具有不同的值。那么,当请求是/url1时,test_str的值应当是location块下的loc1,还是这个location所属的server块下的server80,又或者是其所属http块下的值main呢?完全由mytest模块自己决定,我们可以定义这个行为。
怎样使用http配置
处理http配置项可以分为下面4个步骤:
- 创建数据结构用于存储配置项对应的参数。
- 定配置项在nginx.conf中出现时的限制条件与回调方法。
- 实现第2步中的回调方法,或者使用Nginx框架预设的14个回调方法。
- 合并不同级别的配置块中出现的同名配置项。
不过,这4个步骤如何与Nginx有机地结合起来呢?就是通过第3章中介绍过的两个数据结构ngx_http_module_t和ngx_command_t,它们都是定义一个HTTP模块时不可或缺的部分。