Nginx 的线程池与性能剖析【转载】
正如我們所知,NGINX采用了異步、事件驅動的方法來處理連接。這種處理方式無需(像使用傳統架構的服務器一樣)為每個請求創建額外的專用進程或者線程,而是在一個工作進程中處理多個連接和請求。為此,NGINX工作在非阻塞的socket模式下,并使用了epoll?和?kqueue這樣有效的方法。
因為滿負載進程的數量很少(通常每核CPU只有一個)而且恒定,所以任務切換只消耗很少的內存,而且不會浪費CPU周期。通過NGINX本身的實例,這種方法的優點已經為眾人所知。NGINX可以非常好地處理百萬級規模的并發請求。
每個進程都消耗額外的內存,而且每次進程間的切換都會消耗CPU周期并丟棄CPU高速緩存中的數據。
但是,異步、事件驅動方法仍然存在問題。或者,我喜歡將這一問題稱為“敵兵”,這個敵兵的名字叫阻塞(blocking)。不幸的是,很多第三方模塊使用了阻塞調用,然而用戶(有時甚至是模塊的開發者)并不知道阻塞的缺點。阻塞操作可以毀掉NGINX的性能,我們必須不惜一切代價避免使用阻塞。
即使在當前官方的NGINX代碼中,依然無法在全部場景中避免使用阻塞,NGINX1.7.11中實現的線程池機制解決了這個問題。我們將在后面講述這個線程池是什么以及該如何使用。現在,讓我們先和我們的“敵兵”進行一次面對面的碰撞。
2. 問題
首先,為了更好地理解這一問題,我們用幾句話說明下NGINX是如何工作的。
通常情況下,NGINX是一個事件處理器,即一個接收來自內核的所有連接事件的信息,然后向操作系統發出做什么指令的控制器。實際上,NGINX干了編排操作系統的全部臟活累活,而操作系統做的是讀取和發送字節這樣的日常工作。所以,對于NGINX來說,快速和及時的響應是非常重要的。
工作進程監聽并處理來自內核的事件
事件可以是超時、socket讀寫就緒的通知,或者發生錯誤的通知。NGINX接收大量的事件,然后一個接一個地處理它們,并執行必要的操作。因此,所有的處理過程是通過一個線程中的隊列,在一個簡單循環中完成的。NGINX從隊列中取出一個事件并對其做出響應,比如讀寫socket。在多數情況下,這種方式是非常快的(也許只需要幾個CPU周期,將一些數據復制到內存中),NGINX可以在一瞬間處理掉隊列中的所有事件。
所有處理過程是在一個簡單的循環中,由一個線程完成
但是,如果NGINX要處理的操作是一些又長又重的操作,又會發生什么呢?整個事件處理循環將會卡住,等待這個操作執行完畢。
因此,所謂“阻塞操作”是指任何導致事件處理循環顯著停止一段時間的操作。操作可以由于各種原因成為阻塞操作。例如,NGINX可能因長時間、CPU密集型處理,或者可能等待訪問某個資源(比如硬盤,或者一個互斥體,亦或要從處于同步方式的數據庫獲得相應的庫函數調用等)而繁忙。關鍵是在處理這樣的操作期間,工作進程無法做其他事情或者處理其他事件,即使有更多的可用系統資源可以被隊列中的一些事件所利用。
我們來打個比方,一個商店的營業員要接待他面前排起的一長隊顧客。隊伍中的第一位顧客想要的某件商品不在店里而在倉庫中。這位營業員跑去倉庫把東西拿來。現在整個隊伍必須為這樣的配貨方式等待數個小時,隊伍中的每個人都很不爽。你可以想見人們的反應吧?隊伍中每個人的等待時間都要增加這些時間,除非他們要買的東西就在店里。
隊伍中的每個人不得不等待第一個人的購買
在NGINX中會發生幾乎同樣的情況,比如當讀取一個文件的時候,如果該文件沒有緩存在內存中,就要從磁盤上讀取。從磁盤(特別是旋轉式的磁盤)讀取是很慢的,而當隊列中等待的其他請求可能不需要訪問磁盤時,它們也得被迫等待。導致的結果是,延遲增加并且系統資源沒有得到充分利用。
一個阻塞操作足以顯著地延緩所有接下來的操作
一些操作系統為讀寫文件提供了異步接口,NGINX可以使用這樣的接口(見AIO指令)。FreeBSD就是個很好的例子。不幸的是,我們不能在Linux上得到相同的福利。雖然Linux為讀取文件提供了一種異步接口,但是存在明顯的缺點。其中之一是要求文件訪問和緩沖要對齊,但NGINX很好地處理了這個問題。但是,另一個缺點更糟糕。異步接口要求文件描述符中要設置O_DIRECT標記,就是說任何對文件的訪問都將繞過內存中的緩存,這增加了磁盤的負載。在很多場景中,這都絕對不是最佳選擇。
為了有針對性地解決這一問題,在NGINX 1.7.11中引入了線程池。默認情況下,NGINX+還沒有包含線程池,但是如果你想試試的話,可以聯系銷售人員,NGINX+ R6是一個已經啟用了線程池的構建版本。
現在,讓我們走進線程池,看看它是什么以及如何工作的。
3. 線程池
讓我們回到那個可憐的,要從大老遠的倉庫去配貨的售貨員那兒。這回,他已經變聰明了(或者也許是在一群憤怒的顧客教訓了一番之后,他才變得聰明的?),雇用了一個配貨服務團隊。現在,當任何人要買的東西在大老遠的倉庫時,他不再親自去倉庫了,只需要將訂單丟給配貨服務,他們將處理訂單,同時,我們的售貨員依然可以繼續為其他顧客服務。因此,只有那些要買倉庫里東西的顧客需要等待配貨,其他顧客可以得到即時服務。
傳遞訂單給配貨服務不會阻塞隊伍
對NGINX而言,線程池執行的就是配貨服務的功能。它由一個任務隊列和一組處理這個隊列的線程組成。
當工作進程需要執行一個潛在的長操作時,工作進程不再自己執行這個操作,而是將任務放到線程池隊列中,任何空閑的線程都可以從隊列中獲取并執行這個任務。
工作進程將阻塞操作卸給線程池
那么,這就像我們有了另外一個隊列。是這樣的,但是在這個場景中,隊列受限于特殊的資源。磁盤的讀取速度不能比磁盤產生數據的速度快。不管怎么說,至少現在磁盤不再延誤其他事件,只有訪問文件的請求需要等待。
“從磁盤讀取”這個操作通常是阻塞操作最常見的示例,但是實際上,NGINX中實現的線程池可用于處理任何不適合在主循環中執行的任務。
目前,卸載到線程池中執行的兩個基本操作是大多數操作系統中的read()系統調用和Linux中的sendfile()。接下來,我們將對線程池進行測試(test)和基準測試(benchmark),在未來的版本中,如果有明顯的優勢,我們可能會卸載其他操作到線程池中。
4. 基準測試
現在讓我們從理論過度到實踐。我們將進行一次模擬基準測試(synthetic benchmark),模擬在阻塞操作和非阻塞操作的最差混合條件下,使用線程池的效果。
另外,我們需要一個內存肯定放不下的數據集。在一臺48GB內存的機器上,我們已經產生了每文件大小為4MB的隨機數據,總共256GB,然后配置NGINX,版本為1.9.0。
配置很簡單:
worker_processes?16;events?{accept_mutex?off; }http?{include?mime.types;default_type?application/octet-stream;access_log?off;sendfile?on;sendfile_max_chunk?512k;server?{listen?8000;location?/?{root?/storage;}} }如上所示,為了達到更好的性能,我們調整了幾個參數:禁用了logging和accept_mutex,同時,啟用了sendfile并設置了sendfile_max_chunk的大小。最后一個指令可以減少阻塞調用sendfile()所花費的最長時間,因為NGINX不會嘗試一次將整個文件發送出去,而是每次發送大小為512KB的塊數據。
這臺測試服務器有2個Intel Xeon E5645處理器(共計:12核、24超線程)和10-Gbps的網絡接口。磁盤子系統是由4塊西部數據WD1003FBYX 磁盤組成的RAID10陣列。所有這些硬件由Ubuntu服務器14.04.1 LTS供電。
為基準測試配置負載生成器和NGINX
客戶端有2臺服務器,它們的規格相同。在其中一臺上,在wrk中使用Lua腳本創建了負載程序。腳本使用200個并行連接向服務器請求文件,每個請求都可能未命中緩存而從磁盤阻塞讀取。我們將這種負載稱作隨機負載。
在另一臺客戶端機器上,我們將運行wrk的另一個副本,使用50個并行連接多次請求同一個文件。因為這個文件將被頻繁地訪問,所以它會一直駐留在內存中。在正常情況下,NGINX能夠非常快速地服務這些請求,但是如果工作進程被其他請求阻塞的話,性能將會下降。我們將這種負載稱作恒定負載。
性能將由服務器上ifstat監測的吞吐率(throughput)和從第二臺客戶端獲取的wrk結果來度量。
現在,沒有使用線程池的第一次運行將不會帶給我們非常振奮的結果:
%?ifstat?-bi?eth2 eth2 Kbps?in??Kbps?out 5531.24??1.03e+06 4855.23??812922.7 5994.66??1.07e+06 5476.27??981529.3 6353.62??1.12e+06 5166.17??892770.3 5522.81??978540.8 6208.10??985466.7 6370.79??1.12e+06 6123.33??1.07e+06如上所示,使用這種配置,服務器產生的總流量約為1Gbps。從下面所示的top輸出,我們可以看到,工作進程的大部分時間花在阻塞I/O上(它們處于top的D狀態):
top?-?10:40:47?up?11?days,??1:32,??1?user,??load?average:?49.61,?45.77?62.89 Tasks:?375?total,??2?running,?373?sleeping,??0?stopped,??0?zombie %Cpu(s):??0.0?us,??0.3?sy,??0.0?ni,?67.7?id,?31.9?wa,??0.0?hi,??0.0?si,??0.0?st KiB?Mem:??49453440?total,?49149308?used,???304132?free,????98780?buffers KiB?Swap:?10474236?total,????20124?used,?10454112?free,?46903412?cached?MemPID?USER?????PR??NI????VIRT????RES?????SHR?S??%CPU?%MEM????TIME+?COMMAND4639?vbart????20???0???47180??28152?????496?D???0.7??0.1??0:00.17?nginx4632?vbart????20???0???47180??28196?????536?D???0.3??0.1??0:00.11?nginx4633?vbart????20???0???47180??28324?????540?D???0.3??0.1??0:00.11?nginx4635?vbart????20???0???47180??28136?????480?D???0.3??0.1??0:00.12?nginx4636?vbart????20???0???47180??28208?????536?D???0.3??0.1??0:00.14?nginx4637?vbart????20???0???47180??28208?????536?D???0.3??0.1??0:00.10?nginx4638?vbart????20???0???47180??28204?????536?D???0.3??0.1??0:00.12?nginx4640?vbart????20???0???47180??28324?????540?D???0.3??0.1??0:00.13?nginx4641?vbart????20???0???47180??28324?????540?D???0.3??0.1??0:00.13?nginx4642?vbart????20???0???47180??28208?????536?D???0.3??0.1??0:00.11?nginx4643?vbart????20???0???47180??28276?????536?D???0.3??0.1??0:00.29?nginx4644?vbart????20???0???47180??28204?????536?D???0.3??0.1??0:00.11?nginx4645?vbart????20???0???47180??28204?????536?D???0.3??0.1??0:00.17?nginx4646?vbart????20???0???47180??28204?????536?D???0.3??0.1??0:00.12?nginx4647?vbart????20???0???47180??28208?????532?D???0.3??0.1??0:00.17?nginx4631?vbart????20???0???47180????756?????252?S???0.0??0.1??0:00.00?nginx4634?vbart????20???0???47180??28208?????536?D???0.0??0.1??0:00.11?nginx4648?vbart????20???0???25232???1956????1160?R???0.0??0.0??0:00.08?top 25921?vbart????20???0??121956???2232????1056?S???0.0??0.0??0:01.97?sshd 25923?vbart????20???0???40304???4160????2208?S???0.0??0.0??0:00.53?zsh在這種情況下,吞吐率受限于磁盤子系統,而CPU在大部分時間里是空閑的。從wrk獲得的結果也非常低:
Running?1m?test?@?http://192.0.2.1:8000/1/1/112?threads?and?50?connectionsThread?Stats???Avg????Stdev?????Max??+/-?StdevLatency?????7.42s??5.31s???24.41s???74.73%Req/Sec?????0.15????0.36?????1.00????84.62%488?requests?in?1.01m,?2.01GB?read Requests/sec:??????8.08 Transfer/sec:?????34.07MB請記住,文件是從內存送達的!第一個客戶端的200個連接創建的隨機負載,使服務器端的全部的工作進程忙于從磁盤讀取文件,因此產生了過大的延遲,并且無法在合理的時間內處理我們的請求。
現在,我們的線程池要登場了。為此,我們只需在location塊中添加aio?threads指令:
location?/?{root?/storage;aio?threads; }接著,執行NGINX reload重新加載配置。
然后,我們重復上述的測試:
%?ifstat?-bi?eth2 eth2 Kbps?in??Kbps?out 60915.19??9.51e+06 59978.89??9.51e+06 60122.38??9.51e+06 61179.06??9.51e+06 61798.40??9.51e+06 57072.97??9.50e+06 56072.61??9.51e+06 61279.63??9.51e+06 61243.54??9.51e+06 59632.50??9.50e+06現在,我們的服務器產生的流量是9.5Gbps,相比之下,沒有使用線程池時只有約1Gbps!
理論上還可以產生更多的流量,但是這已經達到了機器的最大網絡吞吐能力,所以在這次NGINX的測試中,NGINX受限于網絡接口。工作進程的大部分時間只是休眠和等待新的時間(它們處于top的S狀態):
top?-?10:43:17?up?11?days,??1:35,??1?user,??load?average:?172.71,?93.84,?77.90 Tasks:?376?total,??1?running,?375?sleeping,??0?stopped,??0?zombie %Cpu(s):??0.2?us,??1.2?sy,??0.0?ni,?34.8?id,?61.5?wa,??0.0?hi,??2.3?si,??0.0?st KiB?Mem:??49453440?total,?49096836?used,???356604?free,????97236?buffers KiB?Swap:?10474236?total,????22860?used,?10451376?free,?46836580?cached?MemPID?USER?????PR??NI????VIRT????RES?????SHR?S??%CPU?%MEM????TIME+?COMMAND4654?vbart????20???0??309708??28844?????596?S???9.0??0.1??0:08.65?nginx4660?vbart????20???0??309748??28920?????596?S???6.6??0.1??0:14.82?nginx4658?vbart????20???0??309452??28424?????520?S???4.3??0.1??0:01.40?nginx4663?vbart????20???0??309452??28476?????572?S???4.3??0.1??0:01.32?nginx4667?vbart????20???0??309584??28712?????588?S???3.7??0.1??0:05.19?nginx4656?vbart????20???0??309452??28476?????572?S???3.3??0.1??0:01.84?nginx4664?vbart????20???0??309452??28428?????524?S???3.3??0.1??0:01.29?nginx4652?vbart????20???0??309452??28476?????572?S???3.0??0.1??0:01.46?nginx4662?vbart????20???0??309552??28700?????596?S???2.7??0.1??0:05.92?nginx4661?vbart????20???0??309464??28636?????596?S???2.3??0.1??0:01.59?nginx4653?vbart????20???0??309452??28476?????572?S???1.7??0.1??0:01.70?nginx4666?vbart????20???0??309452??28428?????524?S???1.3??0.1??0:01.63?nginx4657?vbart????20???0??309584??28696?????592?S???1.0??0.1??0:00.64?nginx4655?vbart????20???0??30958???28476?????572?S???0.7??0.1??0:02.81?nginx4659?vbart????20???0??309452??28468?????564?S???0.3??0.1??0:01.20?nginx4665?vbart????20???0??309452??28476?????572?S???0.3??0.1??0:00.71?nginx5180?vbart????20???0???25232???1952????1156?R???0.0??0.0??0:00.45?top4651?vbart????20???0???20032????752?????252?S???0.0??0.0??0:00.00?nginx 25921?vbart????20???0??121956???2176????1000?S???0.0??0.0??0:01.98?sshd 25923?vbart????20???0???40304???3840????2208?S???0.0??0.0??0:00.54?zsh如上所示,基準測試中還有大量的CPU資源剩余。
wrk的結果如下:
Running?1m?test?@?http://192.0.2.1:8000/1/1/112?threads?and?50?connectionsThread?Stats???Avg??????Stdev?????Max??+/-?StdevLatency???226.32ms??392.76ms???1.72s???93.48%Req/Sec????20.02?????10.84????59.00????65.91%15045?requests?in?1.00m,?58.86GB?read Requests/sec:????250.57 Transfer/sec:??????0.98GB服務器處理4MB文件的平均時間從7.42秒降到226.32毫秒(減少了33倍),每秒請求處理數提升了31倍(250 vs 8)!
對此,我們的解釋是請求不再因為工作進程被阻塞在讀文件,而滯留在事件隊列中,等待處理,它們可以被空閑的進程處理掉。只要磁盤子系統能做到最好,就能服務好第一個客戶端上的隨機負載,NGINX可以使用剩余的CPU資源和網絡容量,從內存中讀取,以服務于上述的第二個客戶端的請求。
5. 依然沒有銀彈
在拋出我們對阻塞操作的擔憂并給出一些令人振奮的結果后,可能大部分人已經打算在你的服務器上配置線程池了。先別著急。
實際上,最幸運的情況是,讀取和發送文件操作不去處理緩慢的硬盤驅動器。如果我們有足夠多的內存來存儲數據集,那么操作系統將會足夠聰明地在被稱作“頁面緩存”的地方,緩存頻繁使用的文件。
“頁面緩存”的效果很好,可以讓NGINX在幾乎所有常見的用例中展示優異的性能。從頁面緩存中讀取比較快,沒有人會說這種操作是“阻塞”。而另一方面,卸載任務到一個線程池是有一定開銷的。
因此,如果內存有合理的大小并且待處理的數據集不是很大的話,那么無需使用線程池,NGINX已經工作在最優化的方式下。
卸載讀操作到線程池是一種適用于非常特殊任務的技術。只有當經常請求的內容的大小,不適合操作系統的虛擬機緩存時,這種技術才是最有用的。至于可能適用的場景,比如,基于NGINX的高負載流媒體服務器。這正是我們已經模擬的基準測試的場景。
我們如果可以改進卸載讀操作到線程池,將會非常有意義。我們只需要知道所需的文件數據是否在內存中,只有不在內存中時,讀操作才應該卸載到一個單獨的線程中。
再回到售貨員那個比喻的場景中,這回,售貨員不知道要買的商品是否在店里,他必須要么總是將所有的訂單提交給配貨服務,要么總是親自處理它們。
人艱不拆,操作系統缺少這樣的功能。第一次嘗試是在2010年,人們試圖將這一功能添加到Linux作為fincore()系統調用,但是沒有成功。后來還有一些嘗試,是使用RWF_NONBLOCK標記作為preadv2()系統調用來實現這一功能(詳情見LWN.net上的非阻塞緩沖文件讀取操作和異步緩沖讀操作)。但所有這些補丁的命運目前還不明朗。悲催的是,這些補丁尚沒有被內核接受的主要原因,貌似是因為曠日持久的撕逼大戰(bikeshedding)。
另一方面,FreeBSD的用戶完全不必擔心。FreeBSD已經具備足夠好的讀文件取異步接口,我們應該用這個接口而不是線程池。
6. 配置線程池
所以,如果你確信在你的場景中使用線程池可以帶來好處,那么現在是時候深入了解線程池的配置了。
線程池的配置非常簡單、靈活。首先,獲取NGINX 1.7.11或更高版本的源代碼,使用–with-threads配置參數編譯。在最簡單的場景中,配置看起來很樸實。我們只需要在http、?server,或者location上下文中包含aio?threads指令即可:
aio?threads;這是線程池的最簡配置。實際上的精簡版本示例如下:
thread_pool?default?threads=32?max_queue=65536; aio?threads=default;這里定義了一個名為“default”,包含32個線程,任務隊列最多支持65536個請求的線程池。如果任務隊列過載,NGINX將輸出如下錯誤日志并拒絕請求:
thread?pool?"NAME"?queue?overflow:?N?tasks?waiting錯誤輸出意味著線程處理作業的速度有可能低于任務入隊的速度了。你可以嘗試增加隊列的最大值,但是如果這無濟于事,那么這說明你的系統沒有能力處理如此多的請求了。
正如你已經注意到的,你可以使用thread_pool指令,配置線程的數量、隊列的最大值,以及線程池的名稱。最后要說明的是,可以配置多個獨立的線程池,將它們置于不同的配置文件中,用做不同的目的:
http?{thread_pool?one?threads=128?max_queue=0;thread_pool?two?threads=32;server?{location?/one?{aio?threads=one;}location?/two?{aio?threads=two;}} … }如果沒有指定max_queue參數的值,默認使用的值是65536。如上所示,可以設置max_queue為0。在這種情況下,線程池將使用配置中全部數量的線程,盡可能地同時處理多個任務;隊列中不會有等待的任務。
現在,假設我們有一臺服務器,掛了3塊硬盤,我們希望把該服務器用作“緩存代理”,緩存后端服務器的全部響應信息。預期的緩存數據量遠大于可用的內存。它實際上是我們個人CDN的一個緩存節點。毫無疑問,在這種情況下,最重要的事情是發揮硬盤的最大性能。
我們的選擇之一是配置一個RAID陣列。這種方法毀譽參半,現在,有了NGINX,我們可以有其他的選擇:
#?我們假設每塊硬盤掛載在相應的目錄中:/mnt/disk1、/mnt/disk2、/mnt/disk3proxy_cache_path?/mnt/disk1?levels=1:2?keys_zone=cache_1:256m?max_size=1024Guse_temp_path=off; proxy_cache_path?/mnt/disk2?levels=1:2?keys_zone=cache_2:256m?max_size=1024Guse_temp_path=off; proxy_cache_path?/mnt/disk3?levels=1:2?keys_zone=cache_3:256m?max_size=1024Guse_temp_path=off;thread_pool?pool_1?threads=16; thread_pool?pool_2?threads=16; thread_pool?pool_3?threads=16;split_clients?$request_uri?$disk?{33.3%?????1;33.3%?????2;*?????????3; }location?/?{proxy_pass?http://backend;proxy_cache_key?$request_uri;proxy_cache?cache_$disk;aio?threads=pool_$disk;sendfile?on; }在這份配置中,使用了3個獨立的緩存,每個緩存專用一塊硬盤,另外,3個獨立的線程池也各自專用一塊硬盤。
緩存之間(其結果就是磁盤之間)的負載均衡使用split_clients模塊,split_clients非常適用于這個任務。
在?proxy_cache_path指令中設置use_temp_path=off,表示NGINX會將臨時文件保存在緩存數據的同一目錄中。這是為了避免在更新緩存時,磁盤之間互相復制響應數據。
這些調優將帶給我們磁盤子系統的最大性能,因為NGINX通過單獨的線程池并行且獨立地與每塊磁盤交互。每塊磁盤由16個獨立線程和讀取和發送文件專用任務隊列提供服務。
我敢打賭,你的客戶喜歡這種量身定制的方法。請確保你的磁盤也持有同樣的觀點。
這個示例很好地證明了NGINX可以為硬件專門調優的靈活性。這就像你給NGINX下了一道命令,讓機器和數據用最佳姿勢來搞基。而且,通過NGINX在用戶空間中細粒度的調優,我們可以確保軟件、操作系統和硬件工作在最優模式下,盡可能有效地利用系統資源。
7. 總結
綜上所述,線程池是一個偉大的功能,將NGINX推向了新的性能水平,除掉了一個眾所周知的長期危害——阻塞——尤其是當我們真正面對大量內容的時候。
甚至,還有更多的驚喜。正如前面提到的,這個全新的接口,有可能沒有任何性能損失地卸載任何長期阻塞操作。NGINX在擁有大量的新模塊和新功能方面,開辟了一方新天地。許多流行的庫仍然沒有提供異步非阻塞接口,此前,這使得它們無法與NGINX兼容。我們可以花大量的時間和資源,去開發我們自己的無阻塞原型庫,但這么做始終都是值得的嗎?現在,有了線程池,我們可以相對容易地使用這些庫,而不會影響這些模塊的性能。
?
原文地址:http://blog.yemou.net/article/query/info/tytfjhfascvhzxcyt158
轉載于:https://www.cnblogs.com/andrew-xie/p/5299410.html
總結
以上是生活随笔為你收集整理的Nginx 的线程池与性能剖析【转载】的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Openstack的镜像上传原理
- 下一篇: RAC 的文章