分布式集羣服務導引:淺析N-ginx的集羣服務部署及高並發實現原理

那麼,由於所有子進程都繼承了父進程的 sockfd,那麼當連接進來時,所有子進程都將收到通知並“爭著”與它建立連接,這就叫“驚群現象”。大量的進程被激活又掛起,只有一個進程可以accept() 到這個連接,這當然會消耗系統資源。

Nginx對驚群現象的處理:

Nginx 提供了一個 accept_mutex 這個東西,這是一個加在accept上的一把互斥鎖。即每個 worker 進程在執行 accept 之前都需要先獲取鎖,獲取不到就放棄執行 accept()。有了這把鎖之後,同一時刻,就只會有一個進程去 accpet(),這樣就不會有驚群問題了。accept_mutex 是一個可控選項,我們可以顯示地關掉,默認是打開的。

worker進程工作流程

當一個 worker 進程在 accept() 這個連接之後,就開始讀取請求,解析請求,處理請求,產生數據後,再返回給客戶端,最後才斷開連接,一個完整的請求。一個請求,完全由 worker 進程來處理,而且只能在一個 worker 進程中處理。

這樣做帶來的好處:

1、節省鎖帶來的開銷。每個 worker 進程都是獨立的進程,不共享資源,不需要加鎖。同時在編程以及問題查上時,也會方便很多。

2、獨立進程,減少風險。採用獨立的進程,可以讓互相之間不會影響,一個進程退出後,其它進程還在工作,服務不會中斷,master 進程則很快重新啟動新的 worker 進程。當然,worker 進程的也能發生意外退出。

多進程模型每個進程/線程只能處理一路IO,那麼 Nginx是如何處理多路IO呢?

如果不使用 IO 多路複用,那麼在一個進程中,同時只能處理一個請求,比如執行 accept(),如果沒有連接過來,那麼程序會阻塞在這裡,直到有一個連接過來,才能繼續向下執行。

而多路複用,允許我們只在事件發生時才將控制返回給程序,而其他時候內核都掛起進程,隨時待命。

核心:Nginx採用的 IO多路複用模型epoll

epoll通過在Linux內核中申請一個簡易的文件系統(文件系統一般用什麼數據結構實現?B+樹),其工作流程分為三部分:

1、調用 int epoll_create(int size)建立一個epoll對象,內核會創建一個eventpoll結構體,用於存放通過epoll_ctl()向epoll對象中添加進來的事件,這些事件都會掛載在紅黑樹中。2、調用 int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event) 在 epoll 對象中為 fd 註冊事件,所有添加到epoll中的事件都會與設備驅動程序建立回調關係,也就是說,當相應的事件發生時會調用這個sockfd的回調方法,將sockfd添加到eventpoll 中的雙鏈表。3、調用 int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout) 來等待事件的發生,timeout 為 -1 時,該調用會阻塞知道有事件發生1234

這樣,註冊好事件之後,只要有 fd 上事件發生,epoll_wait() 就能檢測到並返回給用戶,用戶就能”非阻塞“地進行 I/O 了。

epoll() 中內核則維護一個鏈表,epoll_wait 直接檢查鏈表是不是空就知道是否有文件描述符準備好了。(epoll 與 select 相比最大的優點是不會隨著 sockfd 數目增長而降低效率,使用 select() 時,內核採用輪訓的方法來查看是否有fd 準備好,其中的保存 sockfd 的是類似數組的數據結構 fd_set,key 為 fd,value 為 0 或者 1。)

能達到這種效果,是因為在內核實現中 epoll 是根據每個 sockfd 上面的與設備驅動程序建立起來的回調函數實現的。那麼,某個 sockfd 上的事件發生時,與它對應的回調函數就會被調用,來把這個 sockfd 加入鏈表,其他處於“空閒的”狀態的則不會。在這點上,epoll 實現了一個”偽”AIO。但是如果絕大部分的 I/O 都是“活躍的”,每個 socket 使用率很高的話,epoll效率不一定比 select 高(可能是要維護隊列複雜)。

可以看出,因為一個進程裡只有一個線程,所以一個進程同時只能做一件事,但是可以通過不斷地切換來“同時”處理多個請求。

例子:Nginx 會註冊一個事件:“如果來自一個新客戶端的連接請求到來了,再通知我”,此後只有連接請求到來,服務器才會執行 accept() 來接收請求。又比如向上遊服務器(比如 PHP-FPM)轉發請求,並等待請求返回時,這個處理的 worker 不會在這阻塞,它會在發送完請求後,註冊一個事件:“如果緩衝區接收到數據了,告訴我一聲,我再將它讀進來”,於是進程就空閒下來等待事件發生。

這樣,基於 多進程+epoll, Nginx 便能實現高併發。

使用 epoll 處理事件的一個框架,代碼轉自:http://www.cnblogs.com/fnlingnzb-learner/p/5835573.html

for( ; ; ) // 無限循環{nfds = epoll_wait(epfd,events,20,500); // 最長阻塞 500sfor(i=0;ifd; send( sockfd, md->ptr, strlen((char*)md->ptr), 0 ); //發送數據ev.data.fd=sockfd;ev.events=EPOLLIN|EPOLLET;epoll_ctl(epfd,EPOLL_CTL_MOD,sockfd,&ev); //修改標識符,等待下一個循環時接收數據} else{ //其他的處理}}}

Nginx 與 多進程模式 Apache 的比較:

事件驅動適合於I/O密集型服務,多進程或線程適合於CPU密集型服務:

1、Nginx 更主要是作為反向代理,而非Web服務器使用。其模式是事件驅動。

2、事件驅動服務器,最適合做的就是這種 I/O 密集型工作,如反向代理,它在客戶端與WEB服務器之間起一個數據中轉作用,純粹是 I/O 操作,自身並不涉及到複雜計算。因為進程在一個地方進行計算時,那麼這個進程就不能處理其他事件了。

3、Nginx 只需要少量進程配合事件驅動,幾個進程跑 libevent,不像 Apache 多進程模型那樣動輒數百的進程數。

5、Nginx 處理靜態文件效果也很好,那是因為讀寫文件和網絡通信其實都是 I/O操作,處理過程一樣。

參考 http://codinglife.sinaapp.com/?p=40


分佈式集群服務導引:淺析N-ginx的集群服務部署及高併發實現原理


分享到:


相關文章: