Nginx 的超时处理 作者: yankai0219 0. 序 1. 综述 1) Nginx 管理时间采用的数据结构是红黑树 2) Nginx 检测那些等待处理的事件对象是否已经超时的两种方案 3) Nginx 是如何设置这两种方案的呢 2. 详细分析: 1) 初始化 2) 选择使用哪种超时判定方案: 3) ngx_epoll_process_events 函数 3. 定时事件的执行。 4. 总体流程图 5. 程序实例 本程序在经典的 hello world 模块上进行拓展, 增加了简单的定时器, 实现每隔 1s 输出 hello, yankai^^^V^^^ 文件名: ngx_http_hello_module. c 文件名: config nginx 编译命令 配置文件: nginx. conf 内容 运行 6. 参考文章 0.序 Nginx 的超时处理贯穿整个 Nginx, 是十分重要的一个概念。 因此必须对其有一个深刻的理解。 本文参考网上诸多文章, 将对超时处理给出一个详尽的处理。 1 .综述 1 ) Nginx 管理时间采用的数据结构是红黑树。 nginx 利用红黑树来组织那些等待处理并且需要关注其是否超时的事件对象。 将红黑树节点作为 ngx_event_t 的值成员, 而非指针成员, 则通过红黑树找到最先到期的节点后, 通过 offsetof 宏可以直接得到 ngx_event_t 本身. 一句话概括: 那些加了 定时处理的事件都会加入到红黑树中进行管理。 2) Nginx 检测那些等待处理的事件对象是否已经超时的两种方案: (1 ) 常规的定时检测机制: 设置定时器, 每过一定的时间就对红黑树管理的所有事件对象进行一次超时检测 (2) 经历距离当前最快发生超时的事件对象的时间就进行一次超时检测。 比如有3 个事件, 事件 a、 b、 c, 超时时间 5s,6s,7s,那么距离当前最快发生超时的事件对象为 a,时间为 5s, 那么 5s 后就会进行一次超时检测。 3) Nginx 是如何设置这两种方案的呢? (1 ) 在配置文件中, 通过使用 timer_resulotion xxms;设置使用常规的定时检测机制。 如果没有设置 timer_resulotion 指令, 那么 Nginx 使用 2)(2)中方案。 在 ngx_process_events_and_timers 函数中, 通过下面代码可以确定使用哪种方案。如果设置了 tim er_resolution 方案, 则 timer=NGX_TI MER_INFINITE(其值为-1 ) , 那么在 events = epoll_wait(ep, event_list, (int) nevents, timer);中 epoll_wait 就会无限时间等待, 直到定时器产生 SIGALRM 信号, epoll_wait 才中断退出。 为何定时器会产生 SIGALRM信号, 我们接着分析。 /* ngx_process_events_and_timers 函数* / if (ngx_timer_resolution) { timer = NGX_TIMER_INFINI TE; flags = 0; } else { timer = ngx_event_find_timer(); flags = NGX_UPDATE_TIME; } 4) 接着分析, 为何在 epoll_wait 无限时间等待时, 定时器会产生 SIGALRM 信号, 这儿就必须去看 ngx_event_process_init 函数的实现了。 通过查看下面代码, 我们发现:通过 sigaction 指定 SIGALRM 的信号处理函数为 ngx_timer_signal_handler; 通过系统调用setitimer 设置定时器的时间。 当 setitim er 设定的时间耗尽以后, 会发送 SI GALARM 信号, 由于指定信号处理函数, 因此会调用 ngx_tim er_signal_handler 函数。 至于为何时间耗尽以后会发送 SIGALARM 信号, 请 man setitimer, 这是 setitimer 函数的属性。 if (ngx_timer_resolution&& !(ngx_event_flags& NGX_USE_TIMER_EVENT)) { structsigaction sa; structitimerval itv; ngx_memzero(&sa, sizeof(structsigaction)); sa.sa_handler = ngx_timer_signal_handler; sigemptyset(&sa.sa_mask); if (sigaction(SIGALRM, &sa, NULL) == -1) { ngx_log_error(NGX_LOG_ALERT, cycle->log, ngx_errno, "sigaction(SI GALRM) failed"); return NGX_ERROR; } itv.it_interval.tv_sec = ngx_timer_resolution / 1000; itv.it_interval.tv_usec = (ngx_timer_resolution % 1000) * 1 000; itv.it_value.tv_sec = ngx_timer_resolution / 1000; itv.it_value.tv_usec = (ngx_timer_resolution % 1000 ) * 1000; if (setitimer(ITIMER_REAL, &itv, NULL) = = -1) { ngx_log_error(NGX_LOG_ALERT, cycle->log, ngx_errno, "setitimer() failed"); } } 2.详细分析: 1 ) 初始化: (1 ) 在 ngx_event_process_init 函数中, 初始化红黑树。 其调用关系为ngx_worker_process_cycle--->ngx_worker_process_init---->ngx_event_process_init if (ngx_event_timer_init(cycle->log) == NGX_ERROR) { return NGX_ERROR; } ngx_int_t ngx_event_timer_init(ngx_log_t * log) { ngx_rbtree_init(&ngx_event_timer_rbtree, &ngx_event_timer_sentinel, ngx_rbtree_insert_timer_value); return NGX_OK; } (2) 如果使用了 timer_resulotion 超时判定方案的话, 在ngx_event_process_init 函数中会对定时器进行一些设定。 通过 sigaction 指定 SIGALRM 的 信号处理函数为 ngx_timer_signal_handler; 通过系统调用 setitimer 设置定时器的时间。 这部分内容已经在 1 .综述 4) 中详细介绍 2) 选择使用哪种超时判定方案: 在 ngx_process_events_and_timers 函数中, 选择使用哪种超时判定方案: 其调用关系为 ngx_worker_process_cycle--->ngx_process_events_and_timers if (ngx_timer_resolution) { timer = NGX_TIMER_INFINI TE; flags = 0; } else { timer = ngx_event_find_timer(); flags = NGX_UPDATE_TIME; } 3) 紧接着就是进入 ngx_process_events 函数进行处理即ngx_epoll_process_events 函数。 < 1 > 当采用不同超时判定方案时, epoll_wait 的反应是不同的。 分别进行说明: (1 ) 采用 1 ) 常规定时方案 timer_resolution: a) 当采用 timer_resolution 时, 在ngx_process_events_and_timers 中, timer = NGX_TIMER_INFI NITE (其值为-1 ) ; flags = 0;对于 epoll_wait 而言, 就是无限等待。 b)当 setitimer 设定的时间耗尽时, 会产生 SIGALARM 信号, 此时会调用信号处理函数 ngx_timer_signal_handler 将 ngx_event_timer_alarm 赋值为 1 . c)产生 SIGALARM 信号, 将 epoll_wait 中断, 此时 epoll_wait报错退出, ngx_errno 中保存出错信息, 如果 ngx_errno 为 EI NTR, 那么 epoll_wait 就是因为中断退出。 接着判断中断是否为 SIGALARM 信号, 因为只有中断是 SIGALARM 信号时,才会调用 ngx_timer_signal_handler 将 ngx_event_timer_alarm 赋值为 1 .然后退出返回NGX_OK。 (2)采用经历距离当前最快发生超时的事件对象的时间就进行一次超时检测方案. a)当采用该方案时, 在 ngx_process_events_and_timers中 timer = ngx_event_find_timer(); flags = NGX_UPDATE_TI ME;epoll_wait 等待时间为红黑树中最小的等待时间。 b)当 timer 耗尽后, 由于超时 epoll_wait 会退出。此时 events=0;当 timer! =-1 时, 退出返回 NGX_OK。 < 2> 不管是采用哪种超时判定方案, 都会进行判断是否更新时间缓存。当为方案 2) 或者因为 SI GALARM 中断时, 都会更新时间缓存。 if (flags & NGX_UPDATE_TIME | | ngx_event_timer_alarm) { ngx_time_update(); } < 3> 只要在未超时时, 发生事件, 那么 events 大于零, nginx 就会去处理事件。 events = epoll_wait(ep, event_list, (int) nevents, timer) ; err = (events == -1) ? ngx_errno : 0; if (flags & NGX_UPDATE_TIME | | ngx_event_timer_alarm) { ngx_time_update() ; } if (err) {/*由于中断等错误*/ if (err == NGX_EINTR) { if (ngx_event_timer_alarm) { ngx_event_timer_alarm = 0; return NGX_OK; } } else {. . . . . . . . . . } return NGX_ERROR; } if (events == 0) {/*由于超时*/ if (timer != NGX_TIMER_INFINITE) { return NGX_OK; } return NGX_ERROR; } /*events > 0*/ for (i = 0; i< events; i++) { . . . . . . . . . . . . . . . . 3.定时事件的执行。 (void) ngx_process_events(cycle, timer, flags) ; delta = ngx_current_msec - delta; . . . . . . . . . . . . . . . . . if (delta) {/* delta其实是用来反应epoll_wait 阻塞了多长时间, 所以 delta等于 0 时表示本次 epoll_wait 几乎没有阻塞, 所以上一次的事件循环和本次事件循环是在几乎 0 延迟的时间内完成的, 当前时间没有发生改变, 故不需要去检查定时事件。 */ ngx_event_expire_timers() ; } ngx_event_expire_timers 函数就是遍历一下定时器红黑树, 找出超时的定时事件并执行事件的回调函数。 由于 nginx 利用红黑树来组织管理超时事件, 因此检测是否有事件超时并不需要遍历所有的事件对象, 而直接找到最近的即将超时的事件对象, 判断其是否超时, 如果超时则进行相应的超时处理, 处理完了再判断第二近的即将超时的事件对象, 如此反复, 直到遇到某个事件还未超时或所有事件都超时并已处理就结束检测。 这就是超时检测与处理函数ngx_event_expire_timers 的大致逻辑, 当然还有一些其它额外的动作, 比如将超时了的事件对象的相关字段置位、 超时处理、 事件对象移出事件计时红黑树等等. ngx_event_expire_timer 函数的部分代码: void ngx_event_expire_timers(void) { ngx_event_t *ev; ngx_rbtree_node_t *node, *root, *sentinel; sentinel = ngx_event_timer_rbtree. sentinel; for ( ; ; ) { root = ngx_event_timer_rbtree. root; node = ngx_rbtree_min(root, sentinel) ; if ((ngx_msec_int_t) (node->key - ngx_current_msec) <= 0) { ev = (ngx_event_t *) ((char *) node - offsetof(ngx_event_t, timer) ) ; ngx_rbtree_delete(&ngx_event_timer_rbtree, &ev->timer) ; ngx_mutex_unlock(ngx_event_timer_mutex) ; ev->timer_set = 0; ev->timedout = 1; ev->handler(ev) ; continue; } break; } } 4.总体流程图 流程图我采用 nginx 启动流程总分析中的图 5.程序实例 本程序在经典的 hello world 模块上进行拓展, 增加了简单的定时器, 实现每隔 1 s 输出hello,yankai^ ^ ^ V^ ^ ^ /* 说明: ngx_http_hello_module.c and config 这两个文件放在目录 ngx_http_hello_module 下 本程序在经典的 hello world 模块上进行拓展, 增加了简单的定时器, 实现每隔 1s 输出hello,yankai^^^V^^^ 实现方法: 在模块的 init_process 处 ngx_add_timer; 参考博文: Nginx 定时器的实现及定时事件的使用 */ /* 文件名: ngx_http_hello_module.c */ #include <ngx_config.h> #include <ngx_core.h> #include <ngx_http.h> #include <nginx.h> #include <time.h> #define YK_INIT_PROCESS 1 char * ngx_hello_hello(ngx_conf_t *cf, ngx_command_t *cmd, void *conf); ngx_int_t ngx_http_hello_handler(ngx_http_request_t *r); #if YK_INIT_PROCESS static ngx_int_t ngx_http_hello_process_init( ngx_cycle_t *cycle); void ngx_http_hello_print( ngx_event_t *ev); #endif static ngx_command_t ngx_http_hello_commands[]={ { ngx_string( "hello"), NGX_HTTP_LOC_CONF|NGX_CONF_NOARGS, ngx_hello_hello, 0, 0, NULL }, ngx_null_command }; static ngx_http_module_t ngx_http_hello_module_ctx={ NULL,/*preconfiguration*/ N...