Nginx事件管理之定时器事件

时间:2022-09-04 00:10:47

1. 缓存时间

1.1 管理

Nginx 中的每个进程都会单独地管理当前时间。ngx_time_t 结构体是缓存时间变量的类型:

typedef struct {
    /* 格林威治时间1970年1月1日凌晨0点0分0秒到当前时间的秒数 */
    time_t      sec;
    /* sec成员只能精确到秒,msec则是当前时间相对于sec的毫秒偏移量 */
    ngx_uint_t  msec;
    /* 时区 */
    ngx_int_t   gmtoff;
}ngx_time_t;

Nginx 定义了以下全局变量用于缓存时间:

/* 格林威治时间1970年1月1日凌晨0点0分0秒到当前时间的毫秒数 */
volatile ngx_msec_t      ngx_current_msec;
/* ngx_time_t结构体形式的当前时间 */
volatile ngx_time_t     *ngx_cached_time;
/* 用于记录error_log的当前时间字符串,它的格式类似于:"1970/09/28 12:00:00" */
volatile ngx_str_t       ngx_cached_err_log_time;
/* 用于记录HTTP相关的当前时间字符串,它的格式类似于:"Mon, 28 Sep 1970 06:00:00 GMT" */
volatile ngx_str_t       ngx_cached_http_time;
/* 用于记录HTTP日志的当前时间字符串,它的格式类似于:"28/Sep1970:12:00:00 +0600" */
volatile ngx_str_t       ngx_cached_http_log_time;
/* 以ISO 8601标准格式记录下的字符串形式的当前时间 */
volatile ngx_str_t       ngx_cached_http_log_iso8601;
volatile ngx_str_t       ngx_cached_syslog_time;

对于 worker 进程而言,除了 Nginx 启动时更新一次时间外,任何更新时间的操作都只能由 ngx_epoll_process_events
方法执行。在该方法中,当检测到 flags 参数中有 NGX_UPDATE_TIME 标志,或者 ngx_event_timer_alarm 标志位为 1
时,就会调用 ngx_time_update 方法更新缓存时间。

/*
 * 执行意义:
 * 使用gettimeofday调用以系统时间更新缓存的时间,上述的ngx_current_msec、ngx_cached_time、
 * ngx_cached_err_log_time、ngx_cached_http_time、ngx_cached_http_log_time、
 * ngx_cached_http_log_iso8601、ngx_cached_syslog_time这几个全局变量都会得到刷新 */
void ngx_time_update(void)
{
    u_char          *p0, *p1, *p2, *p3, *p4;
    ngx_tm_t         tm, gmt;
    time_t           sec;
    ngx_uint_t       msec;
    ngx_time_t      *tp;
    struct timeval   tv;

    if (!ngx_trylock(&ngx_time_lock))
    {
        return;
    }

    ngx_gettimeofday(&tv);

    sec = tv.tv_sec;
    msec = tv.tv_usec / 1000;

    ngx_current_msec = (ngx_msec_t)sec * 1000 + msec;

    tp = &cached_time[slot];

    if (tp->sec == sec)
    {
        tp->msec = msec;
        ngx_unlock(&ngx_time_lock);
        return;
    }

    if (slot == NGX_TIME_SLOTS - 1)
    {
        slot = 0;
    }
    else
    {
        slot++;
    }

    tp = &cached_time[slot];

    tp->sec = sec;
    tp->msec = msec;

    ngx_gmtime(sec, &gmt);

    p0 = &cached_http_time[slot][0];

    (void)ngx_sprintf(p0, "%s, %02d %s %4d %02d:%02d:%02d GMT", 
                      week[gmt.ngx_tm_wday], gmt.ngx_tm_mday,
                      months[gmt.ngx_tm_mon - 1], gmt.ngx_tm_year,
                      gmt.ngx_tm_hour, gmt.ngx_tm_min, gmt.ngx_tm_sec);

#if (NGX_HAVE_GETTIMEZONE)

    tp->gmtoff = ngx_gettimezone();
    ngx_gmtime(sec + tp->gmtoff * 60, &tm);

#elif (NGX_HAVE_GMTOFF)

    ngx_localtime(sec, &tm);
    cached_gmtoff = (ngx_int_t)(tm.ngx_tm_gmtoff / 60);
    tp->gmtoff = cached_gmtoff;

#else

    ngx_localtime(sec, &tm);
    cached_gmtoff = ngx_timezone(tm.ngx_tm.isdst);
    tp->gmtoff = cached_gmtoff;

#endif

    p1 = &cached_err_log_time[slot][0];

    (void)ngx_sprintf(p1, "%4d/%02d/%02d %02d:%02d:%02d", 
                      tm.ngx_tm_year, tm.ngx_tm_mon,
                       tm.ngx_tm_mday, tm.ngx_tm_hour,
                       tm.ngx_tm_min, tm.ngx_tm_sec);

    p2 = &cached_http_log_time[slot][0];

    (void) ngx_sprintf(p2, "%02d/%s/%d:%02d:%02d:%02d %c%02i%02i",
                       tm.ngx_tm_mday, months[tm.ngx_tm_mon - 1],
                       tm.ngx_tm_year, tm.ngx_tm_hour,
                       tm.ngx_tm_min, tm.ngx_tm_sec,
                       tp->gmtoff < 0 ? '-' : '+',
                       ngx_abs(tp->gmtoff / 60), ngx_abs(tp->gmtoff % 60));

    p3 = &cached_http_log_iso8601[slot][0];

    (void) ngx_sprintf(p3, "%4d-%02d-%02dT%02d:%02d:%02d%c%02i:%02i",
                       tm.ngx_tm_year, tm.ngx_tm_mon,
                       tm.ngx_tm_mday, tm.ngx_tm_hour,
                       tm.ngx_tm_min, tm.ngx_tm_sec,
                       tp->gmtoff < 0 ? '-' : '+',
                       ngx_abs(tp->gmtoff / 60), ngx_abs(tp->gmtoff % 60));

    p4 = &cached_syslog_time[slot][0];

    (void) ngx_sprintf(p4, "%s %2d %02d:%02d:%02d",
                       months[tm.ngx_tm_mon - 1], tm.ngx_tm_mday,
                       tm.ngx_tm_hour, tm.ngx_tm_min, tm.ngx_tm_sec);

    ngx_memory_barrier();

    ngx_cached_time = tp;
    ngx_cached_http_time.data = p0;
    ngx_cached_err_log_time.data = p1;
    ngx_cached_http_log_time.data = p2;
    ngx_cached_http_log_iso8601.data = p3;
    ngx_cached_syslog_time.data = p4;

    ngx_unlock(&ngx_time_lock);
}

1.2 精度

Nginx 提供了设置更新缓存时间频率的功能(也就是至少每隔 timer_resolution 毫秒必须更新一次缓存时间),通过在
nginx.conf 文件中的 timer_resolution 配置项可以设置更新的最小频率,这样就保证了缓存时间的精度。

ngx_event_core_module 模块在调用 ngx_event_process_init 方法初始化时会使用 setitimer 系统调用告诉内核每隔
timer_resolution 毫秒调用一次 ngx_timer_signal_handler 方法。而 ngx_timer_signal_handler 方法则会将
ngx_event_timer_alarm 标志位设为 1,这样,一旦调用 ngx_epoll_process_events 方法时,如果间隔的时间超过
timer_resolution 毫秒,那么就会调用 ngx_time_update 方法更新缓存时间。

但如果远超过 timer_resolution 毫秒的时间内 ngx_epoll_process_events 方法都得不到调用,那时间精度如何保证?
在这种情况下,Nginx 只能从事件模块对 ngx_event_actions 中的 process_events 接口的实现来保证时间精度了。
process_events 方法的第 2 个参数 timer 表示收集事件时的最长等待时间。例如,在 epoll 模块下,这个 timer 就是
epoll_wait 调用时传入的超时时间参数。如果没有设置 timer_resolution,一般情况下,process_events 方法的 timer
参数都是大于 0 且小于 500 毫秒的值,而如果在设置了 timer_resolution 后,这个 timer 参数就是 -1,它表示如果
epoll_wait 等调用检测不到已经发生的事件,将不等待而是立刻返回,这样就控制了事件精度。但如果某个事件消费模块
的回调方法执行时占用的时间过长,时间精度还是难以得到保证的。

2. 定时器

2.1 概述

定时器是通过一棵红黑树实现的。

/* Nginx设置了两个全局变量以便在程序的任何地方都可以快速地访问到这颗红黑树 */

