
2.2 Nginx模块化设计
Nginx主框架中只提供了少量的核心代码,大量强大的功能是在各模块中实现的。模块设计完全遵循高内聚、低耦合的原则。每个模块只处理自己职责之内的配置项,专注完成某项特定的功能。各类型的模块实现了统一的接口规范,这大大增强了Nginx的灵活性与可扩展性。
2.2.1 模块分类
Nginx官方将众多模块按功能分为5类,如图2-2所示。

图2-2 Nginx模块分类
1)核心模块:Nginx中最重要的一类模块,包含ngx_core_module、ngx_http_module、ngx_events_module、ngx_mail_module、ngx_openssl_module、ngx_errlog_module。每个核心模块定义了同一种风格类型的模块。
2)HTTP模块:与处理HTTP请求密切相关的一类模块。HTTP模块包含的模块数量远多于其他类型的模块。Nginx的大量功能是通过HTTP模块实现的。
3)Event模块:该模块定义了一系列可以运行在不同操作系统、不同内核版本的事件驱动模块。Nginx的事件处理框架完美地支持各类操作系统提供的事件驱动模型,包括epoll、poll、select、kqueue、eventport等。
4)Mail模块:与邮件服务相关的模块。Mail模块使Nginx具备了代理IMAP、POP3、SMTP等的能力。
5)配置模块:此类模块只有ngx_conf_module一个成员,是其他模块的基础,因为其他模块在生效前都需要依赖配置模块处理配置指令并完成各自的准备工作。配置模块指导所有模块按照配置文件提供功能,是Nginx可配置、可定制、可扩展的基础。
2.2.2 模块接口
虽然Nginx模块数量众多、功能复杂多样,但并没有给开发人员带来多少困扰,因为所有的模块都遵循同一个ngx_module_t接口设计规范,定义如下:
struct ngx_module_s { ngx_uint_t ctx_index; ngx_uint_t index; char *name; ngx_uint_t spare0; ngx_uint_t spare1; ngx_uint_t version; const char *signature; void *ctx; ngx_command_t *commands; ngx_uint_t type; ngx_int_t (*init_master)(ngx_log_t *log); ngx_int_t (*init_module)(ngx_cycle_t *cycle); ngx_int_t (*init_process)(ngx_cycle_t *cycle); ngx_int_t (*init_thread)(ngx_cycle_t *cycle); void (*exit_thread)(ngx_cycle_t *cycle); void (*exit_process)(ngx_cycle_t *cycle); void (*exit_master)(ngx_cycle_t *cycle); uintptr_t spare_hook0; uintptr_t spare_hook1; uintptr_t spare_hook2; uintptr_t spare_hook3; uintptr_t spare_hook4; uintptr_t spare_hook5; uintptr_t spare_hook6; uintptr_t spare_hook7; };
这是Nginx源码中非常重要的一个结构体,它包含了一个模块的基本信息,包括模块名称、模块类型、模块指令、模块顺序等。注意,init_master、init_module、init_process等7个钩子函数让每个模块能够在Master进程和Worker进程启动与退出、初始化等阶段嵌入各自的逻辑,这大大提高了模块实现的灵活性。
前面我们提到,Nginx对所有模块都进行了分类。每类模块都有自己的特性,实现了自己特有的方法。如何将各类模块和ngx_module_t结构体关联起来呢?细心的读者可能已经注意到,ngx_module_t中有一个类型为void*的ctx成员,其定义了该模块的公共接口。它是ngx_module_t和各类模块的纽带。何谓“公共接口”?简单点讲,就是每类模块都有各自家族特有的协议规范,通过void*类型的ctx变量进行抽象,同类型的模块只需要遵循这一套规范即可。这里以核心模块和HTTP模块为例进行说明。
对于核心模块,ctx变量指向的是名为ngx_core_module_t的结构体。这个结构体很简单,除了一个name成员就只有create_conf和init_conf两个方法。所有的核心模块都会去实现这两个方法。如果将来Nginx拥有了新的核心模块,那它一定是按照ngx_core_module_t结构体的接口规范来实现的。
typedef struct { ngx_str_t name; void *(*create_conf)(ngx_cycle_t *cycle); char *(*init_conf)(ngx_cycle_t *cycle, void *conf); } ngx_core_module_t;
而对于HTTP模块,ctx变量指向的是名为ngx_http_module_t的结构体。这个结构体中定义了8个通用的方法,分别是HTTP模块在解析配置文件前后以及创建、合并http段、server段、location段配置时所调用的方法,代码如下:
typedef struct { ngx_int_t (*preconfiguration)(ngx_conf_t *cf); ngx_int_t (*postconfiguration)(ngx_conf_t *cf); void *(*create_main_conf)(ngx_conf_t *cf); char *(*init_main_conf)(ngx_conf_t *cf, void *conf); void *(*create_srv_conf)(ngx_conf_t *cf); char *(*merge_srv_conf)(ngx_conf_t *cf, void *prev, void *conf); void *(*create_loc_conf)(ngx_conf_t *cf); char *(*merge_loc_conf)(ngx_conf_t *cf, void *prev, void *conf); } ngx_http_module_t;
Nginx在启动的时候,就可以根据当前执行的上下文依次调用所有HTTP模块里ctx变量所指定的方法。更重要的是,对于开发者来说,他只需要按照ngx_http_module_t的接口规范实现自己想要的逻辑,这样不仅降低了开发成本,还提高了Nginx模块的可扩展性和可维护性。
从全局的角度来看,Nginx的模块接口设计兼顾统一化与差异化思想,以最简单、实用的方式实现了模块的多态性。
2.2.3 模块分工
既然Nginx对模块进行了分类,每个模块都实现了某种特定的功能。那这么多模块是如何被有效地组织起来呢?在Nginx启动过程中,各模块需要完成哪些准备工作?在处理请求过程中,各模块又是如何相互协作完成使命呢?本节先简要介绍一下模块分工,后续章节会详细阐述。
事实上,Nginx主框架只关心6个核心模块的实现,每个核心模块分别“代言”一种类型的模块。例如对于HTTP模块,其统一由ngx_http_module管理,包括创建各HTTP模块存储配置项的结构体以及执行各模块的初始化操作的时间点。就好像一家大型公司的管理团队,每个高级管理者负责一个大部门,部门内每个员工专注完成各自的使命。最高层领导只关注各部门管理者,各部门管理者只需管理各自的下属。这种分层思想使得Nginx的源代码具有高内聚、低耦合的特点。
Nginx启动时需要完成配置文件的解析,这部分工作完全是以Nginx配置模块与解析引擎为基础完成的。对于每一个配置指令,Nginx除了需要精准无误地读取和识别指令,还要进行存储与解析。首先,Nginx会找到对该指令感兴趣的模块并调用该模块预先设定好的处理函数。多数情况下,这里会将参数保存到该模块存储配置项的结构体中并进行初始化操作。而核心模块在启动过程中不仅会创建用于保存该“家族”所有存储配置结构体的容器,而且会按顺序将各结构体组织起来,这样众多的模块的配置信息可统一由所属家族的“老大”管理。Nginx也能按照序号从这些全局容器中迅速获取某个模块的配置项。另外,Event模块在启动过程中需要完成最重要的工作,就是根据用户配置以及操作系统选择一个事件驱动模型。Linux系统中,Nginx默认选择epoll模型,在Worker进程被派生(fork)出来并进入初始化阶段时,Event模块会创建各自的epoll对象,并通过epoll_ctl系统调用将监听端口的fd参数添加到epoll中。
用户请求的处理主要是各HTTP模块负责。为了让处理流程更加灵活,各HTTP模块耦合度需要更低。Nginx有意将处理HTTP请求的过程划分为11个阶段,每个阶段理论上允许多个模块执行相应的逻辑。在启动阶段解析完配置文件之后,各HTTP模块会将各自的handler函数以hook的形式挂载到某个阶段。Nginx的Event模块会根据各种事件调度HTTP模块依次执行各阶段的handler处理方法,并通过返回值来判定是继续向下执行还是结束当前请求,这种流水线式的请求处理流程使各HTTP模块完全解耦,给Nginx模块的设计带来了极大的便捷。开发者在完成模块核心处理逻辑之后,只需要考虑将handler函数注册到哪个阶段即可。
自Nginx开源以来,社区涌现出大量优质的第三方模块,极大地扩展了原生Nginx的核心功能,这些都得益于Nginx优秀的模块化设计思想。