编者按:高可用架构分享及传播在架构领域具有典型意义的文章,本文由陈科在高可用架构群分享。转载请注明来自高可用架构公众号「ArchNotes」。
导读:很多架构师希望了解及掌握高性能服务器开发。阅读优秀源代码是一种有效的方式,其中 nginx 是业界知名的高性能 Web 服务器实现。如何有效的阅读及理解 nginx?本文介绍 HTTP 等核心模块及阅读 nginx 源代码的常见问题,帮助大家来更好的阅读及理解 nginx 关键模块的实现。
陈科,十年行业从业经验,曾在浙江电信、阿里巴巴、华为、五八同城任开发工程&架构师等职,目前负责河狸家后端架构和运维。博客地址:http://www.dumpcache.com/wiki/doku.php
Nginx 在解析完请求行和请求头之后,一共定义了十一个阶段,分别介绍如下
HTTP 模块工作原理
HTTP 处理的十一个阶段定义
typedef enum {
NGX_HTTP_POST_READ_PHASE = 0, // 读取请求内容阶段
NGX_HTTP_SERVER_REWRITE_PHASE, // Server请求地址重写阶段
NGX_HTTP_FIND_CONFIG_PHASE, // 配置查找阶段
NGX_HTTP_REWRITE_PHASE, // Location请求地址重写阶段
NGX_HTTP_POST_REWRITE_PHASE, // 请求地址重写提交阶段
NGX_HTTP_PREACCESS_PHASE, // 访问权限检查准备阶段
NGX_HTTP_ACCESS_PHASE, // 访问权限检查阶段
NGX_HTTP_POST_ACCESS_PHASE, // 访问权限检查提交阶段
NGX_HTTP_TRY_FILES_PHASE, // 配置项try_files处理阶段
NGX_HTTP_CONTENT_PHASE, // 内容产生阶段
NGX_HTTP_LOG_PHASE // 日志模块处理阶段
} ngx_http_phases ;
1、读取请求内容阶段
这个阶段没有默认的 handler,主要用来读取请求体,并对请求体做相应的处理
Server 请求地址重写阶段。这个阶段主要是处理全局的 ( server block) 的 rewrite 规则。
2、配置查找阶段
这个阶段主要是通过 uri 来查找对应的 location。然后将 uri 和 location 的数据关联起来。这个阶段主要处理逻辑在 checker 函数中,不能挂载自定义的 handler。
3、Location 请求地址重写阶段
这个主要处理 location block 的 rewrite。
4、请求地址重写提交阶段
post rewrite,这个主要是进行一些校验以及收尾工作,以便于交给后面的模块。这个 phase 不能挂载自定义 handler。
5、访问权限检查准备阶段
比如流控这种类型的 access 就放在这个 phase,也就是说它主要是进行一些比较粗粒度的 access。
6、访问权限检查阶段
这个比如存取控制,权限验证就放在这个 phase,一般来说处理动作是交给下面的模块做的.这个主要是做一些细粒度的 access。
7、Location 请求地址重写阶段
这个主要处理 location block 的 rewrite。
8、访问权限检查提交阶段
一般来说当上面的access模块得到access_code之后就会由这个模块根据access_code来进行操作 这个phase不能挂载自定义handler
9、配置项 try_files 处理阶段
try_file 模块,也就是对应配置文件中的 try_files 指令。 这个 phase 不能挂载自定义 handler。按顺序检查文件是否存在,返回第一个找到的文件。结尾的斜线表示为文件夹 -$uri/。如果所有的文件都找不到,会进行一个内部重定向到最后一个参数。
10、内容产生阶段
内容处理模块,产生文件内容,如果是 php,去调用 phpcgi,如果是代理,就转发给相应的后端服务器
11、日志模块处理阶段
日志处理模块,是每个请求最后一定会执行的。用于打印访问日志。
自定义的 handler 有时候可以挂载在不同的 phase,都可以正常运行。自定义的 handler,如果依赖某一个 phase 的结果,则必须挂载在该 phase 后面的 phase 上。自定义的 handler 需要遵守 nginx 对不同 phase 的功能划分,但不是必需的。除去 4 个不能挂载的 phase 和 log phase,还有如下 6 个 phase 可以挂载
NGX_HTTP_POST_READ_PHASE
NGX_HTTP_SERVER_REWRITE_PHASE
NGX_HTTP_REWRITE_PHASE
NGX_HTTP_PREACCESS_PHASE
NGX_HTTP_ACCESS_PHASE
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,这个过程分为:
初始化ngx_http_connection_t结构
设置可读回调方法为:ngx_http_wait_request_handler
设置可写回调方法为空:ngx_http_empty_handler
如果可读已经就绪,则执行ngx_http_wait_request_handler
将可读事件加入到定时器中以监控连接是否超时
将可读事件添加到epoll中
第一次可读事件处理
这时执行的是 ngx_http_wait_request_handler 这个步骤分为:
判断请求是否超时
创建接受缓冲区
创建并初始化ngx_http_request_t
更新ngx_http_request_t请求开始时间
调用ngx_http_process_request_line
接收请求行
这个步骤由 ngx_http_process_request_line 完成,由于一次调用未必能全部读取完成,所以,设置了:rev→handler = ngx_http_process_request_line; Top of Form,这个过程分为:
判断请求是否超时
读取请求头
解析请求行
处理请求uri
设置读hander为:ngx_http_process_request_headers;
处理请求头
处理 HTTP 请求
处理完请求头,最终调用 ngx_http_process_request:
由于请求头部已经接收完,所以删除定时器 if (c->read->timer_set) { ngx_del_timer(c->read); }
由于不再接收请求行和头部,读写回调全部设置为:
c->read->handler = ngx_http_request_handler;
c->write->handler = ngx_http_request_handler;
将 request 的 read_event_hander 设置为什么都不做:r->read_event_handler = ngx_http_block_reading;
ngx_http_handler
根据internal标志判断是否要从定向
设置请求的写事件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,上面已经介绍过了。
调用ngx_http_run_posted_requests
处理子请求
ngx_http_run_posted_requests 方法根据链表顺序调用了 write_event_handler
处理 HTTP 包体
该工具方法为 ngx_http_read_client_request_body 执行过程如下:
原始请求计数器+1: r→main→count++
执行各模块子请求的post_handler
接受ngx_http_request_body_t
假如一次性没有接受完,则设置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
1、能否简单对比下 Apache 和 Nginx?
工作模式的不同。ngx 所有的请求都是异步,非阻塞的方式,并且采用了 epoll 模型。Apache 是线程池的模型。
Apache 依赖了很多三方库,ngx 则是全部自己实现。
ngx 的设计全部是模块化的,比较轻量级。
ngx 配置比较简洁。
ngx 对机器的利用率较高,一切设计都是为了节省内存和提高性能。
2、请问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));
}
这个过程会重启工作线程。
3、使用线程池后,Nginx 的性能提升 9 倍是真的吗?
我认为线程池可以从某种程度提升响应时间,毕竟单个线程处理多个请求,速度再快也是先来后到排序的。当然,这个提升也需要在编写模块的时候全部符合作者异步,非阻塞的思路,比如转成 subrequest。
4、用 Nginx 本身的模块来做 ddos 攻击是否合适?如果不合适业界是否有成型的 Nginx 解决方案 or 其它,想参考一下。目前我们的做法是用的后台生成验证码的方式(app)
ngx 做 ddos 攻击的话,就需要在 tcp 层来做了。不能直接编写 ngx http 模块的方法。否则失去了抗 ddos 攻击的意义。
至于业界的方案这个我到不是很清楚,之前在阿里有个基于 Apache 的 humock,是在 tcp 层做的防攻击。
5、Nginx 不是多进程并发处理么?
ngx 的多个进程是指多个工作进程,分别来管理一份 epoll 事件池的。
通过这样的方式可以尽量利用 cpu,而且 ngx 的全部设计思路就是围绕异步,非阻塞。类似 Redis 的事件机制。
6、不是说静态文件处理,Apache 比 Nginx 更有优势吗?
之前我用 ab 做过性能测试,一个 1M 大小的页面,性能 ngx 比 Apache 更强一些。不知道这个 Apache 比 ngx 更有优势的结论是如何的出来的。理论上 epoll 模型会比多线程模型开销更小一些。
7、针对问题 5,是说每个工作进程再搞个线程池?
每个工作进程不需要线程池,epoll 就是个事件池子,等事件就绪之后,就可以回调你之前注册的回调函数
8、如果分配工作进程数和 cpu 核数匹配,因为工作进程的工作方式是异步的,不存在阻塞,所以再每个工作进程再搞个线程池有意义么?,因为 cpu 核都在 run,没有空闲。
我觉得还是有意义的,毕竟 CPU 不会给你那几个进程独占,时间片是会切换的,线程池可以提高吞吐率。当然这需要做下测试,另外需要看下使用场景。
9、针对问题3,使用线程池,是那块使用呢?
Nginx 引入线程池是为了解决因为某些长时间阻塞的调用导致性能下降的问题。可以在编译的时候加选项开启线程池。
10、Nginx的高性能,高吞吐。最主要由什么设计决定呢? 异步非阻塞?
异步非阻塞,绝对是一个核心点,另外 ngx 对任何使用内存的地方非常抠门,都是在框架上搞定了。并且能不使用就不使用,所以在你自己编写模块的时候,你都是从 pool 中拿到的内存。释放也是 ngx 帮你搞定了。而且 ngx 所有的数据结构都是结合场景精心设计的。脱离 ngx 这个场景,你在其他地方都会觉得很怪异。
更多了解陈科介绍的 nginx 源码分析,可以参阅本次分享上半部分:
最后小编补充与高可用架构读者相关的 2 条消息:
为了方便广大读者阅读公众号文章,经过小编努力打包与重新排版,制作成专刊在各大电子平台发表,包含公众号之前发表的系列技术精华文章。目前「高可用架构」专刊已经在多看平台上线,请搜索「高可用架构」下载阅读。Kindle、掌阅等其他阅读平台也将陆续上线。
小编由于春节抢红包太频繁,腱鞘炎间歇性发作,周日( 2 月 21 日)推送的图片有误,向读者致歉。
本文策划邓启明,编辑王杰,审校 Tim Yang,如需第一时间获取高可用架构分享文章请关注公众号。转载请注明来自高可用架构 「ArchNotes」微信公众号及包含以下二维码。