Nginx源码分析:核心模块剖析及常见问题

Nginx 在解析完请求行和请求头以后,一共定义了十一个阶段,分别介绍以下php

HTTP 模块工做原理

HTTP 处理的十一个阶段定义前端

 

typedef enum { nginx

        NGX_HTTP_POST_READ_PHASE = 0, // 读取请求内容阶段   git

        NGX_HTTP_SERVER_REWRITE_PHASE, // Server请求地址重写阶段   github

        NGX_HTTP_FIND_CONFIG_PHASE, // 配置查找阶段 apache

        NGX_HTTP_REWRITE_PHASE, // Location请求地址重写阶段 后端

        NGX_HTTP_POST_REWRITE_PHASE, // 请求地址重写提交阶段   数组

        NGX_HTTP_PREACCESS_PHASE, // 访问权限检查准备阶段   缓存

        NGX_HTTP_ACCESS_PHASE, // 访问权限检查阶段 tomcat

        NGX_HTTP_POST_ACCESS_PHASE, // 访问权限检查提交阶段   

        NGX_HTTP_TRY_FILES_PHASE, // 配置项try_files处理阶段 

        NGX_HTTP_CONTENT_PHASE, // 内容产生阶段       

        NGX_HTTP_LOG_PHASE // 日志模块处理阶段 

} ngx_http_phases ;

 

一、读取请求内容阶段

这个阶段没有默认的 handler,主要用来读取请求体,并对请求体作相应的处理

 Server 请求地址重写阶段。这个阶段主要是处理全局的 ( server block) 的 rewrite 规则。

 

二、配置查找阶段

这个阶段主要是经过 uri 来查找对应的 location。而后将 uri 和 location 的数据关联起来。这个阶段主要处理逻辑在 checker 函数中,不能挂载自定义的 handler。

 

三、Location 请求地址重写阶段

这个主要处理 location block 的 rewrite。

 

四、请求地址重写提交阶段

post rewrite,这个主要是进行一些校验以及收尾工做,以便于交给后面的模块。这个 phase 不能挂载自定义 handler。

 

五、访问权限检查准备阶段

好比流控这种类型的 access 就放在这个 phase,也就是说它主要是进行一些比较粗粒度的 access。

 

六、访问权限检查阶段

这个好比存取控制,权限验证就放在这个 phase,通常来讲处理动做是交给下面的模块作的.这个主要是作一些细粒度的 access。

 

七、Location 请求地址重写阶段

这个主要处理 location block 的 rewrite。

 

八、访问权限检查提交阶段

通常来讲当上面的access模块获得access_code以后就会由这个模块根据access_code来进行操做 这个phase不能挂载自定义handler

 

九、配置项 try_files 处理阶段

try_file 模块,也就是对应配置文件中的 try_files 指令。 这个 phase 不能挂载自定义 handler。按顺序检查文件是否存在,返回第一个找到的文件。结尾的斜线表示为文件夹 -$uri/。若是全部的文件都找不到,会进行一个内部重定向到最后一个参数。

 

十、内容产生阶段

内容处理模块,产生文件内容,若是是 php,去调用 phpcgi,若是是代理,就转发给相应的后端服务器

 

十一、日志模块处理阶段

日志处理模块,是每一个请求最后必定会执行的。用于打印访问日志。

 

自定义的 handler 有时候能够挂载在不一样的 phase,均可以正常运行。自定义的 handler,若是依赖某一个 phase 的结果,则必须挂载在该 phase 后面的 phase 上。自定义的 handler 须要遵照 nginx 对不一样 phase 的功能划分,但不是必需的。除去 4 个不能挂载的 phase 和 log phase,还有以下 6 个 phase 能够挂载

 

  1. NGX_HTTP_POST_READ_PHASE

  2. NGX_HTTP_SERVER_REWRITE_PHASE

  3. NGX_HTTP_REWRITE_PHASE

  4. NGX_HTTP_PREACCESS_PHASE

  5. NGX_HTTP_ACCESS_PHASE

  6. NGX_HTTP_CONTENT_PHASE

 