/* ngx_event_timer_rbtree封装了整颗红黑树结构 */
ngx_rbtree_t              ngx_event_timer_rbtree;
/* ngx_event_timer_sentinel属于红黑树节点类型变量,在红黑树的操作过程中被当做哨兵节点使用 */
static ngx_rbtree_node_t  ngx_event_timer_sentinel;

这棵红黑树中的每个节点都是 ngx_event_t 事件中的 timer 成员,而 ngx_rbtree_node_t 节点的关键字就是事件的超时
时间,以这个超时时间的大小组成了二叉排序树 ngx_event_timer_rbtree。这样,如果需要找出最有可能超时的事件,那
么将 ngx_event_timer_rbtree 树中最左边的节点取出来即可。只要用当前时间去比较这个最左边节点的超时时间,就会
知道这个事件有没有触发超时,如果还没有触发超时,那么会知道最少还要经过多少毫秒满足超时条件而触发超时。

2.2 提供的接口

2.2.1 ngx_event_timer_init:初始化定时器

/* 红黑树(即定时器)的初始化函数 */
ngx_int_t ngx_event_timer_init(ngx_log_t *log)
{
    /* ngx_event_timer_rbtree 和 ngx_event_timer_sentinel 是两个全局变量,前者指向
     * 整颗红黑树,后者指向了哨兵节点, ngx_rbtree_insert_timer_value 函数指针则为
     * 将元素插入这棵红黑树的方法 */
    ngx_rbtree_init(&ngx_event_timer_rbtree, &ngx_event_timer_sentinel, ngx_rbtree_insert_timer_value);

    return NGX_OK;
}

2.2.2 ngx_event_add_timer: 添加一个定时事件

/*
 * 参数含义:
 * - ev:是需要操作的事件
 * - timer:单位是毫秒,它告诉定时器事件ev希望timer毫秒后超时,同时需要回调ev的handler方法
 *
 * 执行意义:
 * 添加一个定时器事件,超时时间为 timer 毫秒
 */
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_DELY milliseconds: this allows
         * to minimize the rbtree operations for fast connections. */

        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);

    /* 将事件添加到红黑树中
     * 这种添加是间接性的,每个事件对象封装结构体中都有一个timer字段,
     * 其为ngx_rbtree_node_t 类型变量,加入到红黑树中就是该字段,
     * 而非事件对象结构体本身。后面要获取该事件结构体时可以通过利用
     * offsetof宏来根据该timer字段方便找到其所在的对应事件对象结构体. */
    ngx_rbtree_insert(&ngx_event_timer_rbtree, &ev->timer);

    /* 置位该变量,表示添加到红黑树中 */
    ev->timer_set = 1;
}

2.2.3 ngx_event_find_timer

/*
 * 执行意义:
 * 找出红黑树中最左边的节点,如果它的超时时间大于当前时间,也就表明目前的定时器中没有一个事件
 * 满足触发条件,这时返回这个超时与当前时间的差值,也就是需要经过多少毫秒会有事件超时触发;如果
 * 它的超时时间小于或等于当前时间,则返回0,表示定时器中已经存在超时需要触发的事件.
 */
ngx_msec_t ngx_event_find_timer(void)
{
    ngx_msec_int_t      timer;
    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;

    /* 找出该树 key 最小的那个节点,即超时时间最小的 */
    node = ngx_rbtree_min(root, sentinel);

    /* 该节点的超时时间与当前时间的毫秒比较,若大于,则表明还没有触发超时,返回它们的差值;
     * 若小于或等于,则表示已经满足超时条件,返回0 */
    timer = (ngx_msec_int_t)(node->key - ngx_current_msec);

    return (ngx_msec_t)(timer > 0 ? timer : 0);
}

2.2.4 ngx_event_expire_timers

/*
 * 执行意义:
 * 检查定时器中的所有事件,按照红黑树关键字由小到大的顺序依次调用已经满足
 * 超时条件的需要被触发事件的 handler 回调方法.
 */
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;
        }

        /* 计算 ev 的首地址位置 */
        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

        /* 置为 0,表示已经不在定时器中了 */
        ev->timer_set = 0;

        /* 置为 1,表示已经超时了 */
        ev->timedout = 1;

        /* 调用该超时事件的方法 */
        ev->handler(ev);
    }
}
ngx_event_expire_timers 流程图

Nginx事件管理之定时器事件