深入剖析Nginx時間快取
本文適合對nginx實現原理比較感興趣的同學閱讀,需要具備一定的服務端程式設計知識。
一、背景
在伺服器開發領域,時間的準確度關係到系統能否正常執行,尤其是當系統中存在超時事件需要處理時。但是系統時間的獲取需要一次昂貴的系統呼叫,作為一款成熟的伺服器軟體,Nginx是如何優化這部分的效能開銷?
二、時間快取
接觸過系統設計的同學都知道,對於頻繁的資料獲取,在資料未變化的情形下,可以通過增加快取來優化效能,因為快取的訪問速度遠高於源資料的訪問速度。這樣的例子有很多,比如CPU設計有二級快取,在傳統的database基礎上有了我們今天的redis、memcache等nosql。對於系統時間也一樣,既然獲取系統時間開銷較大,可以嘗試著將獲取到的時間快取起來,需要時直接從快取中取就可以了。但與此同時,也引入了快取時間與實際時間不一致的可能,下面看看Nginx是如何解決這一問題。
三、設計與實現
Nginx時間快取設計
如上圖所示,Nginx時間快取包括時間讀取和時間寫入者,當需要更新時間時,nginx呼叫gettimeofday系統呼叫獲取時間,然後更新快取。需要獲取時間的程式碼直接從time cache中取出即可。
這裡又產生了新的問題,具體包括:
讀寫併發,即讀和寫同時操作時間快取會造成獲取的時間混亂。
多寫併發,多個執行體同時更新時間快取,同樣造成時間混亂。
而常見的解決互斥的方案包括:
加鎖保證資料序列化
無鎖化設計
像Nginx這樣對於效能有著極致追求的server來說,自然不會使用系統自帶的鎖機制。其實現的ngx_lock和ngx_unlock的背後都是無鎖化的原子操作。
對於多寫併發,nginx在ngx_time_update函式中通過全域性的ngx_time_lock進行互斥,確保同一時刻只會存在一個執行體更新時間快取。
對於讀寫併發,nginx設計了NGX_TIME_SLOTS個slot,用於隔離讀寫操作的時間快取。同時引入時間快取指標,原子地更新當前快取的指向位置。
Nginx時間快取實現
下面看具體實現程式碼(以nginx-1.13.1為例src/core/ngx_times.c):
void ngx_time_update(void)
ngx_time_update的流程圖為:
值得一提的是,這裡採用了ngx_memory_barrier來避免指令重排,這樣可以儘可能地保證ngx_cached_time、ngx_cached_http_time.data、ngx_cached_err_log_time.data、ngx_cached_http_log_time.data、ngx_cached_http_log_iso8601.data、ngx_cached_syslog_time.data
slot設計
上面談到了nginx採用slot來從空間上避免讀寫執行體同時操作時間快取,slot的設計規則為:
獲取時間的執行體採用ngx_timeofday獲取了當前ngx_cached_time的快照,隨後讀取對應的slot中資料,包括sec和msec。
更新時間的執行體通過ngx_time_update原子更新ngx_cached_time指向,這樣更新之後的時間讀取就是新的slot中的時間資料。
這裡,nginx利用了修改指標的原子性,確保讀寫不會造成時間資料混亂。而時間資料本身包括sec和msec,無法完成修改的原子性,這種將非原子性修改操作轉換為原子性修改操作的手法,值得借鑑。