眾所周知,Nginx 使用 異步, 事件驅(qū)動來接收連接 。這就意味著對于每個(gè)請求不會新建一個(gè)專用的進(jìn)程或者線程(就像傳統(tǒng)服務(wù)端架構(gòu)一樣),它是在一個(gè)工作進(jìn)程中接收多個(gè)連接和請求。為了達(dá)成這個(gè)目標(biāo),Nginx 用在一個(gè)非阻塞模式下的 sockets 來實(shí)現(xiàn),并使用例如 epoll 和 kqueue 這樣高效的方法。
因?yàn)闈M載的工作進(jìn)程數(shù)量是很少的(通常只有一個(gè) CPU 內(nèi)核)而且固定的,更少的內(nèi)存占用,CPU 輪訓(xùn)也不會浪費(fèi)在任務(wù)切換上。這種連接方式的優(yōu)秀之處已眾所周知地被 Nginx 自身所證實(shí)。它非常成功地接受了百萬量級的并發(fā)請求。
每個(gè)進(jìn)程都消耗額外的內(nèi)存,并且每個(gè)切換都消耗 CPU 切換和緩存清理
但是異步,事件驅(qū)動連接依舊有一個(gè)問題。或者,我喜歡稱它為“敵人”。這個(gè)敵人的名字叫:阻塞。不幸的是許多第三方模塊使用阻塞調(diào)用,而且用戶(有時(shí)候也有模塊的開發(fā)者自己)并沒有意識到有什么不妥之處。阻塞操作可以毀了 Nginx 的性能,必須要避免這樣的代價(jià)。
甚至在當(dāng)前的 Nginx 官方代碼中在每種情況中避免阻塞調(diào)用也是不可能的,為了解決這個(gè)問題,新的線程池裝置已經(jīng)在 Nginx 的1.7.11 和 Nginx Plus Release 7 中實(shí)現(xiàn)。它是什么,如何使用,我們一會兒再介紹,現(xiàn)在我們來直面我們的敵人。
編者:如果需要了解一下 Nginx Plus R7,請看我們博客中的 Announcing Nginx Plus R7
需要了解 Nginx Plus R7 里的新特性,請看這些相關(guān)文章
- 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
這個(gè)問題
首先,為了更好地理解問題所在,我們需要用簡單的話解釋一下 Nginx 如何工作的。
通常來說,Nginx 是一個(gè)事件處理器,一個(gè)從內(nèi)核中接受所有連接時(shí)發(fā)生的事件信息,然后給出對應(yīng)的操作到操作系統(tǒng)中,告訴它應(yīng)該做什么。事實(shí)上,Nginx 在操作系統(tǒng)進(jìn)行常規(guī)的讀寫字節(jié)的時(shí)候,通過調(diào)度操作系統(tǒng)把所有難做的工作都做了。因此,Nginx 及時(shí),快速的返回響應(yīng)是非常重要的。
工作進(jìn)程從內(nèi)核中監(jiān)聽并執(zhí)行事件
這些事件可以是超時(shí),提示說可以從 sockets 里面讀數(shù)據(jù)或?qū)憯?shù)據(jù),或者是一個(gè)錯(cuò)誤被觸發(fā)了。Nginx 接收一堆事件然后一個(gè)個(gè)執(zhí)行,做出必要的操作。這樣所有的過程都在一個(gè)線程中通過一個(gè)簡單的循環(huán)隊(duì)列完成。Nginx 從一個(gè)隊(duì)列中推出一個(gè)事件 然后響應(yīng)它,例如讀寫 socket 數(shù)據(jù)。在大多數(shù)情況下,這一過程非常的快(或許只是需要很少的 CPU 輪詢從內(nèi)存中拷貝一些數(shù)據(jù))而且 Nginx 繼續(xù)執(zhí)行隊(duì)列中的所有事件非常的快。
所有的過程都在一個(gè)線程中的一個(gè)簡單循環(huán)中完成
但是如果某個(gè)耗時(shí)而且重量級的操作被觸發(fā)了會發(fā)生什么呢?整個(gè)事件循環(huán)系統(tǒng)會有一個(gè)很扯淡的等待時(shí)間,直到這個(gè)操作完成。
所以,我們說的“一個(gè)阻塞操作”的意思是任何一個(gè)會占用大量時(shí)間,使接收事件的循環(huán)暫停的操作。操作可以因?yàn)楹芏嘣虮蛔枞@纾琋ginx 或許會因?yàn)殚L時(shí)間的 CPU 密集型操作而忙碌,或者是不得不等待一個(gè)資源訪問(如硬盤訪問,或者一個(gè)互斥的或函數(shù)庫從數(shù)據(jù)庫里用同步操作的方式獲取返回這種)。關(guān)鍵是當(dāng)做這些操作的時(shí)候,子進(jìn)程無法做其他任何事情,也不能接收其他的事件響應(yīng),即使是有很多的系統(tǒng)資源是空閑的,而且一些隊(duì)列里的事件可以利用這些空閑資源的時(shí)候。
想象一下,一個(gè)店里的銷售人員面對著一個(gè)長長的隊(duì)列,隊(duì)列里的第一個(gè)人跟你要不在店里,而是在倉庫里的東西??。這個(gè)銷售人員得跑去倉庫提貨。現(xiàn)在整個(gè)隊(duì)列一定是因?yàn)檫@次提貨等了好幾個(gè)小時(shí),而且隊(duì)列里的每個(gè)人都很不開心。你能想象一下隊(duì)列里的人會做出什么反應(yīng)么?在這等待的這幾個(gè)小時(shí)里面,隊(duì)列中等待的人在增加,每個(gè)人都等著很可能就在店里面的想要的東西(而不是倉庫里的)
隊(duì)列中的每個(gè)人都在等在第一個(gè)人的訂單完成
Nginx 里面所發(fā)生的事情跟這個(gè)情況是很相似的。當(dāng)讀取一個(gè)并不在內(nèi)存中緩存,而是在硬盤中的文件的時(shí)候。硬盤是很慢的(尤其是正在轉(zhuǎn)的那個(gè)),而其他的在隊(duì)列中的請求可能并不需要訪問硬盤,結(jié)果他們不得不等待。結(jié)果是延遲在增加,但是系統(tǒng)資源并沒有滿負(fù)荷。
Just one blocking operation can delay all following operations for a significant time
一些操作系統(tǒng)提供了一個(gè)異步接口去讀取和發(fā)送文件,Nginx 使用了這個(gè)接口(詳情查看 aio 指令。這里有個(gè)很好的例子就是 FreeBSD,不幸的是,我們不能保證所有的 Linux 都是一樣的。盡管 Linux 提供了一種讀取文件的異步接口,然而仍然有一些重要的缺點(diǎn)。其中一個(gè)就是文件和緩沖區(qū)訪問的對齊問題,而 Nginx 就能很好地處理。第二個(gè)問題就很嚴(yán)重了。異步接口需要 O_DIRECT 標(biāo)志被設(shè)置在文件描述中。這就意味著任意訪問這個(gè)文件都會通過內(nèi)存緩存,并增加硬盤的負(fù)載。在大多數(shù)情況下這并不能提升性能。
為了著重解決這些問題,在 Nginx 1.7.11 和 Nginx Plus Release 7 中加入了線程池。
現(xiàn)在讓我們深入介紹一些線程池是什么,它是如何工作的。
線程池
現(xiàn)在我們重新扮演那個(gè)可憐的要從很遠(yuǎn)的倉庫提貨的售貨員。但是現(xiàn)在他變聰明了(或者是因?yàn)楸灰蝗簯嵟目蛻糇崃艘活D之后變聰明了?)并且招聘了一個(gè)快遞服務(wù)。現(xiàn)在有人要遠(yuǎn)在倉庫的產(chǎn)品的時(shí)候,他不需要自己跑去倉庫提貨了,他只需要扔個(gè)訂單給快遞服務(wù),快遞會處理這個(gè)訂單,而售貨員則繼續(xù)服務(wù)其他客戶。只有那些需要需要遠(yuǎn)在倉庫的產(chǎn)品的客戶需要等待遞送,其他人則可以立即得到服務(wù)。
Passing an order to the delivery service unblocks the queue
在這個(gè)方面,Nginx 的線程池就是做物流服務(wù)的,它由一個(gè)任務(wù)隊(duì)列和很多處理隊(duì)列的線程組成。當(dāng)一個(gè) worker 要做一個(gè)耗時(shí)很長的操作的時(shí)候,它不需要自己去處理這個(gè)操作,而是把這個(gè)任務(wù)發(fā)到線程隊(duì)列中,當(dāng)有空閑的線程的時(shí)候它會被拿出來去處理。
The worker process offloads blocking operations to the thread pool
看起來我們有了另一條隊(duì)列。是的。但是這個(gè)隊(duì)列只被具體的資源所限制。我們讀數(shù)據(jù)不能比生產(chǎn)數(shù)據(jù)還要快。現(xiàn)在,至少是這趟車不會阻塞其他事件進(jìn)程了,而且只有需要訪問文件的請求會等待而已。
從硬盤讀取文件的操作是一個(gè)在阻塞例子中常用的栗子,然而事實(shí)上在 Nginx 中實(shí)現(xiàn)的線程池可以做任何不適合在主工作輪詢中執(zhí)行的任務(wù)。
那個(gè)時(shí)候,扔到線程池里只有三個(gè)基本操作:大多數(shù)操作系統(tǒng)的 read() 系統(tǒng)調(diào)用, Linux 的 sendfile 和 Linux 中調(diào)用 aio_write() 去寫入一些例如緩存的臨時(shí)文件。我們將繼續(xù)測試并對實(shí)現(xiàn)做性能基準(zhǔn)測試,而且我們未來將會把更多的可以獲得明顯收益的其他操作也放到線程池中。
編輯注:對 aio_write() 的支持在 Nginx 1.9.13 和 Nginx Plus R9 中添加。
基準(zhǔn)測試
是時(shí)候從理論轉(zhuǎn)為實(shí)際了。為了證明使用線程池的效果,我們做了一個(gè)模擬阻塞和非阻塞操作混合中最壞情況的合成基準(zhǔn)測試。
這需要一組不適合放在內(nèi)存中的數(shù)據(jù)集。一臺擁有 48G RAM的機(jī)器上,我們用 4MB 的文件生成了 256GB 的隨機(jī)數(shù)據(jù),隨后配置 Nginx 1.9.0 并打開服務(wù)。
配置項(xiàng)非常簡單:
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; } } }
就像你看到的那樣,為了得到更好的性能,我們對一些配置做了調(diào)整: logging 和 accept_mutex 被關(guān)掉了, sendfile 打開了, sendfile_max_chunk 也設(shè)置了。最后一個(gè)指令可以減少阻塞 sendfile 調(diào)用的最大花費(fèi)時(shí)間,因?yàn)?Nginx 不會一次性發(fā)送整個(gè)文件,而是分成 512KB 的小塊。
這個(gè)機(jī)器有兩個(gè) Intel Xeon E5645(共計(jì) 12 核,24 線程),有一個(gè) 10-Gps 網(wǎng)卡,硬盤子系統(tǒng)是由4塊西部數(shù)碼的 WD 1003FBYX 硬盤用 RAID10 陣列組成。所有這些硬件由 Ubuntu Server 14.04.1 LTS 提供支持。
客戶端由兩臺相同規(guī)格的機(jī)器組成,其中一臺機(jī)器上, wrk 創(chuàng)建Lua腳本,這個(gè)腳本從服務(wù)器上以亂序發(fā)起并發(fā)請求,獲取文件,并發(fā)量是 200 個(gè)連接。而且每個(gè)請求結(jié)果很可能并不能命中緩存,需要阻塞地從硬盤中讀取文件,我們稱這個(gè)負(fù)載為 隨機(jī)負(fù)載
我們的第二個(gè)客戶端機(jī)器會執(zhí)行另一個(gè) wrk。它會多次請求同一個(gè)文件,以 50 個(gè)并發(fā)請求。因?yàn)檫@個(gè)文件被經(jīng)常訪問,所以它會被一直放在內(nèi)存中。通常情況下,Nginx 對這些請求會響應(yīng)得非常快。所以我們叫這個(gè)負(fù)載是 固定負(fù)載
性能測評會被在服務(wù)器上的用 ifstat 監(jiān)控的吞吐量以及兩個(gè)客戶端的 wrk 結(jié)果作為標(biāo)準(zhǔn)。
現(xiàn)在,首先運(yùn)行沒有線程池的那個(gè),結(jié)果并不是很滿意。
% 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
正如你所看到的那樣,以這個(gè)配置,服務(wù)器大概總共有 1Gbps 的吞吐量。 top 的輸出表明大部分 worker 進(jìn)程都耗時(shí)在了阻塞 輸入/輸出 操作上(D 狀態(tài)下):
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
這種情況下,吞吐量受限于硬盤系統(tǒng),CPU 則沒事做,wrk 返回的結(jié)果表示非常的慢:
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
記住,這些文件應(yīng)該被放在內(nèi)存中!過大的延遲是因?yàn)樗械墓ぷ鬟M(jìn)程在從硬盤中讀文件的時(shí)候非常的繁忙,它們在應(yīng)對第一個(gè)客戶端 隨機(jī)負(fù)載 發(fā)出的200個(gè)并發(fā)請求。而不能在合適的時(shí)間內(nèi)響應(yīng)我們的請求。
是時(shí)候把線程池放進(jìn)來啦。為了加入我們只需要添加 aio thread 指令到 location 中。
location / { root /storage; aio threads; }
然后讓 Nginx 去讀取這個(gè)配置項(xiàng)。
然后我們重復(fù)測試:
% 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
現(xiàn)在我們的服務(wù)器產(chǎn)生了 9.5Gbps , 對比一下沒有線程池的 1Gbps 左右的結(jié)果。
它可能會更高,但是這個(gè)數(shù)值已經(jīng)到達(dá)了最大物理網(wǎng)卡容量了。所以在這個(gè)測試中, Nginx 受限于網(wǎng)卡接口。工作進(jìn)程大部分時(shí)候都沉睡并等待心得事件( 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的結(jié)果:
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
響應(yīng) 4MB 文件的平均時(shí)間從 7.42 秒降低到了 226.32 毫秒(少了33倍)。每秒請求量增加了31倍(250 vs 8)!
解釋就是我們的請求不再等著事件隊(duì)列去由 worker 進(jìn)程執(zhí)行讀取的阻塞操作。而是被空閑的線程處理。硬盤系統(tǒng)盡其所能去干活的時(shí)候它也能從第一臺機(jī)器上發(fā)出的隨機(jī)負(fù)載請求。 Nginx 使用剩余的 CPU 資源和網(wǎng)絡(luò)容量去響應(yīng)第二個(gè)客戶端的請求,從內(nèi)存里拿到數(shù)據(jù)。
仍舊不是銀彈
出于對阻塞操作的恐懼并取得了一些令人欣喜的結(jié)果之后,你們大多數(shù)可能已經(jīng)準(zhǔn)備去在你們的服務(wù)器上配置線程池了,別急。
真相是這樣的。大多數(shù)讀文件和發(fā)送文件都很幸運(yùn)地不會去處理慢速硬盤。如果你有足夠的RAM去存儲數(shù)據(jù)集,之后操作系統(tǒng)都會很聰明地存儲那些經(jīng)常訪問的文件,這叫做 “page cache”。
頁緩存活很棒,使得 Nginx 在大多數(shù)情況下都能表現(xiàn)出極佳的性能。從頁緩存中讀數(shù)據(jù)非常的快,以至于沒人把它當(dāng)作是”阻塞”操作。并一方面,扔到線程池會有一些開銷的。
所以如果你有足夠的 RAM,而且你的執(zhí)行數(shù)據(jù)不會非常大的時(shí)候,Nginx 在沒有線程池的情況下已經(jīng)做了最好的優(yōu)化。
把讀操作扔在線程池里是針對特定任務(wù)的技術(shù)手段,當(dāng)經(jīng)常訪問的內(nèi)容的空間和操作系統(tǒng)的 VM 緩存不匹配的時(shí)候很有用。情況可能是這樣的,例如負(fù)載很大的,以 Nginx 為基礎(chǔ)的流媒體服務(wù)器,這就是我們做基準(zhǔn)測試時(shí)候的情況。
如果我們可以提升讀操作放到線程池的性能就很好了。我們所需要的是一個(gè)有效的方式去知道需要的數(shù)據(jù)是不是在內(nèi)存中,而且只有第二種情況我們應(yīng)該把讀操作分到另一個(gè)線程中去。
繼續(xù)回到銷售員的比較上,當(dāng)前銷售員并不知道顧客要的商品是不是在店里,所以它要么把所有的訂單都給快遞服務(wù),要么自己接管所有訂單。
罪魁禍?zhǔn)拙褪遣僮飨到y(tǒng)丟掉了這些新功能。第一個(gè)把它以 fincore 系統(tǒng)調(diào)用加到 Linux 上的嘗試發(fā)生在 2010 年,但是并沒有結(jié)果。隨后有數(shù)次嘗試,作為一個(gè)新的 preadv2() 帶著 RWF_NONBLOCK 標(biāo)志的系統(tǒng)調(diào)用實(shí)現(xiàn)(詳情查看 LWN.net 上的信息 Non-blocking buffered file read operations 和 Asynchronous buffered read operations )。所有這些補(bǔ)丁的命運(yùn)仍舊是不清楚。令人傷心的是這里有一個(gè)主要原因,為什么這些補(bǔ)丁至今仍然沒有被內(nèi)核所接受, 繼續(xù)被放逐
另一方面,F(xiàn)reeBSD 的用戶一點(diǎn)兒都不用擔(dān)心,F(xiàn)reeBSD 早就有了一套足夠好的異步讀取文件的接口,你應(yīng)該用它來替換線程池。
配置線程池
那么如果你確定你的情況在配置線程池后會獲得一些收益,那么時(shí)時(shí)候去深入了解這些配置了。
它的配置非常的簡單靈活。第一件事就是你得有 Nginx 1.7.11 及其以上的版本,帶著 –with-threads 參數(shù)編譯到 configure 命令上。Nginx Plus 用戶需要版本 7 及其以上。最簡單的情況下,配置看來來很普通,你所做的就是把 aio threads 指令配置到合適的上下文中。
# in the 'http', 'server', or 'location' context aio threads;
這是關(guān)于線程池的最少配置了。實(shí)際上它是下面這個(gè)配置的縮減版。
# in the 'main' context thread_pool default threads=32 max_queue=65536; # in the 'http', 'server', or 'location' context aio threads=default;
它定義了一個(gè)叫做 default 的線程池,這個(gè)線程池有32個(gè)工作線程,并有最大的任務(wù)隊(duì)列 —— 65536 個(gè)任務(wù)。如果任務(wù)隊(duì)列超出了,Nginx 會拒絕請求,并記錄這個(gè)錯(cuò)誤:
thread pool "NAME" queue overflow: N tasks waiting
這個(gè)錯(cuò)誤意味著可能是因?yàn)榫€程并不能處理這些工作處理得足夠快,快過添加到隊(duì)列中。你可以試著增加隊(duì)列最大值,但如果這么做沒什么用的話,它就意味著你的系統(tǒng)不能提供如此之多的連接容量。
你可能早就注意到了,帶著 thread_pool 指令,你可以配置線程的數(shù)量,隊(duì)列的最大長度,還有特定的線程池的名稱。最后一個(gè)提示就是,你可以配置多個(gè)獨(dú)立的線程池,并在你配置項(xiàng)的不同地方去使用,針對不同的目的:
# in the 'main' context thread_pool one threads=128 max_queue=0; thread_pool two threads=32; http { server { location /one { aio threads=one; } location /two { aio threads=two; } } # ... }
如果 max_queue 沒有指定,默認(rèn)值是 65536。如圖所示,你也可以配置 max_queue 到 0。這種情況下線程池只能處理所配置的線程一樣多的任務(wù); 隊(duì)列中不會有等待的任務(wù)。
現(xiàn)在想象一下你有一個(gè)帶著三塊硬盤的服務(wù)器,而且你想讓這臺服務(wù)器作為一臺 緩存服務(wù)器 ,從后端拿到的所有響應(yīng)存儲起來的那種。那么緩存量是遠(yuǎn)遠(yuǎn)超過內(nèi)存容量的。它實(shí)際上就是你的一個(gè)個(gè)人 CDN 緩存節(jié)點(diǎn)。當(dāng)然,在這種情況下,從硬盤獲得巨大的性能提升就顯得尤為重要。
你的其中一個(gè)選項(xiàng)是調(diào)整 RAID 陣列,這種方式有它自己的優(yōu)缺點(diǎn)。現(xiàn)在你既然有 Nginx,那么你可以用另一種方式了:
# 我們假定每個(gè)硬盤在這些目錄中掛載 # /mnt/disk1, /mnt/disk2, or /mnt/disk3 # in the 'main' context thread_pool pool_1 threads=16; thread_pool pool_2 threads=16; thread_pool pool_3 threads=16; http { 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; split_clients $request_uri $disk { 33.3% 1; 33.3% 2; * 3; } server { # ... location / { proxy_pass http://backend; proxy_cache_key $request_uri; proxy_cache cache_$disk; aio threads=pool_$disk; sendfile on; } } }
在這份配置文件中, thread_pool 指令為每個(gè)硬盤定義了一個(gè)專有獨(dú)立的線程池。添加了 proxy_cache_path 指令為每個(gè)硬盤定義了專有,獨(dú)立的緩存。
split_clients 模塊用來平衡多個(gè)緩存的負(fù)載(同時(shí)也是硬盤的負(fù)載),它非常適合這個(gè)任務(wù)。
proxy_cache_path指令中的 use_temp_path=off 參數(shù)告訴 Nginx 把臨時(shí)文件保存到和響應(yīng)緩存相同的目錄中。要避免在在更新緩存的時(shí)候在硬盤間相互拷貝響應(yīng)數(shù)據(jù)。
所有這些配置一起使我們可以在當(dāng)然的硬盤子系統(tǒng)中獲得最大的性能收益。因?yàn)?Nginx 通過對不同硬盤開辟的并行,獨(dú)立的線程池。每個(gè)硬盤有16個(gè)獨(dú)立線程和一個(gè)讀寫文件的專用任務(wù)隊(duì)列。
我敢打賭,你的客戶一定喜歡這個(gè)量身定制的方法。不過要確保你的硬盤和例子里的一樣。
這個(gè)例子非常好地展示了 Nginx 可以多么靈活地專門為你的硬件做出調(diào)整。就像你給了 Nginx 一個(gè)硬件和你的數(shù)據(jù)集如何交互的最佳實(shí)踐手冊。而且 Nginx 也在用戶空間上做了很棒的協(xié)調(diào),你可以確保你的軟件,操作系統(tǒng)以及硬件結(jié)合在一起以最佳的模式盡可能高效地運(yùn)用你所有的系統(tǒng)資源。
結(jié)論
總的來說,線程池是一個(gè)非常偉大的特性,它使得 Nginx 在性能上達(dá)到了一個(gè)新的高度,它解決了一個(gè)著名而且持久的敵人 —— 阻塞,尤其是在談?wù)摲浅4篌w積的內(nèi)容時(shí)。
還會有更多的東西到來的。正如前面所提到的,這個(gè)新的接口是很有潛力的,它允許我們把任何的耗時(shí)阻塞操作扔到線程池里而不損失性能。 Nginx 為大量的新模塊和心功能帶來了新的希望。仍然有大量受歡迎的庫不支持異步非阻塞接口,此前它們和 Nginx 并不兼容。我們會花大量的時(shí)間和資源為一些庫開發(fā)我們自己的,新的非阻塞接口。但它值得我們付出這些努力么?現(xiàn)在,線程池已經(jīng)就緒了,使用這些庫相對來說可能變得更簡單了,使用這些模塊也不會很影響性能。
敬請關(guān)注。
來自:https://annatarhe.github.io/2017/08/11/Thread-Pools-in-NGINX-Boost-Performance-9x.html
掃碼二維碼 獲取免費(fèi)視頻學(xué)習(xí)資料
- 本文固定鏈接: http://www.wangchenghua.com/post/5775/
- 轉(zhuǎn)載請注明:轉(zhuǎn)載必須在正文中標(biāo)注并保留原文鏈接
- 掃碼: 掃上方二維碼獲取免費(fèi)視頻資料