Cloudflare Nginx優化成果:每天爲網際網路節約54年

總共有1000萬個網站或者應用程序使用Cloudflare為其服務加速。 在最高峰時,我們(共151個數據中心)每秒處理超過1000萬個請求。 多年來,我們對NGINX進行了許多改進,以應對我們的增長。 這篇博文是關於我們眾多改進中的一部分。

NGINX的工作原理

NGINX使用事件循環來解決C10K問題 。 每次網絡事件發生時(新連接,連接可讀/可寫等)NGINX被喚醒,之後處理事件,然後繼續處理其他需要做的事情(可能處理其他事件)。 當事件到達時,與事件相關的數據已準備就緒,這使NGINX可以同時有效地處理許多請求而無需等待。

Cloudflare Nginx优化成果:每天为互联网节约54年

例如,以下是從文件描述符中讀取數據的一段代碼:

Cloudflare Nginx优化成果:每天为互联网节约54年

當fd是socket時,可以讀取已經到達的數據。 最後一次調用將返回EWOULDBLOCK,這意味著我們已經讀取了內核緩衝區中所有數據,所以在有更多數據可用之前我們不應該再次從socket中讀取數據。

磁盤I/O與網絡I/O不同

當fd是Linux上的文件時, EWOULDBLOCK和EAGAIN永遠不會出現,並且read函數總是等待讀取整個緩衝區。 即使用O_NONBLOCK打開文件也是如此。 引用open(2):

請注意,此標誌對常規文件和塊設備無效

換句話說,上面的代碼可以精簡為:

Cloudflare Nginx优化成果:每天为互联网节约54年

這意味著如果需要從磁盤讀取數據,那麼整個循環都會阻塞,直到完成讀取文件,後續事件處理會被delay。

這對於大多數工作負載來說都可以接受,因為從磁盤讀取數據通常足夠快,並且與等待數據包從網絡到達相比更加可預測。 現在大家都使用SSD,而我們的緩存磁盤都是SSD。 現代SSD具有非常低的延遲,通常為10 μs。 最重要的是,我們可以使用多個工作進程運行NGINX,以便慢速事件處理不會阻止其他進程中的請求。 大多數情況下,我們可以依靠NGINX的事件處理來快速有效地處理請求。

SSD性能並不總能達標

估計你已經猜到,我們的假設過於樂觀。 如果每次磁盤讀取需要50μs,那麼在讀取0.19MB(4KB塊大小)數據需要2ms(我們讀取更大的塊)。 但是測試表明,對於讀取速度的99和999百分位數來說,通常會比較糟糕。 換句話說,每100(或每1000)次磁盤數據讀取的最慢值通常並不小。

固態硬盤非常快,並且非常複雜。 從本質上看SSD是有排隊和重新排序I/O功能的計算機,還執行垃圾收集和碎片整理等各種後臺任務。 偶爾會有請求變慢到需要引起重視的程度。 我的同事Ivan Babrou運行了I/O基準測試,其中最慢的磁盤讀取已經達到1s。 此外,一些SSD比其他SSD的性能異常值更多。 展望未來,我們將考慮未來購買的SSD的性能保持一致,但與此同時我們需要為現有硬件提供解決方案。

使用SO_REUSEPORT均勻分佈負載

雖然一個單獨的慢讀取是很難避免的,但我們不希望1秒鐘的磁盤I/O阻塞同一秒內的其他請求。 從概念上講,NGINX可以並行處理多個請求,但它一次卻只能處理1個事件。 所以我添加了以下指標:

Cloudflare Nginx优化成果:每天为互联网节约54年

event_loop_blocked的時間超過了我們TTFB(首字節響應時間)的50%。 也就是說,服務請求所花費的時間的一半是由於事件循環被其他請求阻塞。由於 event_loop_blocked僅測量大約一半的阻塞(因為未測量對epoll_wait()延遲調用)因此阻塞時間的實際比率要高得多。

