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