不少功能挂载在这 6 个 phase,均可以实现。挂载在越前面,咱们的性能会越好,挂载在后面,咱们的自由度会更大。好比说,若是咱们实如今 NGX_HTTP_POST_READ_PHASE 阶段,咱们就不能用 location if 这些后面阶段实现的指令来组合实现一些更复杂的功能。推荐使用:

 

  • NGX_HTTP_PREACCESS_PHASE

  • NGX_HTTP_ACCESS_PHASE

  • NGX_HTTP_CONTENT_PHASE

 

Nginx 在解析 HTTP 的配置时,会将多个 phase handler 数组,合成为一个数组。handler 会在 postconfigure 阶段初始化。实际调用过程当中执行:

 

void ngx_http_core_run_phases(ngx_http_request_t *r) { 

        ngx_int_t rc; 

        ngx_http_phase_handler_t *ph; 

        ngx_http_core_main_conf_t *cmcf;   

        cmcf = ngx_http_get_module_main_conf(r, ngx_http_core_module);   

        ph = cmcf->phase_engine.handlers;   

        while (ph[r->phase_handler].checker) {   

                rc = ph[r->phase_handler].checker(r, &ph[r->phase_handler]);   

               if (rc == NGX_OK) { return; } 

         }

 }

 

实现经过调用 checker,再在 checker 中调用对应的 handler,实现对各个阶段的调用。如下是不一样的 phase,对应不一样的 checker 函数

 

  • NGX_HTTP_POST_READ_PHASE ngx_http_core_generic_phase

  • NGX_HTTP_SERVER_REWRITE_PHASE ngx_http_core_rewrite_phase

  • NGX_HTTP_FIND_CONFIG_PHASE ngx_http_core_find_config_phase

  • NGX_HTTP_REWRITE_PHASE ngx_http_core_rewrite_phase

  • NGX_HTTP_POST_REWRITE_PHASE ngx_http_core_post_rewrite_phase

  • NGX_HTTP_PREACCESS_PHASE ngx_http_core_generic_phase

  • NGX_HTTP_ACCESS_PHASE ngx_http_core_access_phase

  • NGX_HTTP_POST_ACCESS_PHASE ngx_http_core_post_access_phase

  • NGX_HTTP_TRY_FILES_PHASE ngx_http_core_try_files_phase

  • NGX_HTTP_CONTENT_PHASE ngx_http_core_content_phase

  • NGX_HTTP_LOG_PHASE ngx_http_core_generic_phase

 

通常而言 handler 返回:

 

  • NGX_OK : 表示该阶段已经处理完成,须要转入下一个阶段

  • NG_DECLINED : 表示须要转入本阶段的下一个handler继续处理

  • NGX_AGAIN, NGX_DONE : 表示须要等待某个事件发生才能继续处理(好比等待网络IO),此时Nginx为了避免阻塞其余请求的处理,必须中断当前请求的执行链,等待事件发生以后继续执行该handler

  • NGX_ERROR:表示发生了错误,须要结束该请求。

 

checker 函数根据 handler 函数的不一样返回值,给上一层的 ngx_http_core_run_phases 函数返回 NGX_AGAIN 或者 NGX_OK,若是指望上一层继续执行后面的 phase 则须要确保 checker 函数不是返回 NGX_OK,不一样 checker 函数对 handler 函数的返回值处理还不太同样,开发模块时须要确保相应阶段的 checker 函数对返回值的处理在你的预期以内。

 

创建链接

在前面 event poll 初始化的时候,添加了事件方法为 ngx_event_accept 在接受完 accept 请求后,最后一步将会调用 ls->handler(c) 该 hander 为 ngx_http_init_connection,这个过程分为:

  1. 初始化ngx_http_connection_t结构

  2. 设置可读回调方法为:ngx_http_wait_request_handler

  3. 设置可写回调方法为空:ngx_http_empty_handler

  4. 若是可读已经就绪,则执行ngx_http_wait_request_handler

  5. 将可读事件加入到定时器中以监控链接是否超时

  6. 将可读事件添加到epoll中

 

