Nginx
首先要明白,Nginx 采用的是多進程(單線程) & 多路IO復用模型。使用了 I/O 多路復用技術的 Nginx,就成了”並發事件驅動“的服務器。
異步非阻塞(AIO)的詳解http://www.ibm.com/developerworks/cn/linux/l-async/
多進程的工作模式
-
1、Nginx 在啟動后,會有一個 master 進程和多個相互獨立的 worker 進程。
-
2、接收來自外界的信號,向各worker進程發送信號,每個進程都有可能來處理這個連接。
-
3、 master 進程能監控 worker 進程的運行狀態,當 worker 進程退出后(異常情況下),會自動啟動新的 worker 進程。
注意 worker 進程數,一般會設置成機器 cpu 核數。因為更多的worker 數,只會導致進程相互競爭 cpu,從而帶來不必要的上下文切換。
使用多進程模式,不僅能提高並發率,而且進程之間相互獨立,一個 worker 進程掛了不會影響到其他 worker 進程。
驚群現象
主進程(master 進程)首先通過 socket() 來創建一個 sock 文件描述符用來監聽,然后fork生成子進程(workers 進程),子進程將繼承父進程的 sockfd(socket 文件描述符),之后子進程 accept() 后將創建已連接描述符(connected descriptor)),然后通過已連接描述符來與客戶端通信。
那么,由於所有子進程都繼承了父進程的 sockfd,那么當連接進來時,所有子進程都將收到通知並“爭着”與它建立連接,這就叫“驚群現象”。大量的進程被激活又掛起,只有一個進程可以accept() 到這個連接,這當然會消耗系統資源。
Nginx對驚群現象的處理
Nginx 提供了一個 accept_mutex 這個東西,這是一個加在accept上的一把共享鎖。即每個 worker 進程在執行 accept 之前都需要先獲取鎖,獲取不到就放棄執行 accept()。有了這把鎖之后,同一時刻,就只會有一個進程去 accpet(),這樣就不會有驚群問題了。accept_mutex 是一個可控選項,我們可以顯示地關掉,默認是打開的。
Nginx進程詳解
Nginx在啟動后,會有一個master進程和多個worker進程。
master進程
主要用來管理worker進程,包含:接收來自外界的信號,向各worker進程發送信號,監控worker進程的運行狀態,當worker進程退出后(異常情況下),會自動重新啟動新的worker進程。
master進程充當整個進程組與用戶的交互接口,同時對進程進行監護。它不需要處理網絡事件,不負責業務的執行,只會通過管理worker進程來實現重啟服務、平滑升級、更換日志文件、配置文件實時生效等功能。
我們要控制nginx,只需要通過kill向master進程發送信號就行了。比如kill -HUP pid,則是告訴nginx,從容地重啟nginx,我們一般用這個信號來重啟nginx,或重新加載配置,因為是從容地重啟,因此服務是不中斷的。master進程在接收到HUP信號后是怎么做的呢?首先master進程在接到信號后,會先重新加載配置文件,然后再啟動新的worker進程,並向所有老的worker進程發送信號,告訴他們可以光榮退休了。新的worker在啟動后,就開始接收新的請求,而老的worker在收到來自master的信號后,就不再接收新的請求,並且在當前進程中的所有未處理完的請求處理完成后,再退出。當然,直接給master進程發送信號,這是比較老的操作方式,nginx在0.8版本之后,引入了一系列命令行參數,來方便我們管理。比如,./nginx -s reload,就是來重啟nginx,./nginx -s stop,就是來停止nginx的運行。如何做到的呢?我們還是拿reload來說,我們看到,執行命令時,我們是啟動一個新的nginx進程,而新的nginx進程在解析到reload參數后,就知道我們的目的是控制nginx來重新加載配置文件了,它會向master進程發送信號,然后接下來的動作,就和我們直接向master進程發送信號一樣了。
worker進程
而基本的網絡事件,則是放在worker進程中來處理了。多個worker進程之間是對等的,他們同等競爭來自客戶端的請求,各進程互相之間是獨立的。一個請求,只可能在一個worker進程中處理,一個worker進程,不可能處理其它進程的請求。worker進程的個數是可以設置的,一般我們會設置與機器cpu核數一致,這里面的原因與nginx的進程模型以及事件處理模型是分不開的。
worker進程之間是平等的,每個進程,處理請求的機會也是一樣的。當我們提供80端口的http服務時,一個連接請求過來,每個進程都有可能處理這個連接,怎么做到的呢?首先,每個worker進程都是從master進程fork過來,在master進程里面,先建立好需要listen的socket(listenfd)之后,然后再fork出多個worker進程。所有worker進程的listenfd會在新連接到來時變得可讀,為保證只有一個進程處理該連接,所有worker進程在注冊listenfd讀事件前搶accept_mutex,搶到互斥鎖的那個進程注冊listenfd讀事件,在讀事件里調用accept接受該連接。當一個worker進程在accept這個連接之后,就開始讀取請求,解析請求,處理請求,產生數據后,再返回給客戶端,最后才斷開連接,這樣一個完整的請求就是這樣的了。我們可以看到,一個請求,完全由worker進程來處理,而且只在一個worker進程中處理。worker進程之間是平等的,每個進程,處理請求的機會也是一樣的。當我們提供80端口的http服務時,一個連接請求過來,每個進程都有可能處理這個連接,怎么做到的呢?首先,每個worker進程都是從master進程fork過來,在master進程里面,先建立好需要listen的socket(listenfd)之后,然后再fork出多個worker進程。所有worker進程的listenfd會在新連接到來時變得可讀,為保證只有一個進程處理該連接,所有worker進程在注冊listenfd讀事件前搶accept_mutex,搶到互斥鎖的那個進程注冊listenfd讀事件,在讀事件里調用accept接受該連接。當一個worker進程在accept這個連接之后,就開始讀取請求,解析請求,處理請求,產生數據后,再返回給客戶端,最后才斷開連接,這樣一個完整的請求就是這樣的了。我們可以看到,一個請求,完全由worker進程來處理,而且只在一個worker進程中處理。
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 時,該
-
調用會阻塞知道有事件發生
-
這樣,注冊好事件之后,只要有 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); // 最長阻塞 500s
-
for(i=0;i<nfds;++i)
-
{
-
if(events[i].data.fd==listenfd) //有新的連接
-
{
-
connfd = accept(listenfd,(sockaddr *)&clientaddr, &clilen); //accept這個連接
-
ev.data.fd=connfd;
-
ev.events=EPOLLIN|EPOLLET;
-
epoll_ctl(epfd,EPOLL_CTL_ADD,connfd,&ev); //將新的fd添加到epoll的監聽隊列中
-
}
-
else if( events[i].events&EPOLLIN ) //接收到數據,讀socket
-
{
-
n = read(sockfd, line, MAXLINE)) < 0 //讀
-
ev.data.ptr = md; //md為自定義類型,添加數據
-
ev.events=EPOLLOUT|EPOLLET;
-
epoll_ctl(epfd,EPOLL_CTL_MOD,sockfd,&ev); //修改標識符,等待下一個循環時發送數據,異步處理的精髓
-
}
-
else if(events[i].events&EPOLLOUT) //有數據待發送,寫socket
-
{
-
struct myepoll_data* md = (myepoll_data*)events[i].data.ptr; //取數據
-
sockfd = md->fd;
-
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 的比較:
對於Nginx來講,一個進程只有一個主線程,通過異步非阻塞的事件處理機制,實現了循環處理多個准備好的事件,從而實現輕量級和高並發。
事件驅動適合於I/O密集型服務,多進程或線程適合於CPU密集型服務:
1、Nginx 更主要是作為反向代理,而非Web服務器使用。其模式是事件驅動。
2、事件驅動服務器,最適合做的就是這種 I/O 密集型工作,如反向代理,它在客戶端與WEB服務器之間起一個數據中轉作用,純粹是 I/O 操作,自身並不涉及到復雜計算。因為進程在一個地方進行計算時,那么這個進程就不能處理其他事件了。
3、Nginx 只需要少量進程配合事件驅動,幾個進程跑 libevent,不像 Apache 多進程模型那樣動輒數百的進程數。
5、Nginx 處理靜態文件效果也很好,那是因為讀寫文件和網絡通信其實都是 I/O操作,處理過程一樣。