Nginx事件管理之定时器事件

Stella981
• 阅读 942

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事件管理之定时器事件

点赞
收藏
评论区
推荐文章
blmius blmius
3年前
MySQL:[Err] 1292 - Incorrect datetime value: ‘0000-00-00 00:00:00‘ for column ‘CREATE_TIME‘ at row 1
文章目录问题用navicat导入数据时,报错:原因这是因为当前的MySQL不支持datetime为0的情况。解决修改sql\mode:sql\mode:SQLMode定义了MySQL应支持的SQL语法、数据校验等,这样可以更容易地在不同的环境中使用MySQL。全局s
皕杰报表之UUID
​在我们用皕杰报表工具设计填报报表时,如何在新增行里自动增加id呢?能新增整数排序id吗?目前可以在新增行里自动增加id,但只能用uuid函数增加UUID编码,不能新增整数排序id。uuid函数说明:获取一个UUID,可以在填报表中用来创建数据ID语法:uuid()或uuid(sep)参数说明:sep布尔值,生成的uuid中是否包含分隔符'',缺省为
Jacquelyn38 Jacquelyn38
3年前
2020年前端实用代码段,为你的工作保驾护航
有空的时候,自己总结了几个代码段,在开发中也经常使用,谢谢。1、使用解构获取json数据let jsonData  id: 1,status: "OK",data: 'a', 'b';let  id, status, data: number   jsonData;console.log(id, status, number )
Wesley13 Wesley13
3年前
Java日期时间API系列31
  时间戳是指格林威治时间1970年01月01日00时00分00秒起至现在的总毫秒数,是所有时间的基础,其他时间可以通过时间戳转换得到。Java中本来已经有相关获取时间戳的方法,Java8后增加新的类Instant等专用于处理时间戳问题。 1获取时间戳的方法和性能对比1.1获取时间戳方法Java8以前
Stella981 Stella981
3年前
Python之time模块的时间戳、时间字符串格式化与转换
Python处理时间和时间戳的内置模块就有time,和datetime两个,本文先说time模块。关于时间戳的几个概念时间戳,根据1970年1月1日00:00:00开始按秒计算的偏移量。时间元组(struct_time),包含9个元素。 time.struct_time(tm_y
Wesley13 Wesley13
3年前
mysql设置时区
mysql设置时区mysql\_query("SETtime\_zone'8:00'")ordie('时区设置失败,请联系管理员!');中国在东8区所以加8方法二:selectcount(user\_id)asdevice,CONVERT\_TZ(FROM\_UNIXTIME(reg\_time),'08:00','0
Stella981 Stella981
3年前
HIVE 时间操作函数
日期函数UNIX时间戳转日期函数: from\_unixtime语法:   from\_unixtime(bigint unixtime\, string format\)返回值: string说明: 转化UNIX时间戳(从19700101 00:00:00 UTC到指定时间的秒数)到当前时区的时间格式举例:hive   selec
Stella981 Stella981
3年前
Django中Admin中的一些参数配置
设置在列表中显示的字段,id为django模型默认的主键list_display('id','name','sex','profession','email','qq','phone','status','create_time')设置在列表可编辑字段list_editable
Wesley13 Wesley13
3年前
MySQL部分从库上面因为大量的临时表tmp_table造成慢查询
背景描述Time:20190124T00:08:14.70572408:00User@Host:@Id:Schema:sentrymetaLast_errno:0Killed:0Query_time:0.315758Lock_
Python进阶者 Python进阶者
11个月前
Excel中这日期老是出来00:00:00,怎么用Pandas把这个去除
大家好,我是皮皮。一、前言前几天在Python白银交流群【上海新年人】问了一个Pandas数据筛选的问题。问题如下:这日期老是出来00:00:00,怎么把这个去除。二、实现过程后来【论草莓如何成为冻干莓】给了一个思路和代码如下:pd.toexcel之前把这