衆所周知,nginx性能高,而nginx的高性能與其架構是分不開的linux
4.1 進程模型
Nginx在啓動後,會有一個master進程和多個worker進程。master進程主要用來管理worker進程,包含:接收來自外界的信號,向各worker進程發送信號,監控worker進程的運行狀態,當worker進程退出後(異常狀況下),會自動從新啓動新的worker進程。而基本的網絡事件,則是放在worker進程中來處理了。多個worker進程之間是對等的,他們同等競爭來自客戶端的請求,各進程互相之間是獨立的。一個請求,只可能在一個worker進程中處理,一個worker進程,不可能處理其它進程的請求。worker進程的個數是能夠設置的,通常咱們會設置與機器cpu核數一致,這裏面的緣由與nginx的進程模型以及事件處理模型是分不開的。nginx的進程模型,能夠由下圖來表示:nginx
master進程會接收來自外界發來的信號,再根據信號作不一樣的事情。因此咱們要控制nginx,只須要向master進程發送信號就好了。好比kill -HUP pid,則是告訴nginx,從容地重啓nginx,咱們通常用這個信號來重啓nginx(等效於高級命令:nginx -s reload),或從新加載配置,由於是從容地重啓,所以服務是不中斷的。master進程在接收到HUP信號後是怎麼作的呢?首先master進程在接到信號後,會先從新加載配置文件,而後再啓動新的worker進程,並向全部老的worker進程發送信號,告訴他們能夠光榮退休了。新的worker在啓動後,就開始接收新的請求,而老的worker在收到來自master的信號後,就再也不接收新的請求,而且在當前進程中的全部未處理完的請求處理完成後,再退出。web
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進程。編程
4.2 事件模型
所謂的同步和異步關注的是消息通知機制:後端
l 同步:調用發出不會當即返回,但一旦返回就能夠返回最終結果;服務器
l 異步:調用發出以後,被調用方當即返回消息,但返回的非最終結果;被調用者經過狀態、通知機制來通知調者,或經過回調函數來處理結果;網絡
所謂的阻塞和非阻塞關注的是調用等等調用結果(消息、返回值)時的狀態:多線程
l 阻塞:調用結果返回以前,調用者(調用線程)會被掛起;調用者只有在獲得結果以後纔會返回;架構
l 非阻塞:調用結果返回以前,調用不會阻塞當前線程;
常見的五種I/O模型以下:
l 阻塞型IO [blocking IO]
l 非阻塞型IO [nonblocking IO]
l 複用型IO [IO multiplexing] (slect,poll複用器)
l 信號驅動型IO [signal driven IO] (epoll)
l 異步IO [asyncrhonous IO]
能夠舉一個簡單的例子來講明Apache的工做流程,咱們平時去餐廳吃飯。餐廳的工做模式是一個服務員全程服務客戶,流程是這樣,服務員在門口等候客人(listen),客人到了就接待安排的餐桌上(accept),等着客戶點菜(request uri),去廚房叫師傅下單作菜(磁盤I/O),等待廚房作好(read),而後給客人上菜(send),整個下來服務員(進程)不少地方是阻塞的。這樣客人一多(HTTP請求一多),餐廳只能經過叫更多的服務員來服務(fork進程),可是因爲餐廳資源是有限的(CPU),一旦服務員太多管理成本很高(CPU上下文切換),這樣就進入一個瓶頸。
再來看看Nginx得怎麼處理?餐廳門口掛個門鈴(註冊epoll模型的listen),一旦有客人(HTTP請求)到達,派一個服務員去接待(accept),以後服務員就去忙其餘事情了(好比再去接待客人),等這位客人點好餐就叫服務員(數據到了read()),服務員過來拿走菜單到廚房(磁盤I/O),服務員又作其餘事情去了,等廚房作好了菜也喊服務員(磁盤I/O結束),服務員再給客人上菜(send()),廚房作好一個菜就給客人上一個,中間服務員能夠去幹其餘事情。整個過程被切分紅不少個階段,每一個階段都有相應的服務模塊。咱們想一想,這樣一旦客人多了,餐廳也能招待更多的人。
事件驅動實際上是很老的技術,早期的select、poll都是如此。後來基於內核通知的更高級事件機制出現,如libevent裏的epoll,使事件驅動性能得以提升。事件驅動的本質仍是IO事件,應用程序在多個IO句柄間快速切換,實現所謂的異步IO。事件驅動服務器,最適合作的就是這種IO密集型工做,如反向代理,它在客戶端與WEB服務器之間起一個數據中轉做用,純粹是IO操做,自身並不涉及到複雜計算。反向代理用事件驅動來作,顯然更好,一個工做進程就能夠run了,沒有進程、線程管理的開銷,CPU、內存消耗都小。
有人可能要問了,nginx採用多worker的方式來處理請求,每一個worker裏面只有一個主線程,那可以處理的併發數頗有限啊,多少個worker就能處理多少個併發,何來高併發呢?
進程數與併發數不存在很直接的關係,這取決取server採用的工做方式。若是一個server採用一個進程負責一個request的方式,那麼進程數就是併發數。那麼顯而易見的,就是會有不少進程在等待中。等什麼? webserver恰好屬於網絡io密集型應用,不算是計算密集型,最多的應該是等待網絡傳輸。
而nginx 的異步非阻塞工做方式正是利用了這點等待的時間。在須要等待的時候,這些進程就空閒出來待命了。所以表現爲少數幾個進程就解決了大量的併發問題。
nginx是如何利用的呢,簡單來講:一樣的4個進程,若是採用一個進程負責一個request的方式,那麼,同時進來4個request以後,每一個進程就負責其中一個,直至會話關閉。期間,若是有第5個request進來了。就沒法及時反應了,由於4個進程都沒幹完活呢,所以,通常有個調度進程,每當新進來了一個request,就新開個進程來處理。
nginx不這樣,每進來一個request,會有一個worker進程去處理。但不是全程的處理,處理到什麼程度呢?處理到可能發生阻塞的地方,好比向上遊(後端)服務器轉發request,並等待請求返回。那麼,這個處理的worker不會這麼傻等着,他會在發送完請求後,註冊一個事件:「若是upstream返回了,告訴我一聲,我再接着幹」。因而他就休息去了。此時,若是再有request 進來,他就能夠很快再按這種方式處理。而一旦上游服務器返回了,就會觸發這個事件,worker纔會來接手,這個request纔會接着往下走。
因爲web server的工做性質決定了每一個request的大部份生命都是在網絡傳輸中,實際上花費在server機器上的時間片很少。這是幾個進程就解決高併發的祕密所在。
Nginx採用epoll模型,異步非阻塞。對於Nginx來講,把一個完整的鏈接請求處理都劃分紅了事件,一個一個的事件。好比accept(), recv(),磁盤I/O,send()等,每部分都有相應的模塊去處理,一個完整的請求多是由幾百個模塊去處理。真正核心的就是事件收集和分發模塊,這就是管理全部模塊的核心。只有核心模塊的調度才能讓對應的模塊佔用CPU資源,從而處理請求。拿一個HTTP請求來講,首先在事件收集分發模塊註冊感興趣的監聽事件,註冊好以後不阻塞直接返回,接下來就不須要再管了,等待有鏈接來了內核會通知你(epoll的輪詢會告訴進程),cpu就能夠處理其餘事情去了。一旦有請求來,那麼對整個請求分配相應的上下文(其實已經預先分配好),這時候再註冊新的感興趣的事件(read函數),一樣客戶端數據來了內核會自動通知進程能夠去讀數據了,讀了數據以後就是解析,解析完後去磁盤找資源(I/O),一旦I/O完成會通知進程,進程開始給客戶端發回數據send(),這時候也不是阻塞的,調用後就等內核發回通知發送的結果就行。整個下來把一個請求分紅了不少個階段,每一個階段都到不少模塊去註冊,而後處理,都是異步非阻塞。異步這裏指的就是作一個事情,不須要等返回結果,作好了會自動通知你。
想一想apache的經常使用工做方式,每一個請求會獨佔一個工做線程,當併發數上到幾千時,就同時有幾千的線程在處理請求了。這對操做系統來講,是個不小的挑戰,線程帶來的內存佔用很是大,線程的上下文切換帶來的cpu開銷很大,天然性能就上不去了,而這些開銷徹底是沒有意義的。
爲何nginx能夠採用異步非阻塞的方式來處理呢?咱們先回到原點,看看一個請求的完整過程。首先,請求過來,要創建鏈接,而後再接收數據,接收數據後,再發送數據。具體到系統底層,就是讀寫事件,而當讀寫事件沒有準備好時,必然不可操做,若是不用非阻塞的方式來調用,那就得阻塞調用了,事件沒有準備好,那就只能等了,等事件準備好了,你再繼續吧。阻塞調用會進入內核等待,cpu就會讓出去給別人用了,對單線程的worker來講,顯然不合適,當網絡事件越多時,你們都在等待呢,cpu空閒下來沒人用,cpu利用率天然上不去了,更別談高併發了。好吧,你說加進程數,這跟apache的線程模型有什麼區別,注意,別增長無謂的上下文切換。因此,在nginx裏面,最忌諱阻塞的系統調用了。不要阻塞,那就非阻塞嘍。非阻塞就是,事件沒有準備好,立刻返回EAGAIN,告訴你,事件還沒準備好呢,你慌什麼,過會再來吧。好吧,你過一會,再來檢查一下事件,直到事件準備好了爲止,在這期間,你就能夠先去作其它事情,而後再來看看事件好了沒。雖然不阻塞了,但你得不時地過來檢查一下事件的狀態,你能夠作更多的事情了,但帶來的開銷也是不小的。因此,纔會有了異步非阻塞的事件處理機制,具體到系統調用就是像select/poll/epoll/kqueue這樣的系統調用。它們提供了一種機制,讓你能夠同時監控多個事件,調用他們是阻塞的,但能夠設置超時時間,在超時時間以內,若是有事件準備好了,就返回。這種機制正好解決了咱們上面的兩個問題,拿epoll爲例,當事件沒準備好時,放到epoll裏面,事件準備好了,咱們就去讀寫,當讀寫返回EAGAIN時,咱們將它再次加入到epoll裏面。這樣,只要有事件準備好了,咱們就去處理它,只有當全部事件都沒準備好時,纔在epoll裏面等着。這樣,咱們就能夠併發處理大量的併發了,固然,這裏的併發請求,是指未處理完的請求,線程只有一個,因此同時能處理的請求固然只有一個了,只是在請求間進行不斷地切換而已,切換也是由於異步事件未準備好,而主動讓出的。這裏的切換是沒有任何代價,能夠理解爲循環處理多個準備好的事件,事實上就是這樣的。與多線程相比,這種事件處理方式是有很大的優點的,不須要建立線程,每一個請求佔用的內存也不多,沒有上下文切換,事件處理很是的輕量級。併發數再多也不會致使無謂的資源浪費(上下文切換)。更多的併發數,只是會佔用更多的內存而已。如今的網絡服務器基本都採用這種方式,這也是nginx性能高效的主要緣由。
以前說過,推薦設置worker的個數爲cpu的核數,在這裏就很容易理解了,更多的worker數,只會致使進程來競爭cpu資源了,從而帶來沒必要要的上下文切換。並且,nginx爲了更好的利用多核特性,提供了cpu親緣性的綁定選項,咱們能夠將某一個進程綁定在某一個核上,這樣就不會由於進程的切換帶來cache的失效。像這種小的優化在nginx中很是常見,同時也說明了nginx做者的苦心孤詣。好比,nginx在作4個字節的字符串比較時,會將4個字符轉換成一個int型,再做比較,以減小cpu的指令數等等。
異步,非阻塞,使用epoll和大量細節處的優化,是使Nginx脫穎而出的技術基石。
4.3 select模型與epoll模型
epoll是多路複用IO(I/O Multiplexing)中的一種方式,可是僅用於linux2.6以上內核,在開始討論這個問題以前,先來解釋一下爲何須要多路複用IO.
以一個生活中的例子來解釋。
假設你在大學中讀書,要等待一個朋友來訪,而這個朋友只知道你在A號樓,可是不知道你具體住在哪裏,因而大家約好了在A號樓門口見面.
若是你使用的阻塞IO模型來處理這個問題,那麼你就只能一直守候在A號樓門口等待朋友的到來,在這段時間裏你不能作別的事情,不難知道,這種方式的效率是低下的.
如今時代變化了,開始使用多路複用IO模型來處理這個問題.你告訴你的朋友來了A號樓找樓管大媽,讓她告訴你該怎麼走.這裏的樓管大媽扮演的就是多路複用IO的角色.
進一步解釋select和epoll模型的差別.
select版大媽作的是以下的事情:好比同窗甲的朋友來了,select版大媽比較笨,她帶着朋友挨個房間進行查詢誰是同窗甲,你等的朋友來了,因而在實際的代碼中,select版大媽作的是如下的事情:
epoll版大媽就比較先進了,她記下了同窗甲的信息,好比說他的房間號,那麼等同窗甲的朋友到來時,只須要告訴該朋友同窗甲在哪一個房間便可,不用本身親自帶着人滿大樓的找人了.
別小看了這些效率的提升,在一個大規模併發的服務器中,輪詢IO是最耗時間的操做之一.再回到那個例子中,若是每到來一個朋友樓管大媽都要全樓的查詢同窗,那麼處理的效率必然就低下了,過不久樓底就有很多的人了.
對比最先給出的阻塞IO的處理模型, 能夠看到採用了多路複用IO以後, 程序能夠自由的進行本身除了IO操做以外的工做, 只有到IO狀態發生變化的時候由多路複用IO進行通知, 而後再採起相應的操做, 而不用一直阻塞等待IO狀態發生變化了.
select的特色:select 選擇句柄的時候,是遍歷全部句柄,也就是說句柄有事件響應時,select須要遍歷全部句柄才能獲取到哪些句柄有事件通知,所以效率是很是低。可是若是鏈接不多的狀況下, select和epoll的LT觸發模式相比,性能上差異不大。
這裏要多說一句,select支持的句柄數是有限制的,同時只支持1024個,這個是句柄集合限制的,若是超過這個限制,極可能致使溢出,並且很是不容易發現問題, 固然能夠經過修改linux的socket內核調整這個參數。
epoll的特色:epoll對於句柄事件的選擇不是遍歷的,是事件響應的,就是句柄上事件來就立刻選擇出來,不須要遍歷整個句柄鏈表,所以效率很是高,內核將句柄用紅黑樹保存的。