Nginx架構學習筆記

  衆所周知,Nginx 性能高,而 Nginx 的高性能與其架構是分不開的。那麼 Nginx 到底是怎麼樣的呢?html

  Nginx 在啓動後,在 unix/linux 系統中會以 daemon 的方式在後臺運行,後臺進程包含一個 master 進程和多個 worker 進程。咱們也能夠手動地關掉後臺模式,讓 Nginx 在前臺運行,而且經過配置讓 Nginx 取消 master 進程,從而可使 Nginx 以單進程方式運行。很顯然,生產環境下咱們確定不會這麼作,因此關閉後臺模式,通常是用來調試用的。因此,咱們能夠看到,Nginx 是以多進程的方式來工做的,固然 Nginx 也是支持多線程的方式的,只是咱們主流的方式仍是多進程的方式,這也是 Nginx 的默認方式。Nginx 採用多進程的方式有諸多好處,因此這裏就主要講解 Nginx 的多進程模式。linux

  剛纔講到,Nginx 在啓動後,會有一個 master 進程和多個 worker 進程。master 進程主要用來管理 worker 進程,包含:接收來自外界的信號,向各 worker 進程發送信號,監控 worker 進程的運行狀態,當 worker 進程退出後(異常狀況下),會自動從新啓動新的 worker 進程。而基本的網絡事件,則是放在 worker 進程中來處理了。多個 worker 進程之間是對等的,他們同等競爭來自客戶端的請求,各進程互相之間是獨立的。一個請求,只可能在一個 worker 進程中處理,一個 worker 進程,不可能處理其它進程的請求。worker 進程的個數是能夠設置的,通常咱們會設置與機器cpu核數一致,這裏面的緣由與 Nginx 的進程模型以及事件處理模型是分不開的。Nginx 的進程模型,能夠由下圖來表示:nginx

  worker 進程之間是平等的,每一個進程,處理請求的機會也是同樣的。當咱們提供 80 端口的 http 服務時,一個鏈接請求過來,每一個進程都有可能處理這個鏈接,怎麼作到的呢?首先,每一個 worker 進程都是從 master 進程 fork 過來,在 master 進程裏面,先創建好須要 listen 的 socket(listenfd)以後,而後再 fork 出多個 worker 進程。全部 worker 進程的 listenfd 會在新鏈接到來時變得可讀,爲保證只有一個進程處理該鏈接,全部 worker 進程在註冊 listenfd 讀事件前搶 accept_mutex,搶到互斥鎖的那個進程註冊 listenfd 讀事件,在讀事件裏調用 accept 接受該鏈接。當一個 worker 進程在 accept 這個鏈接以後,就開始讀取請求,解析請求,處理請求,產生數據後,再返回給客戶端,最後才斷開鏈接,這樣一個完整的請求就是這樣的了。咱們能夠看到,一個請求,徹底由 worker 進程來處理,並且只在一個 worker 進程中處理。apache

  那麼,Nginx 採用這種進程模型有什麼好處呢?固然,好處確定會不少了。首先,對於每一個 worker 進程來講,獨立的進程,不須要加鎖,因此省掉了鎖帶來的開銷,同時在編程以及問題查找時,也會方便不少。其次,採用獨立的進程,可讓互相之間不會影響,一個進程退出後,其它進程還在工做,服務不會中斷,master 進程則很快啓動新的 worker 進程。固然,worker 進程的異常退出,確定是程序有 bug 了,異常退出,會致使當前 worker 上的全部請求失敗,不過不會影響到全部請求,因此下降了風險。編程

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

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

  爲何 Nginx 能夠採用異步非阻塞的方式來處理呢,或者異步非阻塞究竟是怎麼回事呢?咱們先回到原點,看看一個請求的完整過程。多線程

  首先,請求過來,要創建鏈接,而後再接收數據,接收數據後,再發送數據。具體到系統底層,就是讀寫事件,而當讀寫事件沒有準備好時,必然不可操做,若是不用非阻塞的方式來調用,那就得阻塞調用了,事件沒有準備好,那就只能等了,等事件準備好了,你再繼續吧。阻塞調用會進入內核等待,cpu 就會讓出去給別人用了,對單線程的 worker 來講,顯然不合適,當網絡事件越多時,你們都在等待呢,cpu 空閒下來沒人用,cpu利用率天然上不去了,更別談高併發了。好吧,你說加進程數,這跟apache的線程模型有什麼區別,注意,別增長無謂的上下文切換。因此,在 Nginx 裏面,最忌諱阻塞的系統調用了。不要阻塞,那就非阻塞嘍。非阻塞就是,事件沒有準備好,立刻返回 EAGAIN,告訴你,事件還沒準備好呢,你慌什麼,過會再來吧。好吧,你過一會,再來檢查一下事件,直到事件準備好了爲止,在這期間,你就能夠先去作其它事情,而後再來看看事件好了沒。雖然不阻塞了,但你得不時地過來檢查一下事件的狀態,你能夠作更多的事情了,但帶來的開銷也是不小的。因此,纔會有了異步非阻塞的事件處理機制,具體到系統調用就是像 select/poll/epoll/kqueue 這樣的系統調用(events裏面能夠配置)。它們提供了一種機制,讓你能夠同時監控多個事件,調用他們是阻塞的,但能夠設置超時時間,在超時時間以內,若是有事件準備好了,就返回。這種機制正好解決了咱們上面的兩個問題,拿 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 的事件處理模型:

 while (true) { for t in run_tasks: t.handler(); update_time(&now); timeout = ETERNITY; for t in wait_tasks: /* sorted already */ if (t.time <= now) { t.timeout_handler(); } else { timeout = t.time - now; break; } nevents = poll_function(events, timeout); for i in nevents: task t; if (events[i].type == READ) { t.handler = read_handler; } else { /* events[i].type == WRITE */ t.handler = write_handler; } run_tasks_add(t); } 

  參考:http://tengine.taobao.org/book/index.html

相關文章
相關標籤/搜索