nginx學習(二)——基礎概念之異步非阻塞

上面講了不少關於nginx的進程模型,接下來,咱們來看看nginx是如何處理事件的。java


有人可能要問了,nginx採用多worker的方式來處理請求,每一個worker裏面只有一個主線程,那可以處理的併發數頗有限啊,多少個worker就能處理多少個併發,何來高併發呢?非也,這就是nginx的高明之處,nginx採用了異步非阻塞的方式來處理請求,也就是說,nginx是能夠同時處理成千上萬個請求的。想一想apache的經常使用工做方式(apache也有異步非阻塞版本,但因其與自帶某些模塊衝突,因此不經常使用),每一個請求會獨佔一個工做線程,當併發數上到幾千時,就同時有幾千的線程在處理請求了。這對操做系統來講,是個不小的挑戰,線程帶來的內存佔用很是大,線程的上下文切換帶來的cpu開銷很大,天然性能就上不去了,而這些開銷徹底是沒有意義的。


爲何nginx能夠採用異步非阻塞的方式來處理呢,或者異步非阻塞究竟是怎麼回事呢?咱們先回到原點,看看一個請求的完整過程。首先,請求過來,要創建鏈接,而後再接收數據,接收數據後,再發送數據。具體到系統底層,就是讀寫事件,而當讀寫事件沒有準備好時,必然不可操做,若是不用非阻塞的方式來調用,那就得阻塞調用了,事件沒有準備好,那就只能等了,等事件準備好了,你再繼續吧。阻塞調用會進入內核等待,cpu就會讓出去給別人用了,對單線程的worker來講,顯然不合適,當網絡事件越多時,你們都在等待呢,cpu空閒下來沒人用,cpu利用率天然上不去了,更別談高併發了。好吧,你說加進程數,這跟apache的線程模型有什麼區別,注意,別增長無謂的上下文切換。因此,在nginx裏面,最忌諱阻塞的系統調用了。不要阻塞,那就非阻塞嘍。非阻塞就是,事件沒有準備好,立刻返回EAGAIN,告訴你,事件還沒準備好呢,你慌什麼,過會再來吧。好吧,你過一會,再來檢查一下事件,直到事件準備好了爲止,在這期間,你就能夠先去作其它事情,而後再來看看事件好了沒。雖然不阻塞了,但你得不時地過來檢查一下事件的狀態,你能夠作更多的事情了,但帶來的開銷也是不小的。因此,纔會有了異步非阻塞的事件處理機制,具體到系統調用就是像select/poll/epoll/kqueue這樣的系統調用。它們提供了一種機制,讓你能夠同時監控多個事件,調用他們是阻塞的,但能夠設置超時時間,在超時時間以內,若是有事件準備好了,就返回。這種機制正好解決了咱們上面的兩個問題,拿epoll爲例(在後面的例子中,咱們多以epoll爲例子,以表明這一類函數),當事件沒準備好時,放到epoll裏面,事件準備好了,咱們就去讀寫,當讀寫返回EAGAIN時,咱們將它再次加入到epoll裏面。這樣,只要有事件準備好了,咱們就去處理它,只有當全部事件都沒準備好時,纔在epoll裏面等着。這樣,咱們就能夠併發處理大量的併發了,固然,這裏的併發請求,是指未處理完的請求,線程只有一個,因此同時能處理的請求固然只有一個了,只是在請求間進行不斷地切換而已,切換也是由於異步事件未準備好,而主動讓出的。這裏的切換是沒有任何代價,你能夠理解爲循環處理多個準備好的事件,事實上就是這樣的。與多線程相比,這種事件處理方式是有很大的優點的,不須要建立線程,每一個請求佔用的內存也不多,沒有上下文切換,事件處理很是的輕量級。併發數再多也不會致使無謂的資源浪費(上下文切換)。更多的併發數,只是會佔用更多的內存而已。 我以前有對鏈接數進行過測試,在24G內存的機器上,處理的併發請求數達到過200萬。如今的網絡服務器基本都採用這種方式,這也是nginx性能高效的主要緣由。


