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

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函数里面的

1
2
3
4
5
6
7
8
9
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时进行时间更新。

这分别代表两个策略。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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后会如何呢?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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的实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
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内存屏障,是个宏

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

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

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

1
2
3
4
5
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日午夜)到当前时间的秒数。否则则是当天的。