第3章 Nginx服务器架构初探
通过前面两章的介绍,相信大家对Nginx服务器的来历发展、功能服务有了大致的认识,同时也掌握了初步使用Nginx服务器的一些知识。但是,在进行了一系列应用实践后,相信读者会发现自己对前面的相关知识产生了似懂非懂的感觉,一方面可以比较熟练地使用Nginx服务器提供基本服务,另一方面却没有很清晰的思路讲清楚Nginx服务器是怎样完成强大的Web服务功能的。那么,在这一章中,我们将首次接触Nginx服务器的架构设计,它将作为我们深入探索Nginx世界的开端。
在本章中,我们将学习到以下主要内容:
■ 模块化结构的相关知识。
■ Nginx如何处理Web请求。
■ Nginx的事件驱动模型。
■ Nginx设计架构的概览。
3.1 模块化结构
从初学者到资深程序员,对于“模块化”概念的理解可谓仁者见仁、智者见智,市面上也有不少书籍专门研究相关的理论和应用。“模块化”思想不是本书讨论的重点,但是谈到Nginx服务器的架构,就不能不谈到它。于是,在本章伊始,我们先使用较少的篇幅对模块化简单介绍一下,作为后面深入介绍Nginx架构的预备知识,如果想深入了解的话则可以进一步学习模块化设计相关的知识。
3.1.1 什么是“模块化设计”
到底什么是“模块化设计”呢?还是那句老话,“没有统一的定义”。在20世纪50年代,欧美一些国家正式提出“模块化设计”的概念,把模块化设计提到理论高度来研究。目前,模块化设计的思想已渗透到许多领域。在计算机领域,针对程序设计,常见的说法是把“模块化设计”定义为“以功能块为单位进行程序设计,实现其求解算法的方法”。从这个一般提法中,我们能够看到几层意思。
第一,“功能块”是对模块的描述,一个模块就是一个功能块,应该只负责一个功能,在设计模式理论中类似于经常提到的“单一职责原则”。
第二,如果要体现模块化,就免不了将程序进行分解,这也是模块化编程的另一个原则——自顶向下,逐步求精原则。
第三,一个程序被分解为多个模块,那么它们之间一定要存在一定的依赖关系,但是这个依赖不能太强,否则也就不能称之为“模块化”了。于是,又涉及到模块化编程的一条原则:高内聚、低耦合原则。事实上,在设计模式理论中,也有对应的一条设计原则叫“迪米特原则”。
这个提法把“模块化编程”定义为程序设计的一种方法,这种方法的结果是用一系列以功能块为单位的算法来描述和实现程序。相对于这个一般提法,笔者更认为“模块化”是一种思想,它将人们在日常生活中处理事情的习惯性思维应用到了程序设计当中,这是程序设计历史上一次里程碑式的思想飞跃。接触过面向对象理论的读者对此应该深有体会,也更能够理解模块化编程的意义所在。
模块化设计支持分布式开发。模块化的思想导致大量功能独立的模块出现,这些模块可以分布在世界上的任何角落。无论是开发中小型的应用程序,还是构建大型的诸如操作系统之类的程序,都可以采用分布式开发模型,集合任何可用模块为己所用。
模块化设计支持团队协同合作。在采用模块化思想进行程序设计时,最终的产品由小的、分散的功能块组成,每一块都是基本独立的。这些功能块可以由不同的团队根据他们自己的时间表和生命周期进行开发,互不影响。最终的产品则可以由另一个独立的个体——发行者进行集成。
模块化设计支持应用扩展和升级。应用模块化思想设计出来的程序,就如同用积木搭起来的房子。各个模块之间既能保持自己的独立性,也能通过接口保持联系。在对应用扩展时,只要实现规范的接口,就可以不断加入新功能;在对其中某些模块进行升级时,只要保持原有接口不变,就能够在不影响其他模块的前提下进行。
本小节主要对“模块化设计”思想的定义、应该遵循的一些原则以及它的应用意义做了简单阐述。Nginx服务器的开发完全遵循模块化设计思想。在本书的后续内容中,相信读者能够深刻感受到这一思想给Nginx带来的巨大优越性,尤其是在应用扩展和升级方面,体现得更加淋漓尽致。
3.1.2 Nginx模块化结构
在前面相关章节中多次提到了Nginx的模块。习惯上将Nginx涉及到的模块分为核心模块、标准HTTP模块、可选HTTP模块、邮件服务模块以及第三方模块等五大类。
核心模块是指Nginx服务器正常运行必不可少的模块,它们提供了Nginx最基本最核心的服务,如进程管理、权限控制、错误日志记录等;标准HTTP模块是通过第2章介绍的方法快速编译Nginx后包含的模块,其支持Nginx服务器的标准HTTP功能;可选HTTP模块主要用于扩展标准的HTTP功能,使其能够处理一些特殊的HTTP请求;邮件服务模块主要用于支持Nginx的邮件服务;第三方模块是为了扩展Nginx服务器应用,完成特殊功能而由第三方机构或者个人编写的可编译到Nginx中的模块。Nginx的每个模块都基本符合单一职责原则,在具体环境中可以根据实际情况裁减和加入。
核心模块和标准HTTP模块在Nginx快速编译后就包含在Nginx中。在Linux系统中,将工作目录定位到第2章中编译Nginx-1.2.3的路径/Nginx_123/nginx-1.2.3下,可以看到objs目录。objs目录中包含了这些内容:
# ls objs/ Makefile autoconf.err nginx nginx.8 ngx_auto_config.h ngx_auto_headers.h ngx_modules.c ngx_modules.o src
在此目录中的ngx_modules.c文件中包含了此版本Nginx快速编译后包括的所有固有模块的声明。这些模块声明以extern关键字修饰:
# cat ngx_modules.c|grep extern|cat -n extern ngx_module_t ngx_core_module; extern ngx_module_t ngx_errlog_module; extern ngx_module_t ngx_conf_module; extern ngx_module_t ngx_events_module; extern ngx_module_t ngx_event_core_module; extern ngx_module_t ngx_epoll_module; extern ngx_module_t ngx_regex_module; extern ngx_module_t ngx_http_module; extern ngx_module_t ngx_http_core_module; extern ngx_module_t ngx_http_log_module; extern ngx_module_t ngx_http_upstream_module; extern ngx_module_t ngx_http_static_module; extern ngx_module_t ngx_http_autoindex_module; extern ngx_module_t ngx_http_index_module; extern ngx_module_t ngx_http_auth_basic_module; extern ngx_module_t ngx_http_access_module; extern ngx_module_t ngx_http_limit_conn_module; extern ngx_module_t ngx_http_limit_req_module; extern ngx_module_t ngx_http_geo_module; extern ngx_module_t ngx_http_map_module; extern ngx_module_t ngx_http_split_clients_module; extern ngx_module_t ngx_http_referer_module; extern ngx_module_t ngx_http_rewrite_module; extern ngx_module_t ngx_http_proxy_module; extern ngx_module_t ngx_http_fastcgi_module; extern ngx_module_t ngx_http_uwsgi_module; extern ngx_module_t ngx_http_scgi_module; extern ngx_module_t ngx_http_memcached_module; extern ngx_module_t ngx_http_empty_gif_module; extern ngx_module_t ngx_http_browser_module; extern ngx_module_t ngx_http_upstream_ip_hash_module; extern ngx_module_t ngx_http_upstream_least_conn_module; extern ngx_module_t ngx_http_upstream_keepalive_module; extern ngx_module_t ngx_http_write_filter_module;
extern ngx_module_t ngx_http_header_filter_module; extern ngx_module_t ngx_http_chunked_filter_module; extern ngx_module_t ngx_http_range_header_filter_module; extern ngx_module_t ngx_http_gzip_filter_module; extern ngx_module_t ngx_http_postpone_filter_module; extern ngx_module_t ngx_http_ssi_filter_module; extern ngx_module_t ngx_http_charset_filter_module; extern ngx_module_t ngx_http_userid_filter_module; extern ngx_module_t ngx_http_headers_filter_module; extern ngx_module_t ngx_http_copy_filter_module; extern ngx_module_t ngx_http_range_body_filter_module; extern ngx_module_t ngx_http_not_modified_filter_module;
由于使用extern关键字修饰,因此各模块均可以被其他模块访问。
这里简要说明一下Nginx中模块的命名习惯。一般以ngx_作为前缀,_module作为后缀,中间使用一个或者多个英文单词描述模块的功能。比如ngx_core_module,中间的core表明该模块提供了Nginx程序的核心功能;再如ngx_events_module,中间的events表明该模块提供了解析配置文件中events块的功能;再如ngx_http_core_module,中间的http_core表明该模块提供了Nginx程序http服务的核心功能,等等。了解了Nginx中模块的命名习惯,再阅读上面列出的模块,就可以大致了解Nginx服务器在发布时能提供的主要服务了。
所有固有模块的源码放在编译目录下的src目录中。在src目录中,我们看到一共分成了core、event、http、mail、misc和os等6个目录。从这里看到,源码中包含了邮件服务的模块,但在快速编译时默认不将其编译到Nginx中。
1. 核心模块
3.1.2小节加粗部分的模块提供Nginx的核心功能。详细来说,核心模块主要包含对两类功能的支持,一类是主体功能,包括进程管理、权限控制、错误日志记录、配置解析等,另一类是用于响应请求事件必需的功能,包括事件驱动机制、正则表达式解析等。
2 .标准HTTP模块
在第1章中,我们提到Nginx服务器主要提供基本HTTP服务、高级HTTP服务和邮件服务等。这一模块对应于基本HTTP服务。
这些模块在默认情况下是被编译到Nginx中的,除非在配置时添加--without-XXX参数声明不编译。在表3.1中,笔者对上面列表中比较重要的标准HTTP模块进行了梳理并添加了说明,方便读者查询和理解。
表3.1 常用标准HTTP模块
对于其他未涉及的标准HTTP模块,有兴趣的读者可以访问Nginx的官方网站查找相关内容。
3. 可选HTTP模块
可选HTTP模块在目前的Nginx发行版本中只提供源码,但在快速编译时默认不编译。如果想使用相关模块,就必须在配置时使用--with-XXX参数声明。
在表3.2中,笔者对常见的可选HTTP模块进行了整理和说明,便于读者查询。
表3.2 常用可选HTTP模块
4. 邮件服务模块
在第1章中提到,邮件服务是Nginx服务器提供的主要服务之一。但是在目前的Nginx发行版本中,快速编译时默认并不会编译邮件服务模块。
和Nginx服务器提供的邮件服务相关的模块有:
■ ngx_mail_core_module
■ ngx_mail_pop3_module
■ ngx_mail_imap_module
■ ngx_mail_smtp_module
■ ngx_mail_auth_http_module
■ ngx_mail_proxy_module
■ ngx_mail_ssl_module
这些模块完成了邮件服务的主要功能,包括对POP3协议、IMAP协议和SMTP协议的支持,对身份认证、邮件代理和SSL安全服务的提供。
5. 第三方模块
由于Nginx支持自定义模块编程,第三方模块不断得到扩充,功能也非常丰富。目前,记录在wiki站点的就多达90个,而且还有一些模块是没有包含在内的。对于繁多的第三方模块,我们不打算在这里一一列举,读者可以根据自己的需要从wiki站点自行查找。
在第三方模块的开发作者中,深受广大Nginx用户推崇的要算是一位笔名为agentzh的工程师了。他开发的echo-nginx-module模块(支持在Nginx配置文件中使用echo、sleep、time及exec等类Shell命令)、memc-nginx-module模块(对标准HTTP模块ngx_http_memcached_module的扩展,支持set、add、delete等更多的命令)、rds-json-nginx-module模块(使Nginx支持Json数据的处理)、lua-nginx-module模块(使Nginx支持lua脚本语言)等都是笔者在日常工作中经常使用的。有兴趣的读者可以到wiki站点下载使用。
到此,我们介绍完了Nginx的五大类模块,大家对前面四种包含的具体模块及其功能有了比较清晰的认识。从前面的内容我们能够看出,Nginx服务器在功能定制和扩展上具有其他Web服务器无法媲美的巨大优势,从核心功能到一般功能,再到扩展功能,几乎都可以使用“模块化”技术实现。
那么,这些模块彼此是如何组织在一起,为Nginx服务器提供支持的呢?从下一节开始,我们更进一步从Nginx服务器的设计架构出发,来全面了解各个模块之间的联系。
3.2 Nginx服务器的Web请求处理机制
从设计架构上来说,Nginx服务器是与众不同的。不同之处一方面体现在它的模块化设计,另一方面,也是更重要的一方面,体现在它对客户端请求的处理机制上。
Web服务器和客户端是一对多的关系,Web服务器必须有能力同时为多个客户端提供服务。一般来说,完成并行处理请求工作有三种方式可供选择:多进程方式、多线程方式和异步方式。
3.2.1 多进程方式
多进程方式是指,服务器每当接收到一个客户端时,就由服务器主进程生成一个子进程出来和该客户端建立连接进行交互,直到连接断开,该子进程就结束了。
多进程方式的优点在于,设计和实现相对简单,各个子进程之间相互独立,处理客户端请求的过程彼此不受到干扰,并且当一个子进程产生问题时,不容易将影响漫延到其他进程中,这保证了提供服务的稳定性。当子线程退出时,其占用资源会被操作系统回收,也不会留下任何垃圾。而其缺点也是很明显的。操作系统中生成一个子进程需要进行内存复制等操作,在资源和时间上会产生一定的额外开销,因此,如果Web服务器接收大量并发请求,就会对系统资源造成压力,导致系统性能下降。
初期的Apache服务器就是采用这种方式对外提供服务的。为了应对大量并发请求,Apache服务器采用“预生成进程”的机制对多进程方式进行了改进。“预生成进程”的工作方式很好理解。它将生成子进程的时机提前,在客户端请求还没有到来之前就预先生成好,当请求到来时,主进程分配一个子进程和该客户端进行交互,交互完成之后,该进程也不结束,而被主进程管理起来等待下一个客户端请求的到来。改进的多进程方式在一定程度上缓解了大量并发请求情形下Web服务器对系统资源造成的压力。但是由于Apache服务器在最初的架构设计上采用了多进程方式,因此这不能从根本上解决问题。
3.2.2 多线程方式
多线程方式和多进程方式相似,它是指,服务器每当接收到一个客户端时,会由服务器主进程派生一个线程出来和该客户端进行交互。
由于操作系统产生一个线程的开销远远小于产生一个进程的开销,所以多线程方式在很大程度上减轻了Web服务器对系统资源的要求。该方式使用线程进行任务调度,开发方面可以遵循一定的标准,这相对来说比较规范和有利于协作。但在线程管理方面,该方式有一定的不足。多个线程位于同一个进程内,可以访问同样的内存空间,彼此之间相互影响;同时,在开发过程中不可避免地要由开发者自己对内存进行管理,其增加了出错的风险。服务器系统需要长时间连续不停地运转,错误的逐渐积累可能最终对整个服务器产生重大影响。
IIS服务器使用了多线程方式对外提供服务,它的稳定性相对来说还是不错的,但对于经验丰富的Web服务器管理人员而言,他们通常还是会定期检查和重启服务器,以预防不可预料的故障发生。
3.2.3 异步方式
异步方式是和多进程方式及多线程方式完全不同的一种处理客户端请求的方式。在介绍该方式之前,我们先复习一下同步、异步以及阻塞、非阻塞的概念。
网络通信中的同步机制和异步机制是描述通信模式的概念。同步机制,是指发送方发送请求后,需要等待接收到接收方发回的响应后,才接着发送下一个请求;异步机制,和同步机制正好相反,在异步机制中,发送方发出一个请求后,不等待接收方响应这个请求,就继续发送下个请求。在同步机制中,所有的请求在服务器端得到同步,发送方和接收方对请求的处理步调是一致的;在异步机制中,所有来自发送方的请求形成一个队列,接收方处理完成后通知发送方。
阻塞和非阻塞用来描述进程处理调用的方式,在网络通信中,主要指网络套接字Socket的阻塞和非阻塞方式,而Socket的实质也就是IO操作。Socket的阻塞调用方式为,调用结果返回之前,当前线程从运行状态被挂起,一直等到调用结果返回之后,才进入就绪状态,获取CPU后继续执行;Socket的非阻塞调用方式和阻塞调用方式正好相反,在非阻塞方式中,如果调用结果不能马上返回,当前线程也不会被挂起,而是立即返回执行下一个调用。
在网络通信中,经常可以看到有人将同步和阻塞等同、异步和非阻塞等同。事实上,这两对概念有一定的区别,不能混淆。两对概念的组合,就会产生四个新的概念,同步阻塞、异步阻塞、同步非阻塞、异步非阻塞。
■ 同步阻塞方式,发送方向接收方发送请求后,一直等待响应;接收方处理请求时进行的IO操作如果不能马上得到结果,就一直等到返回结果后,才响应发送方,期间不能进行其他工作。比如,在超市排队付账时,客户(发送方)向收款员(接收方)付款(发送请求)后需要等待收款员找零,期间不能做其他的事情;而收款员要等待收款机返回结果(IO操作)后才能把零钱取出来交给客户(响应请求),期间也只能等待,不能做其他事情。这种方式实现简单,但是效率不高。
■ 同步非阻塞方式,发送方向接收方发送请求后,一直等待响应;接收方处理请求时进行的IO操作如果不能马上得到结果,就立即返回,去做其他事情,但由于没有得到请求处理结果,不响应发送方,发送方一直等待。一直到IO操作完成后,接收方获得结果响应发送方后,接收方才进入下一次请求过程。在实际中不使用这种方式。
■ 异步阻塞方式,发送方向接收方发送请求后,不用等待响应,可以接着进行其他工作;接收方处理请求时进行的IO操作如果不能马上得到结果,就一直等到返回结果后,才响应发送方,期间不能进行其他工作。这种方式在实际中也不使用。
■ 异步非阻塞方式,发送方向接收方发送请求后,不用等待响应,可以继续其他工作;接收方处理请求时进行的IO操作如果不能马上得到结果,也不等待,而是马上返回去做其他事情。当IO操作完成以后,将完成状态和结果通知接收方,接收方再响应发送方。继续使用在超市排队付账的例子。客户(发送方)向收款员(接收方)付款(发送请求)后在等待收款员找零的过程中,还可以做其他事情,比如打电话、聊天等;而收款员在等待收款机处理交易(IO操作)的过程中可以帮助客户将商品打包,当收款机产生结果后,收款员给客户结账(响应请求)。在四种方式中,这种方式是发送方和接收方通信效率最高的一种。
3.2.4 Nginx服务器如何处理请求
Nginx服务器的一个显著优势是能够同时处理大量并发请求。它结合多进程机制和异步机制对外提供服务。异步机制使用的是异步非阻塞方式。
在2.3.2节中,我们介绍过Nginx服务器启动后,可以产生一个主进程(master process)和多个工作进程(worker processes),其中可以在配置文件中指定产生的工作进程数量。Nginx服务器的所有工作进程都用于接收和处理客户端的请求。这类似于Apache使用的改进的多进程机制,预先生成多个工作进程,等待处理客户端请求。
注意
实际上,Nginx服务器的进程模型有两种:Single模型和Master-Worker模型。Single模型为单进程方式,性能较差,一般在实际工作中不使用。Master-Worker模型实际上被更广泛地称为Master-Slave模型。在Nginx服务器中,充当Slave角色的是工作进程。
每个工作进程使用了异步非阻塞方式,可以处理多个客户端请求。当某个工作进程接收到客户端的请求以后,调用IO进行处理,如果不能立即得到结果,就去处理其他的请求;而客户端在此期间也无需等待响应,可以去处理其他的事情;当IO调用返回结果时,就会通知此工作进程;该进程得到通知,暂时挂起当前处理的事务,去响应客户端请求。
客户端请求数量增长、网络负载繁重时,Nginx服务器使用多进程机制能够保证不增长对系统资源的压力;同时使用异步非阻塞方式减少了工作进程在I/O调用上的阻塞延迟,保证了不降低对请求的处理能力。
3.2.5 Nginx服务器的事件处理机制
在上一节中我们提到,Nginx服务器的工作进程调用IO后,就去进行其他工作了;当IO调用返回后,会通知工作进程。这里有一个问题,IO调用是如何把自己的状态通知给工作进程的呢?
一般解决这个问题的方案有两种。一是,让工作进程在进行其他工作的过程中间隔一段时间就去检查一下IO的运行状态,如果完成,就去响应客户端,如果未完成,就继续正在进行的工作;二是, IO调用在完成后能主动通知工作进程。对于前者,虽然工作进程在IO调用过程中没有等待,但不断的检查仍然在时间和资源上导致了不小的开销,最理想的解决方案是第二种。
具体来说,select/poll/epoll/kqueue等这样的系统调用就是用来支持第二种解决方案的。这些系统调用,也常被称为事件驱动模型,它们提供了一种机制,让进程可以同时处理多个并发请求,不用关心IO调用的具体状态。IO调用完全由事件驱动模型来管理,事件准备好之后就通知工作进程事件已经就绪。
3.3 Nginx服务器的事件驱动模型
事件驱动模型是Nginx服务器保障完整功能和具有良好性能的重要机制之一。
3.3.1 事件驱动模型概述
实际上,事件驱动并不是计算机编程领域的专业词汇,它是一种比较古老的响应事件的模型,在计算机编程、公共关系、经济活动等领域均有很广泛的应用。顾名思义,事件驱动就是在持续事务管理过程中,由当前时间点上出现的事件引发的调动可用资源执行相关任务,解决不断出现的问题,防止事务堆积的一种策略。在计算机编程领域,事件驱动模型对应一种程序设计方式,Event-driven programming,即事件驱动程序设计。
如图3.1所示,事件驱动模型一般是由事件收集器、事件发送器和事件处理器三部分基本单元组成。
其中,事件收集器专门负责收集所有的事件,包括来自用户的(如鼠标单击事件、键盘输入事件等)、来自硬件的(如时钟事件等)和来自软件的(如操作系统、应用程序本身等)。事件发送器负责将收集器收集到的事件分发到目标对象中。目标对象就是事件处理器所处的位置。事件处理器主要负责具体事件的响应工作,它往往要到实现阶段才完全确定。
在程序设计过程中,对事件驱动机制的实现方式有多种,这里介绍batch programming,即批次程序设计。批次的程序设计是一种比较初级的程序设计方式。使用批次程序设计的软件,其流程是由程序设计师在设计编码过程中决定的,也就是说,在程序运行的过程中,事件的发生、事件的发送和事件的处理都是预先设计好的。由此可见,事件驱动程序设计更多的关注了事件产生的随机性,使得应用程序能够具备相当的柔性,可以应付种种来自用户、硬件和系统的离散随机事件,这在很大程度上增强了用户和软件的交互性和用户操作的灵活性。
事件驱动程序可以由任何编程语言来实现,只是难易程度有别。如果一个系统是以事件驱动程序模型作为编程基础的,那么,它的架构基本上是这样的:预先设计一个事件循环所形成的程序,这个事件循环程序构成了如图3.1中所示的“事件收集器”,它不断地检查目前要处理的事件信息,然后使用“事件发送器”传递给“事件处理器”。“事件处理器”一般运用虚函数机制来实现。
图3.1 事件驱动模型
我们日常使用的Windows操作系统,就是基于事件驱动程序设计的典型实例。Windows操作系统中的视图(通常叫做“窗口”),是我们所说的事件发送器的目标对象。视图接收事件并能够对其进行相应的处理。当我们将事件发送到具体的某一个视图的时候,实际上我们完成了从传统的流线型程序结构到事件触发方式的转变。
在事件驱动程序的基本单元中,事件收集器已经由Windows操作系统完成;因为Windows操作系统是用C语言实现的,而不是C++语言编写的,所以没有对象的概念,这导致了Windows操作系统只能将发生的事件发送到所谓的“窗口函数”中。事实上,事件尽管不是被发送到具体的对象,但应该说,这是C语言对面向对象方式实现的一个变体。在这里我们可以看到,事件发送器也已经由Windows操作系统完成了部分内容,其中,确定事件的目标所要做的工作的复杂程度可能要超出我们的想象。
3.3.2 Nginx中的事件驱动模型
Nginx服务器响应和处理Web请求的过程,就是基于事件驱动模型的,它也包含事件收集器、事件发送器和事件处理器等三部分基本单元。通过上面的内容,大家应该已经了解了事件驱动模型的基本概念。那么,Nginx服务器是如何使用事件驱动模型来工作的呢?它的“事件收集器”和“事件发送器”的实现没有太大的特点,我们重点介绍一下它的“事件处理器”。
通常,我们在编写服务器处理模型的程序时,基于事件驱动模型,“目标对象”中的“事件处理器”可以有以下几种实现办法:
■ “事件发送器”每传递过来一个请求,“目标对象”就创建一个新的进程,调用“事件处理器”来处理该请求。
■ “事件发送器”每传递过来一个请求,“目标对象”就创建一个新的线程,调用“事件处理器”来处理该请求。
■ “事件发送器”每传递过来一个请求,“目标对象”就将其放入一个待处理事件的列表,使用非阻塞I/O方式调用“事件处理器”来处理该请求。
上面的三种处理方式,各有特点,第一种方式,由于创建新的进程的开销比较大,会导致服务器性能比较差,但其实现相对来说比较简单;第二种方式,由于要涉及到线程的同步,故可能会面临死锁、同步等一系列问题,编码比较复杂;第三种方式,在编写程序代码时,逻辑比前面两种都复杂。大多数网络服务器采用了第三种方式,逐渐形成了所谓的“事件驱动处理库”。
事件驱动处理库又被称为多路IO复用方法,最常见的包括以下三种:select模型、poll模型和epoll模型。Nginx服务器还支持rtsig模型、kqueue模型、dev/poll模型和eventport模型等。通过Nginx配置可以使得Nginx服务器支持这几种事件驱动处理模型。我们在这里详细介绍一下它们,让大家对Nginx服务器的事件处理机制有较为清晰的了解,也为我们后文介绍Nginx服务器源码做铺垫。
3.3.3 select库
select库,是各个版本的Linux和Windows平台都支持的基本事件驱动模型库,并且在接口的定义上也基本相同,只是部分参数的含义略有差异。使用select库的步骤一般是:
首先,创建所关注事件的描述符集合。对于一个描述符,可以关注其上面的读(Read)事件、写(Write)事件以及异常发生(Exception)事件,所以要创建三类事件描述符集合,分别用来收集读事件的描述符、写事件的描述符和异常事件的描述符。
其次,调用底层提供的select()函数,等待事件发生。这里需要注意的一点是,select的阻塞与是否设置非阻塞I/O是没有关系的。
然后,轮询所有事件描述符集合中的每一个事件描述符,检查是否有相应的事件发生,如果有,就进行处理。
Nginx服务器在编译过程中如果没有为其指定其他高性能事件驱动模型库,它将自动编译该库。我们可以使用--with-select_module和--without-select_module两个参数强制Nginx是否编译该库。
3.3.4 poll库
poll库,作为Linux平台上的基本事件驱动模型,是在Linux 2.1.23中引入的。Windows平台不支持poll库。
poll与select的基本工作方式是相同的,都是先创建一个关注事件的描述符集合,再去等待这些事件发生,然后再轮询描述符集合,检查有没有事件发生,如果有,就进行处理。
poll库与select库的主要区别在于,select库需要为读事件、写事件和异常事件分别创建一个描述符集合,因此在最后轮询的时候,需要分别轮询这三个集合。而poll库只需要创建一个集合,在每个描述符对应的结构上分别设置读事件、写事件或者异常事件,最后轮询的时候,可以同时检查这三种事件是否发生。可以说,poll库是select库的优化实现。
Nginx服务器在编译过程中如果没有为其指定其他高性能事件驱动模型库,它将自动编译该库。我们可以使用--with-poll_module和--without-poll_module两个参数强制Nginx是否编译该库。
3.3.5 epoll库
epoll库是Nginx服务器支持的高性能事件驱动库之一,它是公认的非常优秀的事件驱动模型,和poll库及select库有很大的不同。epoll属于poll库的一个变种,是在Linux 2.5.44中引入的,在Linux 2.6及以上的版本都可以使用它。poll库和select库在实际工作中,最大的区别在于效率。
从前面的介绍我们知道,它们的处理方式都是创建一个待处理事件列表,然后把这个列表发给内核,返回的时候,再去轮询检查这个列表,以判断事件是否发生。这样在描述符比较多的应用中,效率就显得比较低下了。一种比较好的做法是,把描述符列表的管理交由内核负责,一旦有某种事件发生,内核把发生事件的描述符列表通知给进程,这样就避免了轮询整个描述符列表。epoll库就是这样一种模型。
首先,epoll库通过相关调用通知内核创建一个有N个描述符的事件列表;然后,给这些描述符设置所关注的事件,并把它添加到内核的事件列表中去,在具体的编码过程中也可以通过相关调用对事件列表中的描述符进行修改和删除。
完成设置之后,epoll库就开始等待内核通知事件发生了。某一事件发生后,内核将发生事件的描述符列表上报给epoll库。得到事件列表的epoll库,就可以进行事件处理了。
epoll库在Linux平台上是高效的。它支持一个进程打开大数目的事件描述符,上限是系统可以打开文件的最大数目;同时,epoll库的IO效率不随描述符数目增加而线性下降,因为它只会对内核上报的“活跃”的描述符进行操作。
3.3.6 rtsig模型
rtsig是Real-Time Signal的缩写,是实时信号的意思。从严格意义上说,rtsig模型并不是常用的事件驱动模型,但Nginx服务器提供了使用实时信号对事件进行响应的支持,官方文档中将rtsig模型与其他的事件驱动模型并列,因此我们也将该模型放到这一节来介绍。rtsig模型在Linux 2.2.19及以上的版本中可以使用。
使用rtsig模型时,工作进程会通过系统内核建立一个rtsig队列用于存放标记事件发生(在Nginx服务器应用中特指客户端请求发生)的信号。每个事件发生时,系统内核就会产生一个信号存放到rtsig队列中等待工作进程的处理。
需要指出的是,rtsig队列有长度限制,超过该长度后就会发生溢出。默认情况下,Linux系统事件信号队列的最大长度设置为1024,也就是同时最多可以存放1024个发生事件的信号。在Linux 2.6.6-mm2之前的版本中,系统各个进程的事件信号队列是由内核统一管理的,用户可以通过修改内核参数/proc/sys/kernel/rtsig-max来自定义该长度设置。在Linux 2.6.6-mm2之后的版本中,该内核参数被取消,系统各个进程分别拥有各自的事件信号队列,这个队列的大小由Linux系统的RLIMIT_SIGPENDING参数定义,在执行setrlimit()系统调用时确定该大小。Nginx提供了worker_rlimit_sigpending参数用于调节这种情况下的事件信号队列长度。
当rtsig队列发生溢出时,Nginx将暂时停止使用rtsig模型,而调用poll库处理未处理的事件,直到rtsig信号队列全部清空,然后再次启动rtsig模型,以防止新的溢出发生。
Nginx在配置文件中提供了相关的参数对rtsig模型的使用进行配置,细节内容在后边讨论Nginx服务器事件驱动模型时将会详细阐述。编译Nginx服务器时,使用-with-rtsig_module配置选项来启用rtsig模型的编译。
3.3.7 其他事件驱动模型
除了以上四种主要的事件驱动模型,Nginx服务器针对特定的Linux平台提供了响应的事件驱动模型支持。目前实现的主要有kqueue模型、/dev/poll模型和eventport模型等。
■ kqueue模型,是用于支持BSD系列平台的高效事件驱动模型,主要用在FreeBSD 4.1及以上版本、OpenBSD 2.9及以上版本、NetBSD 2.0及以上版本以及Mac OS X平台上。该模型也是poll库的一个变种,其和epoll库的处理方式没有本质上的区别,都是通过避免轮询操作提供效率。该模型同时支持条件触发(level-triggered,也叫水平触发,只要满足条件就触发一个事件)和边缘触发(edge-triggered,每当状态变化时,触发一个事件)。如果大家在这些平台下使用Nginx服务器,建议选择该模型用于请求处理,以提高Nginx服务器的处理性能。
■ /dev/poll模型,是用于支持Unix衍生平台的高效事件驱动模型,其主要在Solaris711/99及以上版本、HP/UX 11.22及以上版本、IRIX 6.5.15及以上版本和Tru64 UNIX 5.1A及以上版本的平台中使用。该模型是Sun公司在开发Solaris系列平台时提出的用于完成事件驱动机制的方案,它使用了虚拟的/dev/poll设备,开发人员可以将要监视的文件描述符加入这个设备,然后通过ioctl()调用来获取事件通知。在以上提到的平台中,建议使用该模型处理请求。
■ eventport模型,是用于支持Solaris 10及以上版本平台的高效事件驱动模型。该模型也是Sun公司在开发Solaris系列平台时提出的用于完成事件驱动机制的方案,它可以有效防止内核崩溃等情况的发生,Nginx服务器为此提供了支持。
以上就是Nginx服务器支持的事件驱动库。可以看到,Nginx服务器针对不同的Linux或Unix衍生平台提供了多种事件驱动模型的处理,尽量发挥系统平台本身的优势,最大程度地提高处理客户端请求事件的能力。在实际工作中,我们需要根据具体情况和应用情景选择使用不同的事件驱动模型,以保证Nginx服务器的高效运行。
在本书后文中,我们还会多次提到上面介绍的几种事件驱动的配置。根据不同的环境需求选择不同的事件驱动方式,可以发挥Nginx服务器处理事件的最佳能力。
3.4 设计架构概览
Nginx服务器灵活强大的功能扩展特性是其巨大的优势。本节我们着眼于Nginx服务器的架构,深入了解Nginx服务器的设计思想。架构是一门高深的学问,尤其对于Nginx这样优秀的软件,更是需要在长期的使用中不断学习和总结。本节内容是笔者在自己理解的基础上,结合一些知名学者的观点完成的,希望广大读者在阅读和学习的过程中能够加入我们的讨论,共同探讨Nginx的“架构之美”。
在3.1节中我们介绍了Nginx服务器的模块化设计,对常见的标准HTTP模块、可选HTTP模块、邮件服务模块和第三方模块有了一定的认识。我们看到,从服务器的核心服务到一般应用功能,都是由这些模块支持的,这些模块在功能上彼此独立,在逻辑上又能相互影响、相互联系、共同协作,从而构成一套功能强大完整的服务器程序。那么,Nginx服务器是如何达到这样的效果的呢?这依赖于Nginx服务器的设计架构。
3.4.1 Nginx服务器架构
Nginx服务器启动后,产生一个主进程(master process),主进程执行一系列工作后产生一个或者多个工作进程(worker processes)。主进程主要进行Nginx配置文件解析、数据结构初始化、模块配置和注册、信号处理、网络监听生成、工作进程生成和管理等工作;工作进程主要进行进程初始化、模块调用和请求处理等工作,是Nginx服务器提供服务的主体。
在客户端请求动态站点的过程中,Nginx服务器还涉及和后端服务器的通信。Nginx服务器将接收到的Web请求通过代理转发到后端服务器,由后端服务器进行数据处理和页面组织,然后将结果返回。
另外,Nginx服务器为了提高对请求的响应效率,进一步降低网络压力,采用了缓存机制,将历史应答数据缓存到本地。在每次Nginx服务器启动后的一段时间内,会启动专门的进程对本地缓存的内容重建索引,保证对缓存文件的快速访问。
根据上面的分析,我们可以将Nginx服务器的结构大致分为主进程、工作进程、后端服务器和缓存等部分。图3.2展示了各个部分之间的联系和交互。
图3.2 Nginx服务器架构示意图
在该示意图中,有几个方面的内容我们需要重点阐述,包括Nginx服务器的进程、进程交互和Run-Loop事件处理循环机制等。
3.4.2 Nginx服务器的进程
到目前为止,我们一共提到Nginx服务器的三大类进程:一类是主进程,另一类是由主进程生成的工作进程,还有刚才提到的用于为缓存文件建立索引的进程。
1. 主进程(Master Process)
Nginx服务器启动时运行的主要进程。它的主要功能是与外界通信和对内部其他进程进行管理,具体来说有以下几点:
■ 读取Nginx配置文件并验证其有效性和正确性。
■ 建立、绑定和关闭Socket。
■ 按照配置生成、管理和结束工作进程。
■ 接收外界指令,比如重启、升级及退出服务器等指令。
■ 不中断服务,实现平滑重启,应用新配置。
■ 不中断服务,实现平滑升级,升级失败进行回滚处理。
■ 开启日志文件,获取文件描述符。
■ 编译和处理Perl脚本。
2. 工作进程(Worker Process)
由主进程生成,生成数量可以通过Nginx配置文件指定,正常情况下生存于主进程的整个生命周期。该进程的主要工作有以下几项:
■ 接收客户端请求。
■ 将请求依次送入各个功能模块进行过滤处理。
■ IO调用,获取响应数据。
■ 与后端服务器通信,接收后端服务器处理结果。
■ 数据缓存,访问缓存索引、查询和调用缓存数据。
■ 发送请求结果,响应客户端请求。
■ 接收主程序指令,比如重启、升级和退出等指令。
工作进程完成的工作还有很多,我们在这里列出了主要的几项。从这些工作中可以看到,该进程是Nginx服务器提供Web服务、处理客户端请求的主要进程,完成了Nginx服务器的主体工作。因此,在实际使用中,作为服务器管理者,我们应该重点监视工作进程的运行状态,保证Nginx服务器对外提供稳定的Web服务。
3. 缓存索引重建及管理进程(Cache Loader & Cache Manager)
图3.1中的Cache模块,主要由缓存索引重建(Cache Loader)和缓存索引管理(Cache Manager)两类进程完成工作。缓存索引重建进程是在Nginx服务启动一段时间之后(默认是1分钟)由主进程生成,在缓存元数据重建完成后就自动退出;缓存索引管理进程一般存在于主进程的整个生命周期,负责对缓存索引进行管理。
缓存索引重建进程完成的主要工作是,根据本地磁盘上的缓存文件在内存中建立索引元数据库。该进程启动后,对本地磁盘上存放缓存文件的目录结构进行扫描,检查内存中已有的缓存元数据是否正确,并更新索引元数据库。
缓存索引管理进程主要负责在索引元数据更新完成后,对元数据是否过期做出判断。
这两个进程维护的内存索引元数据库,为工作进程对缓存数据的快速查询提供了便利。
3.4.3 进程交互
Nginx服务器在使用Master-Worker模型时,会涉及主进程与工作进程(Master-Worker)之间的交互和工作进程(Worker-Worker)之间的交互。这两类交互都依赖于管道(channel)机制,交互的准备工作都是在工作进程生成时完成的。
1. Master-Worker交互
工作进程是由主进程生成的(使用了fork函数,具体的源码实现我们在后边的相关章节中完整解析)。Nginx服务器启动以后,主进程根据配置文件决定生成的工作进程的数量,然后建立一张全局的工作进程表用于存放当前未退出的所有工作进程。
在主进程生成工作进程后,将新生成的工作进程加入到工作进程表中,并建立一个单向管道并将其传递给该工作进程。该管道与普通的管道不同,它是由主进程指向工作进程的单向管道,包含了主进程向工作进程发出的指令、工作进程ID、工作进程在工作进程表中的索引和必要的文件描述符等信息。
主进程与外界通过信号机制进行通信,当接收到需要处理的信号时,它通过管道向相关的工作进程发送正确的指令。每个工作进程都有能力捕获管道中可读事件,当管道中有可读事件时,工作进程从管道读取并解析指令,然后采取相应的措施。这样就完成了Master-Worker的交互。
2. Worker-Worker交互
Worker-Worker交互在实现原理上和Master-Worker交互基本是一样的。只要工作进程之间能够得到彼此的信息,建立管道,即可通信。由于工作进程之间是相互隔离的,因此一个进程要想知道另一个进程的信息,只能通过主进程来设置了。
为了达到工作进程之间交互的目的,主进程在生成工作进程后,在工作进程表中进行遍历,将该新进程的ID以及针对该进程建立的管道句柄传递给工作进程表中的其他进程,为工作进程之间的交互做准备。每个工作进程捕获管道中可读事件,根据指令采取响应的措施。
当工作进程W1需要向W2发送指令时,首先在主进程给它的其他工作进程信息中找到W2的进程ID,然后将正确的指令写入指向W2的通道。工作进程W2捕获到管道中的事件后,解析指令并采取相应措施。这样就完成了Worker-Worker交互。
3.4.4 Run Loops事件处理循环模型
Run Loops,指的是进程内部用来不停地调配工作,对事件进行循环处理的一种模型。它属于进程或者线程的基础架构部分。该模型对事件的处理不是自动的,需要在设计代码过程中,在适当的时候启动Run-Loop机制对输入的事件作出响应。
该模型是一个集合,集合中的每一个元素称为一个Run-Loop。每个Run-Loop可运行在不同的模式下,其中可以包含它所监听的输入事件源、定时器以及在事件发生时需要通知的Run-Loop监听器(Run-Loop Observers)。为了监听特定的事件,可以在Run Loops中添加相应的Run-Loop监听器。当被监听的事件发生时,Run-Loop会产生一个消息,被Run-Loop监听器捕获,从而执行预定的动作。
Nginx服务器在工作进程中实现了Run-Loop事件处理循环模型的使用,用来处理客户端发来的请求事件。该部分的实现可以说是Nginx服务器程序实现中最为复杂的部分,包含了对输入事件繁杂的响应和处理过程,并且这些处理过程都是基于异步任务处理的。
我们不打算在这一章深入介绍Nginx服务器的Run-Loop模型。这部分的实现过程结合相关代码实现会更容易理解,因此我们在探讨Nginx服务器源码实现时再对它进行详细介绍,这里大家知道Run-Loop模型的作用就可以了。
通过学习Nginx服务器的整体架构,我们对Nginx服务器各个模块的作用和联系有了比较清晰的认识。可以看到,Nginx服务器提供了异步的、非阻塞的Web服务,系统中的模块各司其职,彼此之间通常使用网络、管道和信号等机制进行通信,从而保持了松耦合的关系。工作进程中事件处理机制的使用,在很大程度上降低了在网络负载繁重的情况下Nginx服务器对内存、磁盘的压力,同时保证了对客户端请求的及时响应。
这里需要提及的一点是,笔者在实际使用Nginx服务器的过程中发现,当磁盘没有足够的性能处理大量IO调用时,工作进程仍然可能因为磁盘读写调用而阻塞,进而导致客户端请求超时失败等问题。目前可以通过多种方法来降低对磁盘IO的调用,比如引入异步输入/输出(Asynchronous Input/Output,AIO)机制等,但这些处理办法没有从根本上解决问题。为了尽量避免产生这种问题,大家在实际部署Nginx服务器时,应当对其运行环境有一个基本的了解,针对不同的网络负载环境选择相匹配的硬件环境,并对Nginx服务器进行合理的配置。
3.5 本章小结
在本章中我们对Nginx服务器的整体架构和各个主要架构部件以及各部件之间的关系和交互进行了阐述。我们首先了解了Nginx服务器的模块化结构思想,并将Nginx服务器的模块大致分为核心模块、标准HTTP模块、可选HTTP模块、邮件服务模块以及第三方模块等五大类。之后对Nginx服务器的Web服务提供机制进行了详细的讨论,其中需要重点关注的是Nginx服务器采用的事件处理机制,这在下一章中我们还会提到。最后,我们重点学习了Nginx服务器的整体设计架构,对Nginx服务器各架构部件的功能、之间的联系进行了详细说明。
相信通过本章的学习,大家对Nginx服务器的整体框架和流程有了一定的认识。在接下来的一章中,我们回到Nginx服务器应用这个中心点上,介绍对Nginx服务器的应用优化。学习了本章内容,大家对下一章中要讨论的内容就更容易理解。
同时,本书的第一篇到此也结束了。在第一篇中,我们着眼于Nginx服务器的基础,对它的历史背景、发展现状进行简要回顾后,通过大量应用实例学习了如何编译、配置、运行Nginx服务器,通过这些学习就可以轻松部署一套可以提供基本Web服务的Nginx服务器。紧接着我们学习了Nginx服务器的整体设计架构,重点对模块化思想和Web请求处理机制进行了阐述,探讨了它如何提供稳定的Web服务和良好的功能扩展,使我们对Nginx服务器的整体运行机制有了一定的了解。
从下一章开始,我们进入Nginx服务器学习的提高阶段,仍然围绕Nginx服务器的应用这一中心,更深入一步地探讨Nginx服务器的高级应用和理论知识。