分类: 架构设计与优化
2014-11-09 23:05:59
众所周知,nginx是处理http的 web server,其必然要用到OS的网络处理机制,nginx的高效来自于OS本身所提供的高效的网络处理机制,在linux中是epoll,在freebsd中是kqueue机制,对于其他的OS我们不予考虑。
epoll从本质上来说是一种基于事件异步处理的高效网络处理机制,其本质是基于轮寻 + mmap机制,相对于select与poll这种水平触发机制来说,epoll可以提供高的多的性能。
Nginx对epoll的使用基于事件机制,而epoll与事件机制本身在nginx中又基于nginx模块机制。 这一系列的过程甚是复杂,待我一一道来 ;)
从架构角度讲,我们甚至可以说nginx主要是由模块组成,众多的软件模块组合在一起工作,构成了nginx的强大,高效的功能。
Nginx所有的模块都需要注册在 ngx_modules[]数组中,在main函数启动之初,nginx会计算出模块的总数 :
333 ngx_max_module = 0;
334 for (i = 0; ngx_modules[i]; i++) {
335 ngx_modules[i]->index = ngx_max_module++;
336 }
换句话说,如果你想加入新的模块,那么你的模块在编译之前必须要放到ngx_modules[]数组中去,这个和用动态库的模块管理方式稍有区别,需要特别注意。
模块的初始化当然都在ngx_init_cycle(cycle)函数中完成,我们前面介绍过这个函数的大致流程,这里结合模块再详细过一遍。
187 cycle->conf_ctx = ngx_pcalloc(pool, ngx_max_module * sizeof(void *));
我们可以看到,针对每一个模块,nginx都会分配一个conf context,由全局的 cycle 来管理,为的是方便在各个处理阶段找到这个contex。注意,此时只是创建了一个指向本context的指针而已。
217 for (i = 0; ngx_modules[i]; i++) {
218 if (ngx_modules[i]->type != NGX_CORE_MODULE) {
219 continue;
220 }
221
222 module = ngx_modules[i]->ctx;
223
224 if (module->create_conf) {
225 rv = module->create_conf(cycle);
226 if (rv == NULL) {
227 ngx_destroy_pool(pool);
228 return NULL;
229 }
230 cycle->conf_ctx[ngx_modules[i]->index] = rv;
231 }
232 }
注意,在上面创建完成 conf context 后,这一步具体为每个模块分配了各自的配置保存结构,当然前提是这个模块需要配置的话。需要注意的是,这里只队CORE类型的模块有效。有效的模块列表如下 :
ngx_core_module ---> 创建 ngx_core_conf_t 结构
ngx_errlog_module ---> NULL
ngx_conf_module ---> NULL
ngx_events_module ---> NULL
ngx_openssl_module ---> NULL
ngx_http_module ---> NULL
ngx_mail_module ---> NULL
ngx_google_perftools_module ---> 创建 ngx_google_perftools_conf_t 结构
再回到网络处理机制上来,我们发现只有ngx_core_module会在这一阶段创建 ngx_core_conf_t 结构,其他都没有(google默认不再modules列表中)。
接下来就是真正的配置文件的初始化过程,在这个过程中,nginx会解析出配置文件的每一个command, 然后调用 ngx_conf_handler() 函数进行处理,在这个函数中,它会将当前的command 名称与所有modules的所有command进行匹配,如果匹配成功则调用此command的 set 函数, 实际上就是把当前的一行配置用 set 函数解析出来后,保存到 之前创建的 cycle->conf_ctx[i] 中去,也就是为 cycle-.conf_ctx[i] 所指向的结构中的某一项内容进行赋值。
依赖最基础的配置文件,我们你可以发现 "worker_processes 1" 这个配置会被 ngx_core_module 的command所处理。
具体到与事件相关的配置上,就是配置文件中的 :
12 events {
13 worker_connections 1024;
14 }
这段配置由 ngx_events_module 的 command 'events' 对应的函数 "ngx_events_block" 所处理,这也是个比较重要的函数,我们来看一下。
在ngx_events_block 函数中我们看到,它会做一些跟模块相关的事情,并且它只处理 EVENT类型的模块,具体就是 : ngx_event_core_module 和 ngx_epoll_module 模块(当然还有select,pool,kqueue等,略过)。在这个函数中,首先为ngx_events_module 创建了一个 配置相关的 ctx,这个ctx其实是一个指针,指向具体各个event module的配置 ctx, 然后它开始便利module列表,找到所有EVENT类型的module,并调用次module的 create_conf() 函数。 我们看到, ngx_event_core_module创建了ngx_event_conf_t的配置结构, ngx_epool_module 创建了ngx_epoll_conf_t结构。然后它递归调用 ngx_conf_parse()函数继续处理events block内部的配置,过程与之前的相同,这里就不再描述了。函数最后,它再次遍历modules列表,找到所有的EVENT类型模块,调用其init_conf() 函数,最后done!
我们要重点关注一下这些 EVENT 类型 模块的 init_conf() 函数都做了些什么事情。首先看 ngx_event_core_module 的 init_conf() 函数 : 它首先找到EPOLL 模块,然后利用EPOLL模块和其他的一些参数来初始化自己ngx_event_conf_t 的配置结构内的参数。 然后我们再来看看ngx_epoll_module的init_conf()函数都做了些什么,它只是简单的初始化了ngx_epoll_conf_t的配置结构的一些参数,非常简单。
所以,我们看,在初始化过程中,EVENT与 EPOLL模块所做的事情很简单,只是创建了自己的配置管理结构并初始化而已,真正的工作在http 模块中 。。。。
ngx_http_module 本身是CORE类型的module,但是它没有create_conf() 以及 init_conf() 函数,所以它并没有预先创建自己的类似于 ngx_http_conf_t 的结构。 在解析配置文件的过程中,当遇到 http tag时候,parse函数就会调用 ngx_http_module 模块的command ngx_http_block() 函数来处理。
这个函数首先创建 ngx_http_conf_ctx_t 配置结构,它包含了子ctx : main, server, location。 然后同样遍历modules数组,找到所有的HTTP类型的模块,然后调用其注册的create_main_conf(),create_srv_conf(),create_loc_conf() 三个函数(如果不为空),我们在这里只关心ngx_http_core_module, 其定义了所有这三个函数,其描述如下 :
ngx_http_core_create_main_conf() ---> 创建 ngx_http_core_main_conf_t,这个结构会通过servers 数组管理ngx_http_core_srv_conf_t 结构。
ngx_http_core_create_srv_conf() ---> 创建ngx_http_core_srv_conf_t,这个结构会通过server_names 数组管理 ngx_http_server_name_t结构
ngx_http_core_create_loc_conf() ---> 创建 ngx_http_core_loc_conf_t,这是个非常庞大复杂的结构,nginx大部分的配置都集中在这里。
接下来,次函数遍历modules数组,找到HTTP模块,调用其preconfiguration函数,其中ngx_http_core_module 利用其 preconfiguration 函数初始化了HTTP 头部处理的函数以及相关数据结构,将其保存在http core main conf 结构的variable keys hash 表中。 并将其保存在HTTP CORE module的全局CTX(保存于cycle中)的main_conf[i] 中。 事实上,几乎所有的HTTP module 的处理函数都保存到了这个variable_keys的HASH表中,这是非常方便的,利用HASH KEY一次计算就可以找到相应的处理函数。
接下来,继续递归式的调用ngx_conf_parse() 函数,处理server{} 配置block, 然后用类似的方法递归调用ngx_conf_parse()处理location{}的配置。
需要注意的是,nginx的listen配置标识了一个虚拟服务器的监听端口,如果没有配置的话默认为80。
所有配置完成后,cycle->listening 里面就有了所有虚拟服务器监听的端口与地址(url,rui)等信息,然后,open listening, configure listening ports, 再然后在worker process启动时调用http core 模块的init_process()函数给每一个处于listening状态的socket分配一个connection,这个connection的receive handler 是ngx_event_accept,用来处理client发起的链接。
在ngx_event_accept() 函数中,nginx accept一个新的客户端链接,申请一个新的connecton来保存处理这个新的连接,需要注意的是,每一个listening 都由一个handler函数,在ngx_event_accept()函数的末尾调用,这个ls->handler 在ngx_http_add_listening()中被赋值为ngx_http_init_connection(),目的是,每当一个新的client链接建立时,首先要初始化这个connection,其中一个最终要的操作是为这个新的connection设置它的read handler函数为 : ngx_http_wait_request_handler, 设置其write handler 函数为 : ngx_http_empty_handler, 这两个函数就是nginx处理HTTP的入口函数! 所以,接下来在EPOLL的 EVENT处理中,直接调用这个HTTP的handler函数就可以进入HTTP的处理流程了!
至此,总算是基本介绍完成了nginx的网络处理机制。可以看到,nginx的网络处理机制可以说是非常的复杂,和其配置文件的处理紧密结合,一定要花相当长的时间去分析代码才能彻底搞懂这套机制,我花了大概两天半的时间反复读nginx的代码才搞定,实在是不容易!
从另外的角度来说,nginx网络部分实现之所以这么复杂,和其支持非常多的网络特性是分不开的,这也是nginx强大的一个很重要的原因。
接下来是HTTP部分的分析,待续 。。。。。。。