天天看點

為什麼nginx性能如此出色?

聲明:本文的内容源于http://tengine.taobao.org/相關資料,如果想深入了解,可以到該網站檢視。

    nginx的高性能在業界已經是衆人皆知了,性能究竟有多高?官方測試Nginx能夠支撐5萬并發連接配接,在實際生産環境中可支撐2~4萬并發的連接配接數是沒有啥問題的。根據實戰Nginx書中描述,同等硬體環境下,Nginx的處理能力相當于Apache的5~10倍。而這麼高的性能,與其架構是分不開的。

    nginx在啟動後,在unix系統中會以daemon的方式在背景運作,背景程序包含一個master程序和多個worker程序。master程序主要用來管理worker程序,包含:接收來自外界的信号、向各worker程序發送信号和監控worker程序的運作狀态等。當worker程序退出後(異常情況下),會自動重新啟動新的worker程序。而基本的網絡事件,則是放在worker程序中來處理了。多個worker程序之間是對等的,他們同等競争來自用戶端的請求,各程序互相之間是獨立的。一個請求,隻可能在一個worker程序中處理,一個worker程序,不可能處理其它程序的請求。worker程序的個數是可以設定的,一般我們會設定與機器cpu核數一緻,這裡面的原因與nginx的程序模型以及事件處理模型是分不開的。

    nginx的程序模型,可以由下圖來表示:

為什麼nginx性能如此出色?

    從上圖中我們可以看到,master來管理worker程序,是以我們隻需要與master程序通信就行了。master程序會接收來自外界發來的信号,再根據信号做不同的事情。是以我們要控制nginx,隻需要通過kill向master程序發送信号就行了。

    那麼,worker進行又是如何處理請求的呢?前面有提到,worker程序之間是平等的,每個程序,處理請求的機會也是一樣的。首先,每個worker程序都是從master程序fork過來,在master程序裡面,先建立好需要listen的socket(listenfd)之後,然後再fork出多個worker程序。所有worker程序的listenfd會在新連接配接到來時變得可讀,為保證隻有一個程序處理該連接配接,所有worker程序在注冊listenfd讀事件前搶accept_mutex,搶到互斥鎖的那個程序注冊listenfd讀事件,在讀事件裡調用accept接受該連接配接。當一個worker程序在accept這個連接配接之後,就開始讀取請求,解析請求,處理請求,産生資料後,再傳回給用戶端,最後才斷開連接配接,這樣一個完整的請求就是這樣的了。我們可以看到,一個請求,完全由worker程序來處理,而且隻在一個worker程序中處理。

    nginx采用這種程序模型有什麼好處呢?當然,好處肯定會很多了。首先,對于每個worker程序來說,獨立的程序,不需要加鎖,是以省掉了鎖帶來的開銷,同時在程式設計以及問題查找時,也會友善很多。其次,采用獨立的程序,可以讓互相之間不會影響,一個程序退出後,其它程序還在工作,服務不會中斷,master程序則很快啟動新的worker程序。當然,worker程序的異常退出,肯定是程式有bug了,異常退出,會導緻目前worker上的所有請求失敗,不過不會影響到所有請求,是以降低了風險。當然,好處還有很多,大家可以慢慢體會。

    到這裡,有人可能要問了,nginx采用多worker的方式來處理請求,每個worker裡面隻有一個主線程,那能夠處理的并發數很有限啊,多少個worker就能處理多少個并發,何來高并發呢?非也,這就是nginx的高明之處,nginx采用了異步非阻塞的方式來處理請求,也就是說,nginx是可以同時處理成千上萬個請求的。想想apache的常用工作方式(apache也有異步非阻塞版本,但因其與自帶某些子產品沖突,是以不常用),每個請求會獨占一個工作線程,當并發數上到幾千時,就同時有幾千的線程在處理請求了。這對作業系統來說,是個不小的挑戰,線程帶來的記憶體占用非常大,線程的上下文切換帶來的cpu開銷很大,自然性能就上不去了,而這些開銷完全是沒有意義的。

    何為異步非阻塞?我們先回到原點,看看一個請求的完整過程。首先,請求過來,要建立連接配接,然後再接收資料,接收資料後,再發送資料。具體到系統底層,就是讀寫事件,而當讀寫事件沒有準備好時,必然不可操作,如果不用非阻塞的方式來調用,那就得阻塞調用了,事件沒有準備好,那就隻能等了,等事件準備好了,你再繼續吧。阻塞調用會進入核心等待,cpu就會讓出去給别人用了,對單線程的worker來說,顯然不合适,當網絡事件越多時,大家都在等待呢,cpu空閑下來沒人用,cpu使用率自然上不去了,更别談高并發了。好吧,你說加程序數,這跟apache的線程模型有什麼差別?注意,别增加無謂的上下文切換。是以,在nginx裡面,最忌諱阻塞的系統調用了。不要阻塞,那就非阻塞喽。非阻塞就是,事件沒有準備好,馬上傳回EAGAIN,告訴你,事件還沒準備好呢,你慌什麼,過會再來吧。好吧,你過一會,再來檢查一下事件,直到事件準備好了為止,在這期間,你就可以先去做其它事情,然後再來看看事件好了沒。雖然不阻塞了,但你得不時地過來檢查一下事件的狀态,你可以做更多的事情了,但帶來的開銷也是不小的。是以,才會有了異步非阻塞的事件處理機制,具體到系統調用就是像select/poll/epoll/kqueue這樣的系統調用。它們提供了一種機制,讓你可以同時監控多個事件,調用他們是阻塞的,但可以設定逾時時間,在逾時時間之内,如果有事件準備好了,就傳回。這種機制正好解決了我們上面的兩個問題,拿epoll為例(在後面的例子中,我們多以epoll為例子,以代表這一類函數),當事件沒準備好時,放到epoll裡面,事件準備好了,我們就去讀寫,當讀寫傳回EAGAIN時,我們将它再次加入到epoll裡面。這樣,隻要有事件準備好了,我們就去處理它,隻有當所有事件都沒準備好時,才在epoll裡面等着。這樣,我們就可以并發處理大量的并發了,當然,這裡的并發請求,是指未處理完的請求,線程隻有一個,是以同時能處理的請求當然隻有一個了,隻是在請求間進行不斷地切換而已,切換也是因為異步事件未準備好,而主動讓出的。這裡的切換是沒有任何代價,你可以了解為循環處理多個準備好的事件,事實上就是這樣的。與多線程相比,這種事件處理方式是有很大的優勢的,不需要建立線程,每個請求占用的記憶體也很少,沒有上下文切換,事件處理非常的輕量級。并發數再多也不會導緻無謂的資源浪費(上下文切換)。更多的并發數,隻是會占用更多的記憶體而已。 

    現在的網絡伺服器基本都采用這種方式,這也是nginx性能高效的主要原因。

繼續閱讀