歡迎光臨
每天分享高質量文章

Cloudflare Nginx最佳化成果:每天為網際網路節約54年

導讀:Nginx是世界範圍內使用最廣泛的負載均衡器和web伺服器之一。Cloudflare大規模使用Nginx來支援自身的邊緣節點。在其使用過程中碰見了一些問題,透過最佳化這些問題,Nginx的效能得到了極大提升。本文是Cloudflare對其所做的一些最佳化的具體分析和結論,對於工程師和架構師來說,十分值得一讀。


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

NGINX的工作原理

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

例如,以下是從檔案描述符中讀取資料的一段程式碼:

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

磁碟I/O與網路I/O不同

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

請註意,此標誌對常規檔案和塊裝置無效

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

這意味著如果需要從磁碟讀取資料,那麼整個迴圈都會阻塞,直到完成讀取檔案,後續事件處理會被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個事件。 所以我添加了以下指標:

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()在執行緒池中完成,不會阻止事件迴圈:

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

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

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

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

如果我們檢視快取命中必須執行的磁碟I/O:

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

這是完成open()所需的6次讀取,而read()只讀了1次! 幸運的是,上面描述的大多數磁碟查詢由dentry快取提供服務,並不需要訪問SSD。 顯然在執行緒池中完成的read()只是整個工作的一部分。

執行緒池中的非阻塞open()

所以我修改了NGINX程式碼,使用執行緒池完成大部分open(),這樣它就不會阻塞事件迴圈。 結果如下:

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

相關閱讀:



本文作者Ka-Hing Cheung,由方圓翻譯,轉載本文請註明出處,技術原創及架構實踐文章,歡迎透過公眾號選單「聯絡我們」進行投稿。


高可用架構

改變網際網路的構建方式

長按二維碼 關註「高可用架構」公眾號

贊(0)

分享創造快樂