今天来分析一下Nginx定时器实现原理。
一、Nginx定时器
在网络通信中,客户端给服务端发送一个请求,如果服务端一直没有响应,那么客户端是不能一直傻傻的等待的,一般情况下,客户端需要设置一个超时定时器,当定时器超时后客户端需要进行后续处理。
Nginx定期器常用在upstream、子请求中,这些在分析http模块时已经介绍。
二、定时器实现
2.1 定时器
Nginx中定时器事件是一种事件,因此Nginx也将其统一到ngx_event_t中。该结构在《XX》介绍过,这里只把相关结构体成员拿出来备注说明一下:
typedef struct ngx_event_s ngx_event_t;
struct ngx_event_s {
...
unsigned timedout : 1; /* 1表示已经超时 */
unsigned timer_set : 1;/* 1表示当前事件为定时器事件 */
ngx_rbtree_node_t timer; /* 红黑树节点 定时器事件存储结构*/
...
}
Nginx中定时器存储结构采用的是红黑树,全局变量为ngx_event_timer_rbtree。对于红黑树原理以及具体源码不想介绍,网上介绍红黑树的文章很多,而且对于红黑树这中变态级数据结构,如果一周不看就会忘的很彻底。大家只要会用就行了。
2.2 源码介绍
Nginx定时器相关文件是ngx_event_timer.c,ngx_event_timer.h文件,具体内容不并很多。
/*
* the event timer rbtree may contain the duplicate keys, however,
* it should not be a problem, because we use the rbtree to find
* a minimum timer value only
* 初始化红黑树
*/
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;
}
/**
* 查找时间最小的那个树节点 用于epoll等待时间
* @return 当返回-1 表示永久阻塞
*/
ngx_msec_t
ngx_event_find_timer(void)
{
ngx_msec_int_t timer;//实际int值
ngx_rbtree_node_t *node, *root, *sentinel;
if (ngx_event_timer_rbtree.root == &ngx_event_timer_sentinel) {//表示没有定时任务
return NGX_TIMER_INFINITE;//返回永久不超时
}
/* 找到最近,要超时的事件 并且返回它所包含的时间 */
root = ngx_event_timer_rbtree.root;
sentinel = ngx_event_timer_rbtree.sentinel;
node = ngx_rbtree_min(root, sentinel);
timer = (ngx_msec_int_t) (node->key - ngx_current_msec);
return (ngx_msec_t) (timer > 0 ? timer : 0);
}
/**
* 循环遍历 注册的超时事件
* 如果有超时事件 则执行回调函数
*/
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;
if (root == sentinel) {
return;
}
node = ngx_rbtree_min(root, sentinel);
/* node->key > ngx_current_msec */
if ((ngx_msec_int_t) (node->key - ngx_current_msec) > 0) {
return;
}
/* 获取event事件 */
ev = (ngx_event_t *) ((char *) node - offsetof(ngx_event_t, timer));
ngx_log_debug2(NGX_LOG_DEBUG_EVENT, ev->log, 0,
"event timer del: %d: %M",
ngx_event_ident(ev->data), ev->timer.key);
ngx_rbtree_delete(&ngx_event_timer_rbtree, &ev->timer); /* 从树中摘除树节点 */
#if (NGX_DEBUG)
ev->timer.left = NULL;
ev->timer.right = NULL;
ev->timer.parent = NULL;
#endif
ev->timer_set = 0;
ev->timedout = 1;
ev->handler(ev);//执行超时处理事件 指定回调函数
}
}
此函数用于定时器超时后,执行回调函数。执行ev->handler指向回调函数,由应用层软件实现。
/**
* 添加定时器任务
* @param ev 事件
* @param timer 等待时间 int值
*/
static ngx_inline void
ngx_event_add_timer(ngx_event_t *ev, ngx_msec_t timer)
{
ngx_msec_t key;
ngx_msec_int_t diff;
key = ngx_current_msec + timer;
if (ev->timer_set) {//表示在二叉树已经注册了超时事件
/*
* Use a previous timer value if difference between it and a new
* value is less than NGX_TIMER_LAZY_DELAY milliseconds: this allows
* to minimize the rbtree operations for fast connections.
* 当新事件与旧事件,时间差值小于300ms则使用旧事件 不在创建新事件
*/
diff = (ngx_msec_int_t) (key - ev->timer.key);
if (ngx_abs(diff) < NGX_TIMER_LAZY_DELAY) {//沿用旧事件,不在创建新节点
ngx_log_debug3(NGX_LOG_DEBUG_EVENT, ev->log, 0,
"event timer: %d, old: %M, new: %M",
ngx_event_ident(ev->data), ev->timer.key, key);
return;
}
ngx_del_timer(ev);
}
ev->timer.key = key;
ngx_log_debug3(NGX_LOG_DEBUG_EVENT, ev->log, 0,
"event timer add: %d: %M:%M",
ngx_event_ident(ev->data), timer, ev->timer.key);
ngx_rbtree_insert(&ngx_event_timer_rbtree, &ev->timer);//注册到红黑树中 实际为插入到二叉树中
ev->timer_set = 1;//标记当前事件为定时器事件
}
/**
* 删除定时器事件
* @param 待删除事件
*/
static ngx_inline void
ngx_event_del_timer(ngx_event_t *ev)
{
ngx_log_debug2(NGX_LOG_DEBUG_EVENT, ev->log, 0,
"event timer del: %d: %M",
ngx_event_ident(ev->data), ev->timer.key);
ngx_rbtree_delete(&ngx_event_timer_rbtree, &ev->timer);//从红黑树中删除
#if (NGX_DEBUG)
ev->timer.left = NULL;
ev->timer.right = NULL;
ev->timer.parent = NULL;
#endif
ev->timer_set = 0;
}
上面介绍添加与删除定时器,主要是往红黑树中插入节点、删除节点。逻辑并不是很复杂。
三、使用场景
为了提升Nginx性能,尽量少调用系统调用gettimeofday(),Nginx实现了时间缓存功能。同时为了提升时间精度,Nginx也支持在nginx.conf中指定时间精度timer_resolution。
在分析事件模型时,就已经介绍过时间这部分处理。今天在仔细深入探讨一下这部分源码
3.1、定时器
在Nginx启动完成后,worker进程会进入无限循环中ngx_worker_process_cycle,然后Nginx会在ngx_process_events_and_timers阻塞/等待超时,如下:
/**
* 此函数是处理事件的入口函数,所有业务流程起始函数
* 《深入理解Nginx模块开发与架构解析》 P331
*/
void ngx_process_events_and_timers(ngx_cycle_t *cycle)
{
ngx_uint_t flags;
ngx_msec_t timer, delta;
if (ngx_timer_resolution)
{//用户指定时间精度
timer = NGX_TIMER_INFINITE;
flags = 0;
}
else
{
//获取下一个超时时间 如果二叉树中没有超时事件则返回-1 代表永久不超时
timer = ngx_event_find_timer();
flags = NGX_UPDATE_TIME; //表示需要更新时间缓存
#if (NGX_WIN32)
/* handle signals from master in case of network inactivity */
if (timer == NGX_TIMER_INFINITE || timer > 500)
{
timer = 500;
}
#endif
}
...
/* 记录时间差 */
delta = ngx_current_msec;
/**
* 如果是epoll模型 此处实际调用函数是ngx_epoll_process_events
* 阻塞在epoll_wait
*/
(void)ngx_process_events(cycle, timer, flags);
delta = ngx_current_msec - delta; /* 记录时间差 */
...
if (delta)
{//时间差 表示时间超时,需要处理超时事件
ngx_event_expire_timers();
}
ngx_event_process_posted(cycle, &ngx_posted_events);
}
说明:
1)当ngx_timer_resolution非0时,表示nginx.conf配置文件中设置时间精度,定时器超时严格按照该时间设定。否则在红黑树中查找最近要超时节点(红黑树节点key值为时间),换句话说就是按照时间最小值设定超时时间。
2)ngx_process_events(cycle, timer, flags);该方法会调用epoll_wait,epoll_wait最后一个参数表示等待时间,如果为-1则表示永久不超时。如下:参数timer则是ngx_process_events_and_timers函数中变量timer
static ngx_int_t
ngx_epoll_process_events(ngx_cycle_t *cycle, ngx_msec_t timer, ngx_uint_t flags)
{
...
/**
* timer不是固定不变的,如果没有任何事件发生(空闲期),
* timer可能是NGX_TIMER_INFINITE 即表示永久阻塞
*/
events = epoll_wait(ep, event_list, (int)nevents, timer);
...
}
Nginx通过epoll_wait,巧妙的实现定时器任务。网上也有settimer、select等方式,整体来说是大同小异的。在进入事件循环之前,记录时间点保存在delta中,当事件循环退出之后进行时间差比较,当delta大于0表示遍历红黑树节点,查找已经超时的节点,调用回调函数。
3.2、时间精度
上一节中不知道大家有没有注意到,当ngx_timer_resolution不为0时表示采用时间精度,那么Nginx会将timer设置为-1,如下:
if (ngx_timer_resolution)
{//用户指定时间精度,超时事件由SIGALRM触发
timer = NGX_TIMER_INFINITE;// -1
flags = 0;
}
如果timer是-1,那么在epoll_wait岂不会永远不能结束了?实则不然。我们来看一下,ngx_timer_resolution相关代码,代码在ngx_event_module_init中,如下:
/**
* nginx.conf开启毫秒定时器精度
*/
if (ngx_timer_resolution && !(ngx_event_flags & NGX_USE_TIMER_EVENT))
{
struct sigaction sa;
struct itimerval itv;
/* 注册信号 */
ngx_memzero(&sa, sizeof(struct sigaction));
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(SIGALRM) failed");
return NGX_ERROR;
}
itv.it_interval.tv_sec = ngx_timer_resolution / 1000;
itv.it_interval.tv_usec = (ngx_timer_resolution % 1000) * 1000;
itv.it_value.tv_sec = ngx_timer_resolution / 1000;
itv.it_value.tv_usec = (ngx_timer_resolution % 1000) * 1000;
//启动定时器 当超时后会产生SIGALRM信号
if (setitimer(ITIMER_REAL, &itv, NULL) == -1)
{
ngx_log_error(NGX_LOG_ALERT, cycle->log, ngx_errno,
"setitimer() failed");
}
}
通过上面代码可知,当开启时间精度,Nginx会向操作系统注册SIGALRM信号, 当时间流失ngx_timer_resolution毫秒后,操作系统就会产生SIGALRM信号中断,然后进程转而调用中断处理函数,当中断处理函数结束后epoll_wait就会返回,返回值为-1且errno为EINTR。以上内容需要了解,这样整个流程就串起来了。
四、总结
通常定时器实现主要需要两点:
1)注册定时器,可以使用系统提供setitimer,select,epoll或者像Nginx一样实现在自己的定时器管理模块。
2)定时器回调函数,这个也是必须的。