(nginx源码系列三)--nginx时间缓存分析

原创内容,转载请注明出处

Posted by Weakyon Blog on April 30, 2015

Why

nginx把整个时间的系统调用都封装了一遍。

其中有两个原因

1 不谈其他平台,至少在linux上,很多时间相关的系统调用不是async-signal-safe或thread-safe的。

例如local_time_r虽然是thread-safe的但不是async-signal-safe的,当然local_time两个都不是。

这里不是专门谈什么是async-siganl-safe的,对此概念上有疑惑或者错用后危害上有疑惑的可以点这个博文。写的相当透彻。

这篇文章主要是讲twemproxy遇到的一个死锁BUG(我在线上也遇到这样的问题了),连twitter的明星coder都会犯这个错误。可见还是需要注意这一点的。

twemproxy-deadlock-on-signal_handler

2 另外则是减少系统调用的耗时,毕竟web server不是那么要求时间的精确性,但是如果有场合需要,nginx还是提供了参数来控制时间的精确性。

How

先看大逻辑再看细节吧。

大逻辑

前文已经分析过nginx的启动流程的。

可以看到时间缓存的更新是在ngx_epoll_process_events函数里面的

static ngx_int_t
ngx_epoll_process_events(ngx_cycle_t *cycle, ngx_msec_t timer, ngx_uint_t flags)
{
    events = epoll_wait(ep, event_list, (int) nevents, timer);
    
    if (flags & NGX_UPDATE_TIME || ngx_event_timer_alarm) {
        ngx_time_update();
    }
}

当flag为NGX_UPDATE_TIME或者ngx_event_timer_alarm不为0时进行时间更新。

这分别代表两个策略。

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;   //-1
        flags = 0;

    } else {
        timer = ngx_event_find_timer();
        flags = NGX_UPDATE_TIME;
    }

    (void) ngx_process_events(cycle, timer, flags);//实际就是ngx_epoll_process_events
}

策略一:当未设置ngx_timer_resolution时直接每一次调用后都更新时间

策略二:当设置了ngx_timer_resolution后会如何呢?

static ngx_int_t
ngx_event_process_init(ngx_cycle_t *cycle)
{
    if (ngx_timer_resolution && !(ngx_event_flags & NGX_USE_TIMER_EVENT)) {
        struct sigaction  sa;
        struct itimerval  itv;

        sa.sa_handler = ngx_timer_signal_handler;
        if (sigaction(SIGALRM, &sa, NULL) == -1) {}

        if (setitimer(ITIMER_REAL, &itv, NULL) == -1) {}
    }
}
void
ngx_timer_signal_handler(int signo)
{
    ngx_event_timer_alarm = 1;
}

可以看到,在最开始事件循环初始化时设置了一个时间信号函数,每隔ngx_timer_resolution时间进行触发

触发函数把ngx_event_timer_alarm设置为1

随后当epoll被信号触发后不在阻塞立刻返回,errno设置为EINTR

随后进行时间更新

时间更新的实现

再看core/ngx_times.c里核心函数ngx_time_update的实现

void
ngx_time_update(void)
{
    u_char          *p0, *p1, *p2, *p3;
    ngx_tm_t         tm, gmt;
    time_t           sec;
    ngx_uint_t       msec;
    ngx_time_t      *tp;
    struct timeval   tv;

	//虽然nginx目前没有多线程,但是现在还是考虑了多线程情况下需要加锁,锁的实际实现是linux下的原子变量
    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 (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);

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

	//....

    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_unlock(&ngx_time_lock);
}

可以看到这里考虑到了很多问题。

比如使用了NGX_TIME_SLOTS大的缓存数组,来进行更新,这是为了防止多线程下或者信号函数读时,这里正在进行更新操作而导致不一致。

目前并未使用多线程机制,所以64这个值已经很大了,我认为使用2也可以同样满足需求。

另外ngx_memory_barrier内存屏障,是个宏

#define ngx_memory_barrier()    __asm__ volatile ("" ::: "memory")

当然在不同平台下有不同实现,总得来说都是让编译器不要优化这一段合并到上面去,让p0,p1,p2,p3的值一起更新到time_cache中去。

编译器可能会优化成这样的顺序:

p0 = &cached_http_time[slot][0];
ngx_cached_http_time.data = p0;
p1 = &cached_err_log_time[slot][0];
ngx_cached_err_log_time.data = p1;
...

加了编译器级别的内存屏障以后,在多线程情况下。

能够使得大部分情况下p0,p1,p2,p3都是代表同一时刻的值。

极小部分情况下当前p0值的代表的时刻可能比p1,p2,p3代表的新。

这里细节处理的很厉害。


顺带提一下ngx_times.c的其他函数

ngx_time_init时间初始化

ngx_time_sigsafe_update当信号切入时进行时间更新,只更新了ngx_cached_err_log_time

ngx_http_time和ngx_http_cookie_time,http和http_cookie的时间格式化,调用了ngx_gmtime

ngx_gmtime(time_t t, ngx_tm_t *tp)使用了自己的算法来进行时间转换

time_t ngx_next_time(time_t when) 这里when参数只是代表了当天的时间,只有秒+分钟+小时,顾名思义,寻找下一次这个时间点出现的时候的绝对时间time_t值

比如已经当前已经过了12点0分0秒,那么返回的就是下一天的12点0分0秒的,从标准计时点(一般是1970年1月1日午夜)到当前时间的秒数。否则则是当天的。

30 Apr 2015