nginx中時間的管理,nginx時間管理
nginx出於效能考慮採用類似lib_event的方式,自己對時間進行了cache,用來減少對gettimeofday()的調用,因為一般來說伺服器對時間的精度要求不是特別的高,不過如果需要比較精確的timer,nginx還提供了一個timer_resolution指令用來設定時間精度,具體的機制再後面會做介紹。在ngx_times.c中提供了ngx_time_update()函數來更新時間緩衝,另外還有一個在訊號處理中用來更新cached_err_log_time的ngx_time_sigsafe_update()函數,其他地方都是從時間緩衝中取得時間。
由於nginx採用的是master-workers多進程的方式,每個一進程都會自己維護一個時間緩衝。那麼在nginx中什麼時候會更新時間緩衝呢?上面說到nginx採用了2種方式來維護時間,首先來介紹沒有用timer_resolution指令設定時間精度的情況,也就是ngx_timer_resolution為0的情況,實際上只要找一下ngx_time_update()和ngx_time_sigsafe_update()這兩個函數被調用的位置就知道。首先來說一下ngx_time_sigsafe_update(),它比較簡單只是更新了ngx_cached_err_log_time,它會在每次執行訊號處理函數的時候被調用,也就是在ngx_signal_handler()函數中。ngx_time_update()函數在master進程中的ngx_master_process_cycle()主迴圈中被調用,具體位置為sigsuspend()函數之後,也就是說master進程捕捉到並處理完一個訊號返回的時候會更新時間緩衝;在worker進程中,ngx_time_update函數的調用鏈為ngx_worker_process_cycle() -> ngx_process_events_and_timers() -> ngx_process_events() -> ngx_time_update(), 其中ngx_process_events()實際上一個宏,nginx中定義如下:
#define ngx_process_events ngx_event_actions.process_events
而ngx_event_actions為nginx的I/O模型介面函數結構體,封裝如epoll, kqueue,select,poll等這些提供的介面,這裡僅對epoll進行分析,其他類似,於是ngx_event_actions.process_events 對應ngx_epoll_module.c檔案中的 ngx_epoll_process_events()函數,在這個函數中執行epoll_wait()返回後會調用ngx_time_update()更新時間緩衝,也就是當epoll通知有事件到達或者epoll逾時返回後會更新一次時間;最後在cache_manager的進程也調用ngx_time_update()維護自己的時間緩衝,這裡不做介紹。
第二種方式,ngx_timer_resolution被設定為大於0,也就是說,此時nginx的時間緩衝精確到ngx_timer_resolution毫秒,具體的實現方法是在event模組的初始化函數ngx_event_process_init()中調用了setitimer()函數,它每隔ngx_timer_resolution毫秒會產生一個SIGALRM訊號,這個訊號的處理函數為ngx_timer_signal_handler(),定義如下:
ngx_timer_signal_handler(int signo)
{
ngx_event_timer_alarm = 1;
#if 1
ngx_log_debug0(NGX_LOG_DEBUG_EVENT, ngx_cycle->log, 0, "timer signal");
#endif
}
它非常簡單,只是將ngx_event_timer_alarm設定為1,用來記錄有SIGALRM訊號發生了,這時在來看ngx_epoll_process_events()函數,epoll_wait()的timeout被設定為-1,如果epoll_wait()是被SIGALRM訊號喚醒,則調用ngx_time_update()更新時間緩衝,否則繼續使用之前的時間緩衝,因為setitimer()每隔ngx_timer_resolution毫秒總會產生一次SIGALRM訊號,這樣就保證了時間緩衝的精度為ngx_timer_resolution毫秒。這裡只介紹了worker進程的情況,其他進程類似。
ngx_time_update()和ngx_time_sigsafe_update()這兩個函數的實現比較簡單,但是還是有幾個值得注意的地方,首先由於時間可能在訊號處理中被更新,另外多線程的時候也可能同時更新時間(nginx現在雖然沒有開放多線程,但是代碼中有考慮),nginx使用了原子變數ngx_time_lock來對時間變數進行寫加鎖,而且nginx考慮到讀時間的操作比較多,出於效能的原因沒有對讀進行加鎖,而是採用維護多個時間slot的方式來盡量減少讀存取違規,基本原理就是,當讀操作和寫操作同時發生時(1,多線程時可能發生;2,當進程正在讀時間緩衝時,被一訊號中斷去執行訊號處理函數,訊號處理函數中會更新時間緩衝),也就是讀操作進行中時(比如剛拷貝完ngx_cached_time->sec,或者拷貝ngx_cached_http_time.data進行到一半時),如果寫操作改變了讀操作的時間,讀操作最終得到的時間就變混亂了。nginx這裡採用了64個slot時間,也就是每次更新時間的時候都是更新下一個slot,如果讀操作同時進行,讀到的還是之前的slot,並沒有被改變,當然這裡只能是盡量減少了時間混亂的幾率,因為slot的個數不是無限的,slot是迴圈的,寫操作總有幾率會寫到讀操作的slot上。不過nginx現在實際上並沒有採用多線程的方式,而且在訊號處理中只是更新cached_err_log_time,所以對其他時間變數的讀訪問是不會發生混亂的。 另一個地方是兩個函數中都調用了 ngx_memory_barrier() ,實際上這個也是一個宏,它的具體定義和編譯器及體繫結構有關,gcc和x86環境下,定義如下:
#define ngx_memory_barrier() __asm__ volatile ("" ::: "memory")
它的作用實際上還是和防止讀操作混亂有關,它告訴編譯器不要將其後面的語句進行最佳化,不要打亂其執行順序,具體還是來看一下 ngx_time_update函數:
ngx_time_update()
{
...
if (!ngx_trylock(&ngx_time_lock)) {
return;
}
...
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%02d%02d",
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));
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_unlock(&ngx_time_lock);
}
可以看到ngx_memory_barrier()之後是四條賦值語句,如果沒有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分別和之前的tp = &cached_time[slot], p0 = &cached_http_time[slot][0], p1 = &cached_err_log_time[slot][0], p2 = &cached_http_log_time[slot][0]合并最佳化掉,這樣的後果是ngx_cached_time,ngx_cached_http_time,ngx_cached_err_log_time, ngx_cached_http_log_time這四個時間緩衝的不一致性時間長度增大了,因為在最後一個ngx_sprintf執行完後這四個時間緩衝才一致,在這之前如果有其他地方正在讀時間緩衝就可能導致讀到的時間不正確或者不一致,而採用ngx_memory_barrier()後,時間緩衝更新到一致的狀態只需要幾個刻度,因為只有四條賦值指令,顯然在這麼短的時間內發生讀時間緩衝的機率會小的多了。從這裡可以看出Igor考慮是非常細緻的。