第一次可读事件处理

这时执行的是 ngx_http_wait_request_handler 这个步骤分为:

  1. 判断请求是否超时

  2. 建立接受缓冲区

  3. 建立并初始化ngx_http_request_t

  4. 更新ngx_http_request_t请求开始时间

  5. 调用ngx_http_process_request_line

 

接收请求行

这个步骤由 ngx_http_process_request_line 完成,因为一次调用未必能所有读取完成,因此,设置了:rev→handler = ngx_http_process_request_line; Top of Form,这个过程分为:

  1. 判断请求是否超时

  2. 读取请求头

  3. 解析请求行

  4. 处理请求uri

  5. 设置读hander为:ngx_http_process_request_headers;

  6. 处理请求头

 

处理 HTTP 请求

处理完请求头,最终调用 ngx_http_process_request:

  1. 因为请求头部已经接收完,因此删除定时器 if (c->read->timer_set) { ngx_del_timer(c->read); }

  2. 因为再也不接收请求行和头部,读写回调所有设置为:

    c->read->handler = ngx_http_request_handler; 

    c->write->handler = ngx_http_request_handler;

  3. 将 request 的 read_event_hander 设置为何都不作:r->read_event_handler = ngx_http_block_reading;

  4. ngx_http_handler

    1. 根据internal标志判断是否要从定向

    2. 设置请求的写事件hander为ngx_http_core_run_phases,执行ngx_http_core_run_phases方法。r->write_event_handler = ngx_http_core_run_phases; ngx_http_core_run_phases(r)。这个过程执行十一个阶段的hander而且调用checker,上面已经介绍过了。

  5. 调用ngx_http_run_posted_requests

     

处理子请求

ngx_http_run_posted_requests 方法根据链表顺序调用了 write_event_handler

 

处理 HTTP 包体

该工具方法为 ngx_http_read_client_request_body 执行过程以下:

  1. 原始请求计数器+1: r→main→count++

  2. 执行各模块子请求的post_handler

  3. 接受ngx_http_request_body_t

  4. 假如一次性没有接受完,则设置request的read_event_handler为:ngx_http_read_client_request_body_handler

 

放弃处理 HTTP 包体

ngx_http_discard_request_body该方法只读取上游body数据,但不保存。

 

发送 HTTP 响应

分别调用 head 和 body 的责任链:

 

ngx_int_t ngx_http_send_header(ngx_http_request_t *r) { 

        if (r->header_sent) { 

                ngx_log_error(NGX_LOG_ALERT, r->connection->log, 0, "header already sent"); 

                return NGX_ERROR; 

        }   

        if (r->err_status) { 

                r->headers_out.status = r->err_status; 

                r->headers_out.status_line.len = 0; 

        }   

        return ngx_http_top_header_filter(r);

}

 

ngx_int_t ngx_http_output_filter(ngx_http_request_t *r, ngx_chain_t *in) { 

        ngx_int_t rc; 

        ngx_connection_t *c;   

       c = r->connection;   

       ngx_log_debug2(NGX_LOG_DEBUG_HTTP, c->log, 0,"http output filter \"%V?%V\"", &r->uri, &r->args);   

       rc = ngx_http_top_body_filter(r, in);   

       if (rc == NGX_ERROR) { /* NGX_ERROR may be returned by any filter */ 

               c->error = 1; 

       }   

      return rc;

}

 

这个也为自定义 HTTP 过滤模块的开发提供了可能。

 

结束 HTTP 请求

ngx_http_finalize_request 用于结束 HTTP 请求

 

整个 HTTP 请求的流程图:

 

Upstream

upstream的工做过程:(以ngx memcached模块为例,此时上游服务器为memcached):

 

