Nginx 的線程池與效能剖析

WBOY
發布: 2016-08-08 09:19:43
原創
1069 人瀏覽過
如我們所知,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可能因長時間、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 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资源和网络容量,从内存中读取,以服务于上述的第二个客户端的请求。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/disk3 proxy_cache_path /mnt/disk1 levels=1:2 keys_z                  use_temp_path=off; proxy_cache_path /mnt/disk2 levels=1:2 keys_z                  use_temp_path=off; proxy_cache_path /mnt/disk3 levels=1:2 keys_z                  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在用戶空間中細粒度的調優,我們可以確保軟體、作業系統和硬體工作在最優模式下,盡可能有效地利用系統資源。 7. 總結綜上所述,線程池是一個偉大的功能,將NGINX推向了新的性能水平,除掉了一個眾所周知的長期危害——阻塞——尤其是當我們真正面對大量內容的時候。 甚至,還有更多的驚喜。正如前面提到的,這個全新的接口,有可能沒有任何性能損失地卸載任何長期阻塞操作。 NGINX在擁有大量的新模組和新功能方面,開闢了一方新天地。許多流行的庫仍然沒有提供非同步非阻塞接口,此前,這使得它們無法與NGINX相容。我們可以花大量的時間和資源,去開發自己的無阻塞原型庫,但這麼做始終都是值得的嗎?現在,有了線程池,我們可以相對容易地使用這些庫,而不會影響這些模組的效能。





以上就介紹了Nginx 的線程池與效能剖析,包括了方面的內容,希望對PHP教程有興趣的朋友有所幫助。

相關標籤:
來源:php.cn
本網站聲明
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn
熱門教學
更多>
最新下載
更多>
網站特效
網站源碼
網站素材
前端模板
關於我們 免責聲明 Sitemap
PHP中文網:公益線上PHP培訓,幫助PHP學習者快速成長!