Nginx現在是非常火爆的web伺服器,她使用更少的資源,支援更多的并發連接配接數,她實作了linux的epoll模型,能夠支援高達 50,000 個并發連接配接數的響應。Nginx采用的是多程序單線程和多路IO複用模型。使用了I/O多路複用技術的Nginx,就成了”并發事件驅動“的伺服器。這裡再強調下重點,
- 多程序單線程
- 多路IO複用模型

一、多程序單線程
Nginx 自己實作了對epoll的封裝,是多程序單線程的典型代表。使用多程序模式,不僅能提高并發率,而且程序之間是互相獨立的,一 個worker程序挂了不會影響到其他worker程序。
master程序管理worker程序:
- 接收來自外界的信号。
- 向各worker程序發送信号。
- 監控woker程序的運作狀态。
- 當woker程序退出後(異常情況下),會自動重新啟動新的woker程序。
注意worker程序數,一般會設定成機器cpu核數。因為更多的worker隻會導緻程序之間互相競争cpu,進而帶來不必要的上下文切換。
二、IO多路複用模型epoll
多路複用,允許我們隻在事件發生時才将控制傳回給程式,而其他時候核心都挂起程序,随時待命。
epoll通過在Linux核心中申請一個簡易的檔案系統(檔案系統一般用B+樹資料結構來實作),其工作流程分為三部分:
- 調用 int epoll_create(int size)建立一個epoll對象,核心會建立一個eventpoll結構體,用于存放通過epoll_ctl()向epoll對象中添加進來的事件,這些事件都會挂載在紅黑樹中。
- 調用 int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event) 在 epoll 對象中為 fd 注冊事件,所有添加到epoll中的事件都會與裝置驅動程式建立回調關系,也就是說,當相應的事件發生時會調用這個sockfd的回調方法,将sockfd添加到eventpoll 中的雙連結清單。
- 調用 int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout) 來等待事件的發生,timeout 為 -1 時,該調用會阻塞知道有事件發生。
注冊好事件之後,隻要有fd上事件發生,epoll_wait()就能檢測到并傳回給使用者,使用者執行阻塞函數時就不會發生阻塞了。
epoll()在中核心維護一個連結清單,epoll_wait直接檢查連結清單是不是空就知道是否有檔案描述符準備好了。順便提一提,epoll與select、poll相比最大的優點是不會随着sockfd數目增長而降低效率,使用select()時,核心采用輪訓的方法來檢視是否有fd準備好,其中的儲存sockfd的是類似數組的資料結構fd_set,key 為 fd,value為0或者1(發生時間)。
能達到這種效果,是因為在核心實作中epoll是根據每 sockfd 上面的與裝置驅動程式建立起來的回調函數實作的。那麼,某個sockfd上的事件發生時,與它對應的回調函數就會被調用,将這個sockfd加傳入連結表,其他處于“空閑的”狀态的則不會。在這點上,epoll 實作了一個"僞"AIO。
可以看出,因為一個程序裡隻有一個線程,是以一個程序同時隻能做一件事,但是可以通過不斷地切換來“同時”處理多個請求。
例子:Nginx 會注冊一個事件:“如果來自一個新用戶端的連接配接請求到來了,再通知我”,此後隻有連接配接請求到來,伺服器才會執行 accept() 來接收請求。又比如向上遊伺服器(比如 PHP-FPM)轉發請求,并等待請求傳回時,這個處理的 worker 不會在這阻塞,它會在發送完請求後,注冊一個事件:“如果緩沖區接收到資料了,告訴我一聲,我再将它讀進來”,于是程序就空閑下來等待事件發生。
這樣,基于 多程序+epoll, Nginx 便能實作高并發。
三、worker程序工作流程
當一個 worker 程序在 accept() 這個連接配接之後,就開始讀取請求,解析請求,處理請求,産生資料後,再傳回給用戶端,最後才斷開連接配接,一個完整的請求。一個請求,完全由worker程序來處理,而且隻會在一個worker程序中處理。優點:
- 節省鎖帶來的開銷。每個worker程序都彼此獨立地工作,不共享任何資源,是以不需要鎖。同時在程式設計以及問題排查上時,也會友善很多。
- 獨立程序,減少風險。采用獨立的程序,可以讓互相之間不會影響,一個程序退出後,其它程序還在工作,服務不會中斷,master程序則很快重新啟動新的worker程序。當然,worker程序自己也能發生意外退出。
四、對驚群效應的處理
Nginx提供了一個accept_mutex這個東西,這是一個加在accept上的一把互斥鎖。即每個worker程序在執行accept()之前都需要先擷取鎖,accept()成功之後再解鎖。有了這把鎖,同一時刻,隻會有一個程序執行accpet(),這樣就不會有驚群問題了。accept_mutex是一個可控選項,我們可以顯示地關掉,預設是打開的。