1.模块配置初始化的时候,设置配置的handler

 

2.http请求在ngx_http_core_find_config_phase阶段,调用了:

ngx_http_update_location_config,将配置的handler设置为:r->content_handler

(这里为:ngx_http_memcached_handler)

 

3.ngx_http_core_content_phase阶段ngx_http_memcached_handler接管了请求

 

4.ngx_http_memcached_handler初始化:

a)u->input_filter_init = ngx_http_memcached_filter_init;

    u->input_filter = ngx_http_memcached_filter;

    u->input_filter_ctx = ctx;

 

b)ngx_http_upstream_create

c)ngx_http_upstream_init

d)ngx_http_upstream_init_request初始化请求

 

5.ngx_http_upstream_connect:

设置链接的读写回调函数:

    c->write->handler = ngx_http_upstream_handler;

    c->read->handler = ngx_http_upstream_handler;

 

    u->write_event_handler = ngx_http_upstream_send_request_handler;

    u->read_event_handler = ngx_http_upstream_process_header;

6.ngx_http_upstream_send_request_handler发送请求给上游服务器

 

7.ngx_http_upstream_process_header处理上游返回的头:

若是subrequest_in_memory:

u->read_event_handler = ngx_http_upstream_process_body_in_memory

不然:

ngx_http_upstream_send_response:

 u->read_event_handler = ngx_http_upstream_process_non_buffered_upstream;

 r->write_event_handler = ngx_http_upstream_process_non_buffered_downstream;

 

 8.若是上游内容较多继续调用ngx_http_upstream_process_non_buffered_upstream

 

 9.ngx_http_upstream_process_non_buffered_downstream发送数据给下游。

 

 注意:假以下游速度过慢,能够经过u->buffering来控制是否缓存或者临时文件保存上游的结果数据。

 

原理图以下:

 

Nginx 锁机制 & 原子性保证

 

原子性

ngx 的 atomic 的代码以下:

 

static ngx_inline ngx_atomic_uint_tngx_atomic_cmp_set(ngx_atomic_t *lock, ngx_atomic_uint_t old,ngx_atomic_uint_t set) {

        u_char res;

        __asm__ volatile (NGX_SMP_LOCK” cmpxchgl %3, %1;”” sete %0; ” : “=a” (res) : “m” (*lock), “a” (old), “r” (set) : “cc”, “memory”);

        return res ; 

}

 

工做原理:

  • 在多核环境下,NGX_SMP_LOCK 其实就是一条 lock 指令,用于锁住总线。

  • cmpxchgl 会保证指令同步执行。

  • sete 根据 cmpxchgl 执行的结果(eflags 中的 zf 标志位)来设置 res 的值。

 

其中假如 cmpxchgl 执行完以后,时间片轮转,这个时候 eflags 中的值会在堆栈中保持,这是 cpu task 切换机制所保证的,因此,等时间片切换回来再次执行 sete 的时候,也不会致使并发问题。

 

至于信号量,互斥锁,最终还得依赖原子性的保证,具体锁实现能够有兴趣本身再去阅读源代码。

 

Nginx cpu 亲和度设置

 

例 1:worker_processes 4 ;  worker_cpu_affinity 0001 0010 0100 1000 ; 该配置实现将每一个CPU绑定到一个worker进程上。

例 2:worker_processes 2 ;  worker_cpu_affinity 0101 1010 ; 该配置实现将 1,3 号 CPU 绑定到worker1上,2,4 号 CPU 绑定到 worker2 上。

 

ngx 代码实现

 

void ngx_setaffinity(uint64_t cpu_affinity, ngx_log_t *log) { 

        cpuset_t mask; 

        ngx_uint_t i;   

        ngx_log_error(NGX_LOG_NOTICE, log, 0, "cpuset_setaffinity(0x%08Xl)", cpu_affinity);   

        CPU_ZERO(&mask); 

        i = 0; 

        do { 

                 if (cpu_affinity & 1) { 

                        CPU_SET(i, &mask); 

                } 

                i++; 

                cpu_affinity >>= 1; 

        } 

        while (cpu_affinity) ;  

        if (cpuset_setaffinity(CPU_LEVEL_WHICH, CPU_WHICH_PID, -1, sizeof(cpuset_t), &mask) == -1)  { 

                ngx_log_error(NGX_LOG_ALERT, log, ngx_errno, "cpuset_setaffinity() failed"); 

        } 

}

 

