Nginx線程池性能提升9倍(Thread Pools in NGINX Boost Performance 9x!)

jopen 9年前發布 | 34K 次閱讀 Nginx Web服務器

五年級英語水平,端午家庭作業。

前言

Nginx以異步、事件驅動的方式處理連接。傳統的方式是每個請求新起一個進程或線程,Nginx沒這樣做,它通過非阻塞sockets、epoll、kqueue等高效手段,實現一個worker進程處理多個連接和請求。

一般情況下下是一個CPU內核對應一個worker進程,所以worker進程數量固定,并且不多,所以在任務切換上消耗的內存和CPU減少了。這種方式很不錯,在高并發和擴展能力等方面都能體現。

看圖說話,任務切換不見了。

Nginx線程池性能提升9倍(Thread Pools in NGINX Boost Performance 9x!)

但是異步事件模式很不喜歡阻塞(blocking)。很多第三方模塊使用了阻塞模式的調用,有時候,用戶乃至模塊作者都不知道到阻塞調用會大大降低Nginx的性能。

Nginx自己的代碼都有一些場景需要使用到阻塞,所以在1.7.11版本中,引入了新的“線程池”機制,在了解這個機制前,我們先瞅瞅阻塞。

問題

了解阻塞前,先講兩句

Nginx其實就是一個事件處理器,接收內核發出的所有與connections相關的事件信息,然后告訴操作系統該做什么。操作系統如此復雜和底層,所以Nginx的指揮必須叼。

Nginx線程池性能提升9倍(Thread Pools in NGINX Boost Performance 9x!)

從上圖看,有超時、sockets準備好讀寫、錯誤通知等事件。這些事件都放在一個隊列中,Nginx對事件隊列進行處理。

Nginx線程池性能提升9倍(Thread Pools in NGINX Boost Performance 9x!)

如果一個事件對于到的操作非常耗時,那么整個隊列的處理就會延遲。

“阻塞操作”就是這樣一個導致隊列處理延遲的什么鬼。舉個例子,CPU密集型計算,資源訪問(硬盤、mutex、同步訪問數據庫等等)。發生阻塞時,worker進程只能等待。

就跟過安檢時一樣,如果你的隊伍里面有個小朋友帶了一大瓶AD鈣奶,那你只有等他喝完。

Nginx線程池性能提升9倍(Thread Pools in NGINX Boost Performance 9x!)

有些系統提供的異步文件接口,例如FreeBSD。Linux也提供了類似機制,但是不太好用。首先它要求文件或緩存是扇區對齊的,好吧,Nginx能做到。其次更苛刻的一點是,它要求文件設置O_DIRECT標志位,這就是說,所有訪問這個文件的操作都是直接讀取,不走任何緩存,這樣反而會增加磁盤IO負擔。

問了解決這些問題,1.7.11版本中引入了線程池。

線程池

你家樓下的順豐快遞就是一個線程池,不用每次寄快遞都要去順豐總部,狗屎一樣的比喻。。

Nginx線程池性能提升9倍(Thread Pools in NGINX Boost Performance 9x!)

對Nginx來說,線程池的作用跟快遞點一樣。它包括一個任務隊列以及配套線程。當一個worker進行需要處理阻塞操作時,它會將這個任務交給線程池來完成。

Nginx線程池性能提升9倍(Thread Pools in NGINX Boost Performance 9x!)

這里引入了一個新的隊列,在例子中,這個隊列因為讀取資源導致緩慢,讀取硬盤雖然慢,至少它不會影響事件隊列的繼續處理。

任何阻塞操作都可以放到線程池中。目前,我們只嘗試了兩個核心操作:主流操作系統的read()系統調用和Linux上的sendfile()。后續經過性能測試會考慮納入更多的操作。

性能測試

為了證實上述理論,進行了如下測試,測試場景包括各種阻塞操作和非阻塞操作。

我們在一臺48G內存的機器上生成了總共256的隨機文件,每個文件大小為4MB。這樣做的目的是保證數據不受內存緩存影響。

簡單的配置如下:

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和accpet_mutex,啟用sendfile并設置sendfile_max_chunk,有利于減少阻塞調用sendfile時帶來的總時間。

