正如咱們所知,NGINX採用了異步、事件驅動的方法來處理鏈接。這種處理方式無需(像使用傳統架構的服務器同樣)爲每一個請求建立額外的專用進程或者線程,而是在一個工做進程中處理多個鏈接和請求。爲此,NGINX工做在非阻塞的socket模式下,並使用了epoll 和 kqueue這樣有效的方法。html
由於滿負載進程的數量不多(一般每核CPU只有一個)並且恆定,因此任務切換隻消耗不多的內存,並且不會浪費CPU週期。經過NGINX自己的實例,這種方法的優勢已經爲衆人所知。NGINX能夠很是好地處理百萬級規模的併發請求。nginx
每一個進程都消耗額外的內存,並且每次進程間的切換都會消耗CPU週期並丟棄CPU高速緩存中的數據。數據庫
可是,異步、事件驅動方法仍然存在問題。或者,我喜歡將這一問題稱爲「敵兵」,這個敵兵的名字叫阻塞(blocking)。不幸的是,不少第三方模塊使用了阻塞調用,然而用戶(有時甚至是模塊的開發者)並不知道阻塞的缺點。阻塞操做能夠毀掉NGINX的性能,咱們必須不惜一切代價避免使用阻塞。後端
即便在當前官方的NGINX代碼中,依然沒法在所有場景中避免使用阻塞,NGINX1.7.11中實現的線程池機制解決了這個問題。咱們將在後面講述這個線程池是什麼以及該如何使用。如今,讓咱們先和咱們的「敵兵」進行一次面對面的碰撞。緩存
首先,爲了更好地理解這一問題,咱們用幾句話說明下NGINX是如何工做的。服務器
一般狀況下,NGINX是一個事件處理器,即一個接收來自內核的全部鏈接事件的信息,而後向操做系統發出作什麼指令的控制器。實際上,NGINX幹了編排操做系統的所有髒活累活,而操做系統作的是讀取和發送字節這樣的平常工做。因此,對於NGINX來講,快速和及時的響應是很是重要的。網絡
工做進程監聽並處理來自內核的事件架構
事件能夠是超時、socket讀寫就緒的通知,或者發生錯誤的通知。NGINX接收大量的事件,而後一個接一個地處理它們,並執行必要的操做。所以,全部的處理過程是經過一個線程中的隊列,在一個簡單循環中完成的。NGINX從隊列中取出一個事件並對其作出響應,好比讀寫socket。在多數狀況下,這種方式是很是快的(也許只須要幾個CPU週期,將一些數據複製到內存中),NGINX能夠在一瞬間處理掉隊列中的全部事件。併發
全部處理過程是在一個簡單的循環中,由一個線程完成app
可是,若是NGINX要處理的操做是一些又長又重的操做,又會發生什麼呢?整個事件處理循環將會卡住,等待這個操做執行完畢。
所以,所謂「阻塞操做」是指任何致使事件處理循環顯著中止一段時間的操做。操做能夠因爲各類緣由成爲阻塞操做。例如,NGINX可能因長時間、CPU密集型處理,或者可能等待訪問某個資源(好比硬盤,或者一個互斥體,亦或要從處於同步方式的數據庫得到相應的庫函數調用等)而繁忙。關鍵是在處理這樣的操做期間,工做進程沒法作其餘事情或者處理其餘事件,即便有更多的可用系統資源能夠被隊列中的一些事件所利用。
咱們來打個比方,一個商店的營業員要接待他面前排起的一長隊顧客。隊伍中的第一位顧客想要的某件商品不在店裏而在倉庫中。這位營業員跑去倉庫把東西拿來。如今整個隊伍必須爲這樣的配貨方式等待數個小時,隊伍中的每一個人都很不爽。你能夠想見人們的反應吧?隊伍中每一個人的等待時間都要增長這些時間,除非他們要買的東西就在店裏。
隊伍中的每一個人不得不等待第一我的的購買
在NGINX中會發生幾乎一樣的狀況,好比當讀取一個文件的時候,若是該文件沒有緩存在內存中,就要從磁盤上讀取。從磁盤(特別是旋轉式的磁盤)讀取是很慢的,而當隊列中等待的其餘請求可能不須要訪問磁盤時,它們也得被迫等待。致使的結果是,延遲增長而且系統資源沒有獲得充分利用。
一個阻塞操做足以顯著地延緩全部接下來的操做
一些操做系統爲讀寫文件提供了異步接口,NGINX可使用這樣的接口(見AIO指令)。FreeBSD就是個很好的例子。不幸的是,咱們不能在Linux上獲得相同的福利。雖然Linux爲讀取文件提供了一種異步接口,可是存在明顯的缺點。其中之一是要求文件訪問和緩衝要對齊,但NGINX很好地處理了這個問題。可是,另外一個缺點更糟糕。異步接口要求文件描述符中要設置O_DIRECT標記,就是說任何對文件的訪問都將繞過內存中的緩存,這增長了磁盤的負載。在不少場景中,這都絕對不是最佳選擇。
爲了有針對性地解決這一問題,在NGINX 1.7.11中引入了線程池。默認狀況下,NGINX+尚未包含線程池,可是若是你想試試的話,能夠聯繫銷售人員,NGINX+ R6是一個已經啓用了線程池的構建版本。
如今,讓咱們走進線程池,看看它是什麼以及如何工做的。
讓咱們回到那個可憐的,要從大老遠的倉庫去配貨的售貨員那兒。這回,他已經變聰明瞭(或者也許是在一羣憤怒的顧客教訓了一番以後,他才變得聰明的?),僱用了一個配貨服務團隊。如今,當任何人要買的東西在大老遠的倉庫時,他再也不親自去倉庫了,只須要將訂單丟給配貨服務,他們將處理訂單,同時,咱們的售貨員依然能夠繼續爲其餘顧客服務。所以,只有那些要買倉庫裏東西的顧客須要等待配貨,其餘顧客能夠獲得即時服務。
傳遞訂單給配貨服務不會阻塞隊伍
對NGINX而言,線程池執行的就是配貨服務的功能。它由一個任務隊列和一組處理這個隊列的線程組成。
當工做進程須要執行一個潛在的長操做時,工做進程再也不本身執行這個操做,而是將任務放到線程池隊列中,任何空閒的線程均可以從隊列中獲取並執行這個任務。
工做進程將阻塞操做卸給線程池
那麼,這就像咱們有了另一個隊列。是這樣的,可是在這個場景中,隊列受限於特殊的資源。磁盤的讀取速度不能比磁盤產生數據的速度快。無論怎麼說,至少如今磁盤再也不延誤其餘事件,只有訪問文件的請求須要等待。
「從磁盤讀取」這個操做一般是阻塞操做最多見的示例,可是實際上,NGINX中實現的線程池可用於處理任何不適合在主循環中執行的任務。
目前,卸載到線程池中執行的兩個基本操做是大多數操做系統中的read()系統調用和Linux中的sendfile()。接下來,咱們將對線程池進行測試(test)和基準測試(benchmark),在將來的版本中,若是有明顯的優點,咱們可能會卸載其餘操做到線程池中。
如今讓咱們從理論過分到實踐。咱們將進行一次模擬基準測試(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供電。
客戶端有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 Mem PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND 4639 vbart 20 0 47180 28152 496 D 0.7 0.1 0:00.17 nginx 4632 vbart 20 0 47180 28196 536 D 0.3 0.1 0:00.11 nginx 4633 vbart 20 0 47180 28324 540 D 0.3 0.1 0:00.11 nginx 4635 vbart 20 0 47180 28136 480 D 0.3 0.1 0:00.12 nginx 4636 vbart 20 0 47180 28208 536 D 0.3 0.1 0:00.14 nginx 4637 vbart 20 0 47180 28208 536 D 0.3 0.1 0:00.10 nginx 4638 vbart 20 0 47180 28204 536 D 0.3 0.1 0:00.12 nginx 4640 vbart 20 0 47180 28324 540 D 0.3 0.1 0:00.13 nginx 4641 vbart 20 0 47180 28324 540 D 0.3 0.1 0:00.13 nginx 4642 vbart 20 0 47180 28208 536 D 0.3 0.1 0:00.11 nginx 4643 vbart 20 0 47180 28276 536 D 0.3 0.1 0:00.29 nginx 4644 vbart 20 0 47180 28204 536 D 0.3 0.1 0:00.11 nginx 4645 vbart 20 0 47180 28204 536 D 0.3 0.1 0:00.17 nginx 4646 vbart 20 0 47180 28204 536 D 0.3 0.1 0:00.12 nginx 4647 vbart 20 0 47180 28208 532 D 0.3 0.1 0:00.17 nginx 4631 vbart 20 0 47180 756 252 S 0.0 0.1 0:00.00 nginx 4634 vbart 20 0 47180 28208 536 D 0.0 0.1 0:00.11 nginx 4648 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/1 12 threads and 50 connections Thread Stats Avg Stdev Max +/- Stdev Latency 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 Mem PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND 4654 vbart 20 0 309708 28844 596 S 9.0 0.1 0:08.65 nginx 4660 vbart 20 0 309748 28920 596 S 6.6 0.1 0:14.82 nginx 4658 vbart 20 0 309452 28424 520 S 4.3 0.1 0:01.40 nginx 4663 vbart 20 0 309452 28476 572 S 4.3 0.1 0:01.32 nginx 4667 vbart 20 0 309584 28712 588 S 3.7 0.1 0:05.19 nginx 4656 vbart 20 0 309452 28476 572 S 3.3 0.1 0:01.84 nginx 4664 vbart 20 0 309452 28428 524 S 3.3 0.1 0:01.29 nginx 4652 vbart 20 0 309452 28476 572 S 3.0 0.1 0:01.46 nginx 4662 vbart 20 0 309552 28700 596 S 2.7 0.1 0:05.92 nginx 4661 vbart 20 0 309464 28636 596 S 2.3 0.1 0:01.59 nginx 4653 vbart 20 0 309452 28476 572 S 1.7 0.1 0:01.70 nginx 4666 vbart 20 0 309452 28428 524 S 1.3 0.1 0:01.63 nginx 4657 vbart 20 0 309584 28696 592 S 1.0 0.1 0:00.64 nginx 4655 vbart 20 0 30958 28476 572 S 0.7 0.1 0:02.81 nginx 4659 vbart 20 0 309452 28468 564 S 0.3 0.1 0:01.20 nginx 4665 vbart 20 0 309452 28476 572 S 0.3 0.1 0:00.71 nginx 5180 vbart 20 0 25232 1952 1156 R 0.0 0.0 0:00.45 top 4651 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/1 12 threads and 50 connections Thread Stats Avg Stdev Max +/- Stdev Latency 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資源和網絡容量,從內存中讀取,以服務於上述的第二個客戶端的請求。
在拋出咱們對阻塞操做的擔心並給出一些使人振奮的結果後,可能大部分人已經打算在你的服務器上配置線程池了。先彆着急。
實際上,最幸運的狀況是,讀取和發送文件操做不去處理緩慢的硬盤驅動器。若是咱們有足夠多的內存來存儲數據集,那麼操做系統將會足夠聰明地在被稱做「頁面緩存」的地方,緩存頻繁使用的文件。
「頁面緩存」的效果很好,可讓NGINX在幾乎全部常見的用例中展現優異的性能。從頁面緩存中讀取比較快,沒有人會說這種操做是「阻塞」。而另外一方面,卸載任務到一個線程池是有必定開銷的。
所以,若是內存有合理的大小而且待處理的數據集不是很大的話,那麼無需使用線程池,NGINX已經工做在最優化的方式下。
卸載讀操做到線程池是一種適用於很是特殊任務的技術。只有當常常請求的內容的大小,不適合操做系統的虛擬機緩存時,這種技術纔是最有用的。至於可能適用的場景,好比,基於NGINX的高負載流媒體服務器。這正是咱們已經模擬的基準測試的場景。
咱們若是能夠改進卸載讀操做到線程池,將會很是有意義。咱們只須要知道所需的文件數據是否在內存中,只有不在內存中時,讀操做才應該卸載到一個單獨的線程中。
再回到售貨員那個比喻的場景中,這回,售貨員不知道要買的商品是否在店裏,他必需要麼老是將全部的訂單提交給配貨服務,要麼老是親自處理它們。
人艱不拆,操做系統缺乏這樣的功能。第一次嘗試是在2010年,人們試圖將這一功能添加到Linux做爲fincore()系統調用,可是沒有成功。後來還有一些嘗試,是使用RWF_NONBLOCK標記做爲preadv2()系統調用來實現這一功能(詳情見LWN.net上的非阻塞緩衝文件讀取操做和異步緩衝讀操做)。但全部這些補丁的命運目前還不明朗。悲催的是,這些補丁尚沒有被內核接受的主要緣由,貌似是由於曠日持久的撕逼大戰(bikeshedding)。
另外一方面,FreeBSD的用戶徹底沒必要擔憂。FreeBSD已經具有足夠好的讀文件取異步接口,咱們應該用這個接口而不是線程池。
因此,若是你確信在你的場景中使用線程池能夠帶來好處,那麼如今是時候深刻了解線程池的配置了。
線程池的配置很是簡單、靈活。首先,獲取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/disk一、/mnt/disk二、/mnt/disk3 proxy_cache_path /mnt/disk1 levels=1:2 keys_zone=cache_1:256m max_size=1024G use_temp_path=off; proxy_cache_path /mnt/disk2 levels=1:2 keys_zone=cache_2:256m max_size=1024G use_temp_path=off; proxy_cache_path /mnt/disk3 levels=1:2 keys_zone=cache_3:256m max_size=1024G use_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在用戶空間中細粒度的調優,咱們能夠確保軟件、操做系統和硬件工做在最優模式下,儘量有效地利用系統資源。
綜上所述,線程池是一個偉大的功能,將NGINX推向了新的性能水平,除掉了一個衆所周知的長期危害——阻塞——尤爲是當咱們真正面對大量內容的時候。
甚至,還有更多的驚喜。正如前面提到的,這個全新的接口,有可能沒有任何性能損失地卸載任何長期阻塞操做。NGINX在擁有大量的新模塊和新功能方面,開闢了一方新天地。許多流行的庫仍然沒有提供異步非阻塞接口,此前,這使得它們沒法與NGINX兼容。咱們能夠花大量的時間和資源,去開發咱們本身的無阻塞原型庫,但這麼作始終都是值得的嗎?如今,有了線程池,咱們能夠相對容易地使用這些庫,而不會影響這些模塊的性能。