经过位运算而后获得 cpu 号存入 mask ,最终调用 cpuset_setaffinity 让 cpu 和当前线程挂钩。

 

 

Nginx 模块开发方法

nginx 自定义模块能够分红几种模块:

 

 

  • ngx http过滤模块。能够参考:https://github.com/lingqi1818/beacon/blob/master/ngx_http_beacon_module.c。原理主要改变了 ngx_http_output_header_filter_pt 和 ngx_http_output_body_filter_pt 的指针

  • ngx http模块。能够参考:http://blog.csdn.net/poechant/article/details/7627828

 

Nginx 实战

静态服务器

用于替换apache相同的静态资源处理功能,目前世界上大多数网站都已经采用了nginx服务器。

 

静态资源合并

例如:http://static.helijia.com/js/a.js,js/b.js,js/c.js,这块 tengine 已经实现,无需本身开发模块。

 

这样作的好处是,可让客户端减小与服务端的请求次数,一次请求获取多个静态资源文件内容。

 

动态更新CDN版本号,结合 inotify

 

单机多个 tomcat 进程负载分发

不少时候,在单机部署多个jetty/tomcat这样的服务时,能够借助nginx来作负载均衡:

上图中的normandy即为部署在jetty/tomcat中的登录服务。

 

前端 abtest 种 cookie:

例如:1 if ngx.var.cookie_login_test == nil then

  2    local r = math.random(100);

  3    ngx.log(ngx.ERR,r)

  4    if r >=1 and r <=90 then

  5    ngx.header["Set-Cookie"]={"login_test:0;expires=Thu, 31 Dec 2115 23:59:59 GMT;max-age=3153600000;domain=xxx   ;path=/"}

  6    else

  7    ngx.header["Set-Cookie"]={"login_test:1;expires=Thu, 31 Dec 2115 23:59:59 GMT;max-age=3153600000;domain=xxx    ;path=/"}

  8    end

  9 end

 

经过上面代码返回不一样几率的 login_test 值,让客户端的登录页面产生不一样的内容。

 

后端 abtest 根据规则分发请求

结合upstream模块的开发,可让后ngx根据不一样的cookie或者请求数据,来发送给不一样版本的后端服务器。

 

ngx_lua 脚本

目前有两套解决方案:

  • 一套是淘宝写的ngx_lua模块,淘宝的模块其实经过对ngx嵌入lua功能,加强了ngx_body_filter和ngx_head_fiter的过程,对于简单的请求过滤处理能够作到很是轻量级的开发。

  • 另外一套是OpenResty,OpenResty则功能更强大些,提供的能力也更为复杂,具体使用哪一个能够根据不一样的需求场景本身作判断并选择。

 

Q & A

一、可否简单对比下 Apache 和 Nginx?

  • 工做模式的不一样。ngx 全部的请求都是异步,非阻塞的方式,而且采用了 epoll 模型。Apache 是线程池的模型。

  • Apache 依赖了不少三方库,ngx 则是所有本身实现。

  • ngx 的设计所有是模块化的,比较轻量级。

  • ngx 配置比较简洁。

  • ngx 对机器的利用率较高,一切设计都是为了节省内存和提升性能。

 

二、请问Nginx重加载配置是如何处理老配置和当前请求的?

ngx 是经过 SIGHUP 信号来 reload 配置的,这个过程由 master 进程负责。2.8处理 SIGHUP 信号,2.8.1 平滑升级,重启 worker 进程。

 

