月考成績+NGINX的線程池提升性能9倍!
Python部落(python.freelycode.com)組織翻譯,禁止轉載,歡迎轉發。
Nginx不會為每一個請求創建一個專用的進程或線程(如使用傳統架構的伺服器那樣),它是通過非同步和事件驅動來進行連接處理的,並且是在一個工作進程中處理多個請求和連接。為了實現這一點,Nginx在非阻塞模式下使用socket,而且配合其他的高效方法,如 epoll 和 kqueue 。
因為全權重(full-weight)的進程數量很少(一般一個CPU內核只有一個)而且一般都是固定的,所以佔用內存更少,並且CPU周期不會浪費在切換任務上面。通過Nginx本身這個例子,大部分人知道了Nginx的這個優點並且廣泛接受。它成功地處理了數百萬的並發請求並且有良好的擴展性。
每個進程消耗額外的內存,並且它們之間的每次切換都會消耗CPU並且丟棄L-cache
但是非同步和事件驅動的方法仍非完美,依舊存在問題。我喜歡把它比喻為,一個「敵人」,一個名字叫「阻塞」的敵人。不幸的是,許多第三方模塊都使用的是阻塞調用,但是用戶(有時候甚至開發人員)並不知道這些缺陷。阻塞操作可能會破壞Nginx的性能,所以我們需要不惜一切代價來避免。
即使在當前的Nginx代碼中,所有情況中都不可能避免掉組阻塞操作,為了解決這個問題,在Nginx 1.7.11 和 Nginx Plus Release 7 中實現了新的「線程池」機制。後面我們會講到這到底是個什麼並且教大家如何使用。現在讓我們來正面看看「阻塞」這個敵人。
編輯 - 有關NGINX Plus R7的概述,請參閱我們的博客上的「 Announcing NGINX Plus R7」
有關NGINX Plus R7中其他新功能的詳細討論,請參閱這些相關博文:
--HTTP/2 Now Fully Supported in NGINX Plus
--Socket Sharding in NGINX
--The New NGINX Plus Dashboard in Release 7
--TCP Load Balancing in NGINX Plus R7
問題
首先,為了更好的理解這個問題,我們先來了解Nginx的工作原理。
一般來說,Nginx是一個事件處理程序,它是一個從內核接收信息的控制器,處理髮生在連接上面的事,然後再向操作系統發出相應操作的命令。而事實上,Nginx通過編排操作系統完成了所有的繁重工作,而操作系統則完成了讀取和發送位元組的日常工作。因此Nginx能快速及時的做出響應非常重要。
工作進程監聽並處理來自內核的事件
事件可以是超時、關於準備讀取或寫入的socket的通知,或出現錯誤的通知。Nginx接收一組事件,然後逐個進行處理,執行必要的操作。因此,所有的處理都是在一個線程中的一個簡單的循環中完成的。Nginx從隊列中去除某個事件,然後通過如寫入或讀取一個socket來進行反應。大多數情況下,這非常快(可能只需要幾個CPU周期來複制內存中的一些數據),Nginx在一瞬間完成了隊列中的所有事件。
所有的處理都是在一個線程中的一個簡單的循環中完成
但是,如果發生漫長而繁重的操作,會發生什麼?事件處理的整個周期會被卡住等待該操作完成。
因此,通常我們說的「阻塞操作」,是指可以在相當長時間內停止處理事件操作的操作(讀起來比較繞口)。各種原因都可能導致操作阻塞。例如,Nginx可能會忙於冗長的CPU密集型處理,或者它不得不等待訪問資源(例如硬碟驅動,或互斥鎖或庫函數調用,以同步方式從資料庫獲取響應,等等)。關鍵是在處理這些操作時,即使有很多其他資源可用,甚至隊列中的很多事件可以使用其他那些資源來完成工作,工作進程也還是不能做其他任何事情,也不能處理額外的事件。
想想一下一個商店的售貨員面前排著長隊。隊列的第一個人需要的東西不在商店裡,而是在倉庫里。售貨員到倉庫去拿貨送貨,整個隊列必須等幾個小時來完成交付,隊列中的每個人都不滿意。你能想像到人們的反應嗎?隊列中每個人的等待時間都在增加,但是他們想買的商品可能就在商店裡。
隊列中的每個人都不得不等待第一個人的命令完成
Nginx有類似的情況,當Nginx讀取一個沒有在內存中緩存的文件時,但是需要從磁碟讀取。硬碟驅動器很慢(特別是機械硬碟),而等待隊列的其他請求可能不需要訪問驅動器,其他請求就被迫等待,因此,延遲增加變大,系統資源未得到充分利用。
僅僅一個操作就可以延遲其他操作很長時間
一些操作系統提供了讀取和發送文件的非同步介面,Nginx可以使用此介面(請參閱aio)。一個很的例子是FreeBSD。不幸的是,linux卻和它並非相同。雖然Linux 提供了文件讀取的非同步介面,但是有一些顯著的缺點。其中一個是對文件訪問和緩衝區的對齊要求,但Nginx處理的很好。第二個問題就更蛋疼了,非同步介面要求在文件描述符上設置O_DIRECT標誌,這意味著對文件的訪問繞過內存中的緩存,並增加硬碟上的負載。這樣它在絕大多數情況下都不是最優的。
為了專門解決這個問題,在Nginx 1.7.11 和 Nginx Plus Release 7中引入了線程池。
現在讓我們來看看線程池是什麼以及工作原理。
線程池(Thread Pools)
我們再回到剛剛那個售貨員的例子,從倉庫提取貨物運送交付。但他變聰明了(或許是被客戶暴揍了一頓後變聰明的),他僱傭了一個送貨服務。現在,如果有人要倉庫內的商品,售貨員自己不去倉庫,而是將訂單送到送貨服務,送貨服務來進行處理訂單,而售貨員將繼續為其他顧客服務,因此,只有那些需要倉庫商品的客戶在等待交付,而另外一些客戶則可以立即服務。
將訂單傳遞給送貨服務可以解除阻塞隊列
在Nginx中,線程池就是執行上面的送貨服務功能。它由一個任務隊列和多個處理隊列的線程組成。當一個worker進程需要做一個可能很長時間的操作,而不是自己處理這個操作時,它會在池的隊列中增加一個任務,在這個隊列中,它可以被任何空閑線程處理。
Worker進程將阻塞操作卸載到線程池
看起來我們還有另一個隊列,對吧。但是在這種情況下,隊列收到特定資源的限制。我們從驅動器上讀取速度不會比驅動器產生數據的速度快。現在,至少驅動器不會延遲其他任務的處理,只有需要訪問文件的請求會需要等待。
「從磁碟讀取」操作通常被用作阻塞操作的最常見示例,但實際上,Nginx中的線程池實現可以用於任何不適合在主工作周期中處理的任務。
目前,卸載到線程池僅僅用到三個操作:
read()
大多數操作系統的系統調用,Linux上的sendfile()
和aio_write()
,在編寫一些臨時文件(例如緩存文件)時使用的。我們將繼續測試並進行評估,如果有明顯的好處,我們可能會將其他操作卸載到線程池中。編輯 - 在NGINX 1.9.13和NGINX Plus R9中添加了對系統調用的支持。
aio_write()
性能測試(Benchmarking)
是時候從理論轉向實踐了。為了演示使用線程池的效果,我們將執行一個合成基準,模擬阻塞和非阻塞的最壞組合。
它需要保證不適合內存的數據集。在一台內存
48GB
的機器上,我們已經在4MB
文件中生成了256GB
的隨機數據,然後配置了Nginx1.9.0來服務。配置比較簡單:
正如你所看到的,為了達到更好的性能做了一些調整:
log
和accept_mutex
被禁用,sendfile
和sendfile_max_chunk
開啟設置。最後一個指令可以減少阻止調用
sendfile()
的最長時間,因為Nginx不會一次嘗試發送整個文件,而是切割為512KB
的塊進行。該機器有兩個
Intel Xeon E5645
(12 核,總共24個HT線程)處理器和 一個10Gbps
網路介面。磁碟子系統是4塊西數
WD1003FBYX硬碟組成的RAID10
結構。操作系統為Ubuntu Server 14.04.1 LTS
為基準測試的負載均衡和NGINX的配置
客戶端由兩台相同規格的機器來表示。在其中一台機器上,wrk使用Lua腳本創建負載。該腳本使用200個並行連接隨機請求伺服器文件,並且每個請求都可能導致緩存未命中和從磁碟讀取的阻塞。我們稱它為隨機負載。
在第二台客戶機上我們運行另外一個wrk副本,它將使用50個並行連接多次請求相同的文件。由於該文件被頻繁訪問,所以它將一直保存在內存中。在正常情況下,Nginx會迅速處理完成這些請求。但是如果工作進程被其他請求阻塞,性能就會下降,我們稱這個負載為恆定負載。
在第二個機器上通過
ifstat
命令查看伺服器吞吐量和wrk結果來測試機器性能。現在,在沒有線程池的第一次運行並沒有給我們滿意的結果:
正如你所見,通過這個配置,伺服器總共大約可以產生
1Gbps
的流量。使用top輸出,我們可以看到所有的工作進程大部分時間處於阻塞I/O
(都處於D
狀態):在這種情況下,吞吐量受到了磁碟的限制,而大多數時候CPU處於空閑狀態。結果wrk也很低:
記住,這是從內存中請求文件!過大的延遲是因為所有的進程都忙於從驅動器中讀取文件,以服務由第一個客戶機創建的200個連接所創建的隨機負載,且無法解釋處理我們的請求。
是時候把線程池加上了。我們只需要把
aio threads
添加到location
模塊:記得
重載
nginx讓配置生效
。重載nginx後,我們再測試:
相比於沒有線程池的
1Gbps
,現在我們的伺服器產生了9.5Gbps流量
!它可能產生更多,但已經達到了實際的網路最大容量,所以在這個測試中,Nginx受到了網路介面的限制。大部分worker進程都在休眠和等待新事件(top中顯示為
S
):仍有大量的CPU資源。
Wrk結果如下:
處理4MB文件的平均時間從
7.42秒
減少到226.32毫秒
(少了33次),並且每秒的請求數增加了31倍(250/8)!解釋是,我們的請求不再在事件隊列中等待處理,而工作進程在讀取時被阻塞,而是由空閑進程處理。只要磁碟子系統儘可能的完成任務,就可以完美解決第一台客戶機提供的隨機負載,Nginx使用剩餘的CPU資源和網路容量從內存中提取數據處理第二個客戶端的請求。
仍然不是銀彈
在我們對阻塞操作和一些令人興奮的結果考慮之後,可能大多數人已經在伺服器上配置線程池了。先別急。
事實上,幸運的是大多數讀取和發送文件操作不通過慢速硬碟處理。如果你有足夠的RAM來存儲數據集,那麼操作系統就運轉足夠快,可以在所謂的「頁面緩存」中緩存經常使用的文件。
良好的頁面緩存,允許Nginx在幾乎所有常見的用例中表現出出色的性能。從頁面緩存中讀取文檔相當快,沒有人可以模擬出這種操作的「阻塞」。另一方面,卸載到線程池也需要消耗一定的開銷。
因此,如果你有一個合理的RAM並且你的工作數據集不是很大,那麼Nginx已經以最好的方式在工作,而不需要使用線程池。
將讀取操作卸載到線程池是適用於非常特定任務的技術。在經常請求的內容的卷不適合操作系統的VM緩存情況下,它是最有用的。例如,可能是基於Nginx的大量負載的流媒體伺服器。這是我們在基準測試中模擬的情況。
如果我們能通過將讀取操作卸載到線程池中來提高性能,那就太好了。所以我們所需要的是一種有效的方法來知道所需文件數據是否在內存中,只有在後一種情況下,讀取操作才會被卸載到單獨的線程中。
回到我們的銷售類比,售貨員無法知道所請求的商品是否在商店內,並且必須始終將所有的訂單傳遞給交付服務,或者總是自己處理。
究其原因是操作系統忽略了這個特性。第一次嘗試將其作為fincore() 系統調用添加到Linux,那是2010年,但是並未實現。後來,有一些嘗試將它作為一個新的preadv2() 系統調用做具有RWF_NONBLOCK標誌的新系統調用程序實現(有關詳細信息,請參閱無阻塞緩衝文件讀取操作和LWN.net上的非同步緩衝讀取操作)。這些補丁是否能成功實現還是未知數。為什麼補丁還不能被接受到內核的最重要的原因是並不是非常被重視。
另一方面,
FreeBSD
用戶完全不需要擔心。FreeBSD已經有一個非常好的,可用於替代線程池的非同步介面來讀取文件。配置線程池
因此,如果你確定線程池對你的用例有好處的話,那麼是時候來深入研究配置了。
配置非常簡單靈活。首先你需要Nginx版本1.7.11或者更高版本,加上
- -with-threads
參數來編譯。Nginx Plus用戶需要7版本或者更高版本。在最簡單的情況下,配置看起來很簡單。你需要做的是在適當的位置包含該指令:aio threads
這是線程池最小可用配置。其實這是下面配置的簡寫版:
他定義了名為
default
的線程池,擁有32
個工作線程,並且任務隊列的最大長度為65536
個任務。如果任務隊列過載,Nginx拒絕請求並記錄此錯誤:這個錯誤意味著線程可能無法像添加到隊列中那樣快速處理工作。你可以嘗試增加最大隊列大小,如果增加了但還是和原來一樣,則說明你的系統不能提供這麼多請求。
你已經注意到,使用
thread_pool
指令可以配置線程的數量、隊列最大長度和特定的線程池名稱。最後一個意味著你可以配置多個獨立的線程池,並在配置文件的不同位置提供不同的用途:如果
max_queue
未指定參數,則默認值為65536
。如上圖所示,可以設置max_queue
為零。在這種情況下,線程池只能處理與線程池配置一樣多的任務,沒有任務會在隊列中等待。現在假設你有一個帶有三個硬碟的伺服器,並且你希望這個伺服器作為一個緩存代理來緩存你後端的所有響應。預期的緩存數據量遠遠超過可用的RAM。它實際上是個人CDN的緩存節點。當然在這種情況下,最重要的是利用磁碟驅動的最大性能。
你可以選擇配置
RAID
陣列。這種方法有利有弊。現在你可以用Nginx做另一個:在這個配置中,
thread_pool
指令為每個磁碟定義一個專用的獨立線程池,並且proxy_cache_path
指令在每個磁碟上定義一個專用的獨立緩存。Split_clients
模塊用於緩存之間的負載均衡(以及磁碟之間的結果),完全符合這個任務。use_temp_path
=
off
參數給proxy_cache_path
提供指令,只是Nginx將臨時文件保存到響應的緩存數據所在的相同目錄中。在更新緩存時,需要避免在硬碟驅動器之間複製響應數據。所有的這些配置讓我們可以利用當前磁碟子系統的最大性能。因為Nginx通過單獨的線程池並行和獨立地與驅動器進行交互。每個驅動器由16個獨立線程提供,擁有用於讀取和發送文件的專用任務隊列
我打賭你的客戶也喜歡你的這種定製方法。確保你的硬碟也像這樣。
這個例子很好的展示了如何靈活的為硬體調整Nginx。就像你正在給Nginx說明關於機器和數據集交互的最佳方法。通過在用戶空間微調Nginx,你可以確保你的軟體、操作系統和硬體在最佳模式下協同工作,儘可能有效的利用所有系統資源。
結論
總體來說,線程池這個功能很強大,它通過消除阻塞,來推動Nginx達到新性能水平。
還有更多將會到來,如前所述。這個全新的介面有可能允許任何長期阻塞在不損失性能的情況下運行卸載。Nginx有大量的新模塊和功能,開闢了新視野。許多流行的庫仍未提供一個非同步非阻塞介面,所以它們和Nginx不兼容。我們可能花費大量時間和資源開發一些我們自己的庫的非阻塞原型,但是這一直這樣做值得嗎?現在,我們有了線程池,相對更容易的使用這些庫,在模塊性能不被影響的情況下。
敬請關注。
英文原文:https://www.nginx.com/blog/thread-pools-boost-performance-9x/
譯者:IC


※一個純python、快速、可擴展的Markdown解釋器
※最好的情人節禮物,就是更好的自己
※小編閑談:區分「編輯器」與「編譯器」
※下篇
※網站、爬蟲培訓更新:《梯子閱讀》解讀視頻上線
TAG:Python程序員 |