Apache與Nginx網路模型
Nginx的高併發得益於其採用了epoll模型,與傳統的伺服器程式架構不同,epoll是linux核心2.6以後才出現的。下面通過比較Apache和Nginx工作原理來比較。
傳統Apache都是多程序或者多執行緒來工作,假設是多程序工作(prefork),apache會先生成幾個程序,類似程序池的工作原理,只不過這裡的程序池會隨著請求數目的增加而增加。對於每一個連線,apache都是在一個程序內處理完畢。具體是 recv(),以及根據 URI 去進行磁碟I/O來尋找檔案,還有 send()都是阻塞的。其實說白了都是 apche 對於套接字的I/O,讀或者寫,但是讀或者寫都是阻塞的,阻塞意味著程序就得掛起進入sleep狀態,那麼一旦連線數很多,Apache必然要生成更多的程序來響應請求,一旦程序多了,CPU對於程序的切換就頻繁了,很耗資源和時間,所以就導致apache效能下降了,說白了就是處理不過來這麼多程序了。其實仔細想想,如果對於程序每個請求都沒有阻塞,那麼效率肯定會提高很多。 Nginx採用epoll模型,非同步非阻塞。對於Nginx來說,把一個完整的連線請求處理都劃分成了事件,一個一個的事件。比如accept(), recv(),磁碟I/O,send()等,每部分都有相應的模組去處理,一個完整的請求可能是由幾百個模組去處理。真正核心的就是事件收集和分發模組,這就是管理所有模組的核心。只有核心模組的排程才能讓對應的模組佔用CPU資源,從而處理請求。拿一個HTTP請求來說,首先在事件收集分發模組註冊感興趣的監聽事件,註冊好之後不阻塞直接返回,接下來就不需要再管了,等待有連線來了核心會通知你(epoll的輪詢會告訴程序),cpu就可以處理其他事情去了。一旦有請求來,那麼對整個請求分配相應的上下文(其實已經預先分配好),這時候再註冊新的感興趣的事件(read函式),同樣客戶端資料來了核心會自動通知程序可以去讀資料了,讀了資料之後就是解析,解析完後去磁碟找資源(I/O),一旦I/O完成會通知程序,程序開始給客戶端發回資料send(),這時候也不是阻塞的,呼叫後就等核心發回通知傳送的結果就行。整個下來把一個請求分成了很多個階段,每個階段都到很多模組去註冊,然後處理,都是非同步非阻塞。非同步這裡指的就是做一個事情,不需要等返回結果,做好了會自動通知你。
select/epoll的特點
select的特點:select 選擇控制代碼的時候,是遍歷所有控制代碼,也就是說控制代碼有事件響應時,select需要遍歷所有控制代碼才能獲取到哪些控制代碼有事件通知,因此效率是非常低。但是如果連線很少的情況下, select和epoll的LT觸發模式相比, 效能上差別不大。 這裡要多說一句,select支援的控制代碼數是有限制的, 同時只支援1024個,這個是控制代碼集合限制的,如果超過這個限制,很可能導致溢位,而且非常不容易發現問題, 當然可以通過修改linux的socket核心調整這個引數。 epoll的特點:epoll對於控制代碼事件的選擇不是遍歷的,是事件響應的,就是控制代碼上事件來就馬上選擇出來,不需要遍歷整個控制代碼連結串列,因此效率非常高,核心將控制代碼用紅黑樹儲存的。 對於epoll而言還有ET和LT的區別,LT表示水平觸發,ET表示邊緣觸發,兩者在效能以及程式碼實現上差別也是非常大的。
Epoll模型主要負責對大量併發使用者的請求進行及時處理,完成伺服器與客戶端的資料互動。其具體的實現步驟如下: (a) 使用epoll_create()函式建立檔案描述,設定將可管理的最大socket描述符數目。 (b) 建立與epoll關聯的接收執行緒,應用程式可以建立多個接收執行緒來處理epoll上的讀通知事件,執行緒的數量依賴於程式的具體需要。 © 建立一個偵聽socket描述符ListenSock;將該描述符設定為非阻塞模式,呼叫Listen()函式在套接字上偵聽有無新的連線請求,在 epoll_event結構中設定要處理的事件型別EPOLLIN,工作方式為 epoll_ET,以提高工作效率,同時使用epoll_ctl()註冊事件,最後啟動網路監視執行緒。 (d) 網路監視執行緒啟動迴圈,epoll_wait()等待epoll事件發生。 (e) 如果epoll事件表明有新的連線請求,則呼叫accept()函式,將使用者socket描述符新增到epoll_data聯合體,同時設定該描述符為非 阻塞,並在epoll_event結構中設定要處理的事件型別為讀和寫,工作方式為epoll_ET. (f) 如果epoll事件表明socket描述符上有資料可讀,則將該socket描述符加入可讀佇列,通知接收執行緒讀入資料,並將接收到的資料放入到接收資料 的連結串列中,經邏輯處理後,將反饋的資料包放入到傳送資料鏈表中,等待由傳送執行緒傳送。
epoll的操作就這麼簡單,總共不過4個 API:epoll_create, epoll_ctl, epoll_wait和close。
可以舉一個簡單的例子來說明Apache的工作流程,我們平時去餐廳吃飯。餐廳的工作模式是一個服務員全程服務客戶,流程是這樣,服務員在門口等候客人(listen),客人到了就接待安排的餐桌上(accept),等著客戶點菜(request uri),去廚房叫師傅下單做菜(磁碟I/O),等待廚房做好(read),然後給客人上菜(send),整個下來服務員(程序)很多地方是阻塞的。這樣客人一多(HTTP請求一多),餐廳只能通過叫更多的服務員來服務(fork程序),但是由於餐廳資源是有限的(CPU),一旦服務員太多管理成本很高(CPU上下文切換),這樣就進入一個瓶頸。
再來看看Nginx得怎麼處理?餐廳門口掛個門鈴(註冊epoll模型的listen),一旦有客人(HTTP請求)到達,派一個服務員去接待(accept),之後服務員就去忙其他事情了(比如再去接待客人),等這位客人點好餐就叫服務員(資料到了read()),服務員過來拿走選單到廚房(磁碟I/O),服務員又做其他事情去了,等廚房做好了菜也喊服務員(磁碟I/O結束),服務員再給客人上菜(send()),廚房做好一個菜就給客人上一個,中間服務員可以去幹其他事情。整個過程被切分成很多個階段,每個階段都有相應的服務模組。我們想想,這樣一旦客人多了,餐廳也能招待更多的人。
不管是Nginx還是Squid這種反向代理,其網路模式都是事件驅動。事件驅動其實是很老的技術,早期的select、poll都是如此。後來基於核心通知的更高階事件機制出現,如libevent裡的epoll,使事件驅動效能得以提高。事件驅動的本質還是IO事件,應用程式在多個IO控制代碼間快速切換,實現所謂的非同步IO。事件驅動伺服器,最適合做的就是這種IO密集型工作,如反向代理,它在客戶端與WEB伺服器之間起一個數據中轉作用,純粹是IO操作,自身並不涉及到複雜計算。反向代理用事件驅動來做,顯然更好,一個工作程序就可以run了,沒有程序、執行緒管理的開銷,CPU、記憶體消耗都小。
所以Nginx、Squid都是這樣做的。當然,Nginx也可以是多程序 + 事件驅動的模式,幾個程序跑libevent,不需要Apache那樣動輒數百的程序數。Nginx處理靜態檔案效果也很好,那是因為靜態檔案本身也是磁碟IO操作,處理過程一樣。至於說多少萬的併發連線,這個毫無意義。隨手寫個網路程式都能處理幾萬的併發,但如果大部分客戶端阻塞在那裡,就沒什麼價值。
再看看Apache或者Resin這類應用伺服器,之所以稱他們為應用伺服器,是因為他們真的要跑具體的業務應用,如科學計算、圖形影象、資料庫讀寫等。它們很可能是CPU密集型的服務,事件驅動並不合適。例如一個計算耗時2秒,那麼這2秒就是完全阻塞的,什麼event都沒用。想想MySQL如果改成事件驅動會怎麼樣,一個大型的join或sort就會阻塞住所有客戶端。這個時候多程序或執行緒就體現出優勢,每個程序各幹各的事,互不阻塞和干擾。當然,現代CPU越來越快,單個計算阻塞的時間可能很小,但只要有阻塞,事件程式設計就毫無優勢。所以程序、執行緒這類技術,並不會消失,而是與事件機制相輔相成,長期存在。
總言之,事件驅動適合於IO密集型服務,多程序或執行緒適合於CPU密集型服務,它們各有各的優勢,並不存在誰取代誰的傾向。