咱們以前說過,推薦設置worker的個數爲cpu的核數,在這裏就很容易理解了,更多的worker數,只會致使進程來競爭cpu資源了,從而帶來沒必要要的上下文切換。並且,nginx爲了更好的利用多核特性,提供了cpu親緣性的綁定選項,咱們能夠將某一個進程綁定在某一個核上,這樣就不會由於進程的切換帶來cache的失效。像這種小的優化在nginx中很是常見,同時也說明了nginx做者的苦心孤詣。好比,nginx在作4個字節的字符串比較時,會將4個字符轉換成一個int型,再做比較,以減小cpu的指令數等等。


如今,知道了nginx爲何會選擇這樣的進程模型與事件模型了。對於一個基本的web服務器來講,事件一般有三種類型,網絡事件、信號、定時器。從上面的講解中知道,網絡事件經過異步非阻塞能夠很好的解決掉。如何處理信號與定時器?


首先,信號的處理。對nginx來講,有一些特定的信號,表明着特定的意義。信號會中斷掉程序當前的運行,在改變狀態後,繼續執行。若是是系統調用,則可能會致使系統調用的失敗,須要重入。關於信號的處理,你們能夠學習一些專業書籍,這裏很少說。對於nginx來講,若是nginx正在等待事件(epoll_wait時),若是程序收到信號,在信號處理函數處理完後,epoll_wait會返回錯誤,而後程序可再次進入epoll_wait調用。


另外,再來看看定時器。因爲epoll_wait等函數在調用的時候是能夠設置一個超時時間的,因此nginx藉助這個超時時間來實現定時器。nginx裏面的定時器事件是放在一顆維護定時器的紅黑樹裏面,每次在進入epoll_wait前,先從該紅黑樹裏面拿到全部定時器事件的最小時間,在計算出epoll_wait的超時時間後進入epoll_wait。因此,當沒有事件產生,也沒有中斷信號時,epoll_wait會超時,也就是說,定時器事件到了。這時,nginx會檢查全部的超時事件,將他們的狀態設置爲超時,而後再去處理網絡事件。由此能夠看出,當咱們寫nginx代碼時,在處理網絡事件的回調函數時,一般作的第一個事情就是判斷超時,而後再去處理網絡事件。


nginx

咱們能夠用一段僞代碼來總結一下nginx的事件處理模型:web

 

[java]  view plain  copy
  1. while (true) {  
  2.     for t in run_tasks:  
  3.         t.handler();  
  4.     update_time(&now);  
  5.     timeout = ETERNITY;  
  6.     for t in wait_tasks: /* sorted already */  
  7.         if (t.time <= now) {  
  8.             t.timeout_handler();  
  9.         } else {  
  10.             timeout = t.time - now;  
  11.             break;  
  12.         }  
  13.     nevents = poll_function(events, timeout);  
  14.     for i in nevents:  
  15.         task t;  
  16.         if (events[i].type == READ) {  
  17.             t.handler = read_handler;  
  18.         } else { /* events[i].type == WRITE */  
  19.             t.handler = write_handler;  
  20.         }  
  21.         run_tasks_add(t);  
  22. }  

 

總結:

IO密集型場景下,因爲阻塞IO會讓出CPU,而nginx的Worker又是單線程(在作IO,其餘啥也幹不了),致使CPU利用率下降。
這時增長進程的話,和Apache的線程模型同樣,會增長無謂的上下文切換。
同步非阻塞,當即返回EAGAIN,告知沒準備好數據,能夠作其餘事情,可是須要去輪詢。
異步非阻塞,能夠同時監控多個事件,調用時是帶超時時間的阻塞,準備好了就返回,沒準備好放到epoll中等待。這裏Worker仍是單線程,能處理的請求只有一個,可是會在請求間作切換,這個切換沒有開銷,由於是異步事件沒有準備好,自動讓出的。
相關文章
相關標籤/搜索