測試機器配置為雙Intel至強E5645(共12核-24線程),10G網卡,四塊西數1003FBYX組成的RAID10,系統為Ubuntu Server 14.04.1 LTS。

Nginx線程池性能提升9倍(Thread Pools in NGINX Boost Performance 9x!)

兩臺配置一樣的客戶端,一臺機器通過Lua和wrk隨機產生200個并發請求,每個請求都不會命中緩存,所以Nginx處理時會產生讀盤阻塞操作。另一臺機器則是產生50個并發請求,每個請求讀取固定文件,頻繁的文件讀取會命中緩存,所以一般情況下此類請求處理速度較快,當worker進程阻塞時請求速度會受影響。

通過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進程主要消耗在阻塞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</pre>

IO受磁盤限制,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

需要提醒的是,這些請求原本是應該命中緩存非常快速的,但是因為worker進程受第一臺服務器的200并發影響,所以最終比較慢。

接下來對照線程池實驗,在location配置中添加一個aio線程指令

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的吞吐性能。

性能沒準還能更改,因為已經達到了網卡瓶頸。這次,worker進程主要消耗在sleeping和時間等待上(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</pre>

就是說,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.41秒提升至了226.32毫秒(約33倍),QPS提升了大約31倍(250比8)。

提升的原因不再贅述,大約就是事件隊列沒有受阻罷了。

不是萬靈丹

看到這里,是不是立馬就想去修改你的生產環境了,且慢。

事實上,絕大多數的read和sendfile都是在緩存頁中進行的,操作系統會把頻繁使用的文件放在緩存頁中。

當你的數據量較小,并且內存足夠大時,Nginx已經是處于最佳狀態了,開線程池反倒會引入開銷。線程池能夠良好應對的一個場景,是數據無法被完全緩存,例如流媒體服務器,我們上面的測試環境,就是模擬的流媒體服務。

能否用線程池來提升讀操作的性能呢?唯一需要做的,就是能有效區分哪些文件已經被緩存,哪些文件未緩存。

咱們的系統沒有提供這樣的信息。早在2010年Linux嘗試通過fincore()來實現未果。接下來是preadv2()和RWF_NONBLOCK標志位方式,可惜也不好用,具體可以參考內核bikeshedding一文。

哈哈,至少FreeBSD用戶可以先喝咖啡了,無需在線程池問題上傷腦筋。

線程池配置

如果你確信引入線程池對性能提升有效,那么咱們可以繼續了解一些調優參數。

這些調優都是基于1.7.11+ 版本,編譯選項為--with-threads參數。最簡單的場景下,僅需在http、server或location區塊配置aio thread參數即可

aio threads;

它對應的完整配置是

thread_pool default threads=32 max_queue=65536;
aio threads=default;

默認情況下包括一個32個線程的線程池,長度為65536的請求隊列。如果隊列溢出,Nginx會輸出如下錯誤并拒絕請求。

thread pool "NAME" queue overflow: N tasks waiting

這個錯誤表示這個線程池消費小于生產,所以可以增加隊列長度,如果調整無效,說明系統達到了瓶頸。

另外,我們可以調整線程相關的參數,例如對不同場景,可以提供獨立的線程池。

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,當設置成0時,服務能力等同線程數量。

假如你的緩存代理服務器有3塊磁盤,內存不能放下預期需要緩存的文件,所以我們首先需要讓磁盤工作最大化。

一個方式是RAID,好壞兼并。另一個方式是Nginx

# We assume that each of the hard drives is mounted on one of the directories:

/mnt/disk1, /mnt/disk2, or /mnt/disk3 accordingly

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; proxycache cache$disk; aio threads=pool_$disk; sendfile on; }</pre>

使用了3個獨立的緩存,每個緩存指定到一塊磁盤,然后有3個獨立的線程池。

split_clients模塊用于緩存間的負載均衡。

use_temp_path=off參數讓Nginx將緩存文件保存至文件同級目錄,可以避免緩存更新時磁盤間的文件數據交換。

結論

明天他媽又要上課了

原文地址: http://nginx.com/blog/thread-pools-boost-performance-9x/

原文 http://segmentfault.com/a/1190000002924458

 本文由用戶 jopen 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!