1. Nginx
首先要明白,Nginx 采用的是多進程(單線程) & 多路IO復用模型。使用了 I/O 多路復用技術的 Nginx,就成了”並發事件驅動“的服務器。
2. 多進程的工作模式
- 1、Nginx 在啟動后,會有一個 master 進程和多個相互獨立的 worker 進程。
- 2、接收來自外界的信號,向各worker進程發送信號,每個進程都有可能來處理這個連接。
- 3、 master 進程能監控 worker 進程的運行狀態,當 worker 進程退出后(異常情況下),會自動啟動新的 worker 進程。
3.注意:
worker 進程數,一般會設置成機器 cpu 核數。因為更多的worker 數,只會導致進程相互競爭 cpu,從而帶來不必要的上下文切換。
使用多進程模式,不僅能提高並發率,而且進程之間相互獨立,一個 worker 進程掛了不會影響到其他 worker 進程。
4.驚群現象
- 主進程(master 進程)首先通過 socket() 來創建一個 sock 文件描述符用來監聽,然后fork生成子進程(workers 進程),子進程將繼承父進程的 sockfd(socket 文件描述符),之后子進程 accept() 后將創建已連接描述符(connected descriptor)),然后通過已連接描述符來與客戶端通信。
- 那么,由於所有子進程都繼承了父進程的 sockfd,那么當連接進來時,所有子進程都將收到通知並“爭着”與它建立連接,這就叫“驚群現象”。大量的進程被激活又掛起,只有一個進程可以accept() 到這個連接,這當然會消耗系統資源。
5.Nginx對驚群現象的處理:
Nginx 提供了一個 accept_mutex 這個東西,這是一個加在accept上的一把互斥鎖。即每個 worker 進程在執行 accept 之前都需要先獲取鎖,獲取不到就放棄執行 accept()。有了這把鎖之后,同一時刻,就只會有一個進程去 accpet(),這樣就不會有驚群問題了。accept_mutex 是一個可控選項,我們可以顯示地關掉,默認是打開的。
6.worker進程工作流程
當一個 worker 進程在 accept() 這個連接之后,就開始讀取請求,解析請求,處理請求,產生數據后,再返回給客戶端,最后才斷開連接,一個完整的請求。一個請求,完全由 worker 進程來處理,而且只能在一個 worker 進程中處理。
這樣做帶來的好處:
- 1、節省鎖帶來的開銷。每個 worker 進程都是獨立的進程,不共享資源,不需要加鎖。同時在編程以及問題查上時,也會方便很多。
- 2、獨立進程,減少風險。采用獨立的進程,可以讓互相之間不會影響,一個進程退出后,其它進程還在工作,服務不會中斷,master 進程則很快重新啟動新的 worker 進程。當然,worker 進程的也能發生意外退出。
7.多進程模型每個進程/線程只能處理一路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 高(可能是要維護隊列復雜)。
可以看出,因為一個進程里只有一個線程,所以一個進程同時只能做一件事,但是可以通過不斷地切換來“同時”處理多個請求。
8.例子:
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 { //其他的處理 } } }
9.Nginx 與 多進程模式 Apache 的比較:
事件驅動適合於I/O密集型服務,多進程或線程適合於CPU密集型服務:
- 1、Nginx 更主要是作為反向代理,而非Web服務器使用。其模式是事件驅動。
- 2、事件驅動服務器,最適合做的就是這種 I/O 密集型工作,如反向代理,它在客戶端與WEB服務器之間起一個數據中轉作用,純粹是 I/O 操作,自身並不涉及到復雜計算。因為進程在一個地方進行計算時,那么這個進程就不能處理其他事件了。
- 3、Nginx 只需要少量進程配合事件驅動,幾個進程跑 libevent,不像 Apache 多進程模型那樣動輒數百的進程數。
- 4、Nginx 處理靜態文件效果也很好,那是因為讀寫文件和網絡通信其實都是 I/O操作,處理過程一樣。
原文鏈接:https://blog.csdn.net/kim_weir/article/details/80036462