if (ngx_new_binary) {

        ngx_start_worker_processes(cycle, ccf->worker_processes,NGX_PROCESS_RESPAWN);

        ngx_start_cache_manager_processes(cycle, 0);

        ngx_noaccepting = 0;

        continue;

}

 

2.8.2不是平滑升级,须要从新读取配置

...

    cycle = ngx_init_cycle(cycle);

 

    if (cycle == NULL) {

            cycle = (ngx_cycle_t *) ngx_cycle;

            continue;

    }

 

        ngx_cycle = cycle;

        ccf = (ngx_core_conf_t *) ngx_get_conf(cycle->conf_ctx,

        ngx_core_module);

        ngx_start_worker_processes(cycle, ccf->worker_processes,

        NGX_PROCESS_JUST_RESPAWN);

        ngx_start_cache_manager_processes(cycle, 1);

 

    /* allow new processes to start */

        ngx_msleep(100);

        live = 1;

        ngx_signal_worker_processes(cycle,

        ngx_signal_value(NGX_SHUTDOWN_SIGNAL));

}

 

这个过程会重启工做线程。

 

三、使用线程池后,Nginx 的性能提高 9 倍是真的吗?

我认为线程池能够从某种程度提高响应时间,毕竟单个线程处理多个请求,速度再快也是先来后到排序的。固然,这个提高也须要在编写模块的时候所有符合做者异步,非阻塞的思路,好比转成 subrequest。

 

四、用 Nginx 自己的模块来作 ddos 攻击是否合适?若是不合适业界是否有成型的 Nginx 解决方案 or 其它,想参考一下。目前咱们的作法是用的后台生成验证码的方式(app)

ngx 作 ddos 攻击的话,就须要在 tcp 层来作了。不能直接编写 ngx http 模块的方法。不然失去了抗 ddos 攻击的意义。

 

至于业界的方案这个我到不是很清楚,以前在阿里有个基于 Apache 的 humock,是在 tcp 层作的防攻击。

 

五、Nginx 不是多进程并发处理么?

ngx 的多个进程是指多个工做进程,分别来管理一份 epoll 事件池的。

经过这样的方式能够尽可能利用 cpu,并且 ngx 的所有设计思路就是围绕异步,非阻塞。相似 Redis 的事件机制。

 

六、不是说静态文件处理,Apache 比 Nginx 更有优点吗?

以前我用 ab 作过性能测试,一个 1M 大小的页面,性能 ngx 比 Apache 更强一些。不知道这个 Apache 比 ngx 更有优点的结论是如何的出来的。理论上 epoll 模型会比多线程模型开销更小一些。

 

七、针对问题 5,是说每一个工做进程再搞个线程池?

每一个工做进程不须要线程池,epoll 就是个事件池子,等事件就绪以后,就能够回调你以前注册的回调函数

 

八、若是分配工做进程数和 cpu 核数匹配,由于工做进程的工做方式是异步的,不存在阻塞,因此再每一个工做进程再搞个线程池有意义么?,由于 cpu 核都在 run,没有空闲。

我以为仍是有意义的,毕竟 CPU 不会给你那几个进程独占,时间片是会切换的,线程池能够提升吞吐率。固然这须要作下测试,另外须要看下使用场景。

 

九、针对问题3,使用线程池,是那块使用呢?

Nginx 引入线程池是为了解决由于某些长时间阻塞的调用致使性能降低的问题。能够在编译的时候加选项开启线程池。

 

十、Nginx的高性能,高吞吐。最主要由什么设计决定呢? 异步非阻塞?

异步非阻塞,绝对是一个核心点,另外 ngx 对任何使用内存的地方很是抠门,都是在框架上搞定了。而且能不使用就不使用,因此在你本身编写模块的时候,你都是从 pool 中拿到的内存。释放也是 ngx 帮你搞定了。并且 ngx 全部的数据结构都是结合场景精心设计的。脱离 ngx 这个场景,你在其余地方都会以为很怪异。

相关文章
相关标签/搜索