我們的每臺機器運有15個NGINX進程,這意味著一個慢速I/O應該只阻塞最多6%的請求。但是,IO事件並不是均勻分佈的,最嚴重的情況有11%的請求被阻塞(或者是預期的兩倍)。

SO_REUSEPORT可以解決分佈不均的問題。 Marek Majkowski之前撰寫過相關文章,但是跟我們的實際情況不符,由於我們使用長連接,因此打開連接導致的延遲可忽略不計。 僅此配置更改就使SO_REUSEPORT峰值p99提高了33%。

將read移動到線程池

解決這個問題的方法是使read不阻塞。 事實上,這是一個在NGINX中已經實現的功能! 使用以下配置時, read和write在線程池中完成,不會阻止事件循環:

Cloudflare Nginx优化成果:每天为互联网节约54年

然而,當我們對此進行測試時,實際上看到p99略有改善,而不是看到大幅度的響應時間改善。 數據差異在誤差範圍內,我們對結果感到氣餒,並暫時停止深究。

有幾個原因導致沒達到預期的優化程度。 在相關測試中,他們使用200個併發連接來請求大小為4MB的文件,這些文件位於機械硬盤上。 機械磁盤會增加I/O延遲,因此優化read延遲會產生更大的影響。

而且我們主要關注p99(和p999)的性能。 有助於平均性能的解決方案不一定有助於異常值。

最後,在我們的環境中,典型文件要小得多。 90%的緩存命中小於60KB的文件。 較小的文件意味著更少的阻塞時間(我們通常在2次I/O中讀取整個文件)。

如果我們查看緩存命中必須執行的磁盤I/O:

Cloudflare Nginx优化成果:每天为互联网节约54年

32KB不是靜態數字,如果文件頭很小,我們只需要讀取4KB(我們不使用direct IO,因此內核將最多四捨五入)。 open看起來似乎沒啥毛病,但它實際上並非沒有問題。 內核至少需要檢查文件是否存在以及調用進程是否有權打開它。 為此,它必須找到/cache/prefix/dir/EF/BE/CAFEBEEF的inode,也必須在/cache/prefix/dir/EF/BE/中查找CAFEBEEF。 長話短說,在最壞的情況下,內核必須執行以下查找:

Cloudflare Nginx优化成果:每天为互联网节约54年

這是完成open所需的6次讀取,而read只讀了1次! 幸運的是,上面描述的大多數磁盤查找由dentry緩存提供服務,並不需要訪問SSD。 顯然在線程池中完成的read只是整個工作的一部分。

線程池中的非阻塞open

所以我修改了NGINX代碼,使用線程池完成大部分open,這樣它就不會阻塞事件循環。 結果如下:

Cloudflare Nginx优化成果:每天为互联网节约54年

6月26日,我們對我們最繁忙的5個數據中心進行了升級,然後在第二天進行了全球範圍使用。 總體峰值p99 TTFB(首字節響應時間)提高了6倍。實際上,把我們一天處理的請求節約的時間加和(每秒800萬請求),我們為互聯網節省了54年。

我們的事件循環處理仍然不是完全非阻塞的。 第一次緩存文件的時候( open(O_CREAT)和rename()),或重新做驗證更新的時候,依然是會阻塞的。 但是由於我們的緩存命中率較高,上述情況較為罕見,所以暫時問題不大。 在未來,我們也考慮將這些代碼移出事件循環以進一步改善我們的p99延遲。

結論

NGINX是一個功能強大的平臺,但應對Linux上極高的I/O負載可能具有挑戰性。 上游NGINX可以在單獨的線程中處理文件讀取,但在我們的規模下,我們需要做的更好才能應對挑戰。

英文原文:

https://blog.cloudflare.com/how-we-scaled-nginx-and-saved-the-world-54-years-every-day/?ref


分享到:


相關文章: