[轉]Nginx實現高併發的原理

Nginx

首先要明白,Nginx 採用的是多進程(單線程) & 多路IO複用模型。使用了 I/O 多路複用技術的 Nginx,就成了」併發事件驅動「的服務器。html

異步非阻塞(AIO)的詳解http://www.ibm.com/developerworks/cn/linux/l-async/linux

這裏寫圖片描述

多進程的工做模式

  1.  
    一、Nginx 在啓動後,會有一個 master 進程和多個相互獨立的 worker 進程。
  2.  
    二、接收來自外界的信號,向各worker進程發送信號,每一個進程都有可能來處理這個鏈接。
  3.  
    三、 master 進程能監控 worker 進程的運行狀態,當 worker 進程退出後(異常狀況下),會自動啓動新的 worker 進程。

注意 worker 進程數,通常會設置成機器 cpu 核數。由於更多的worker 數,只會致使進程相互競爭 cpu,從而帶來沒必要要的上下文切換。ios

使用多進程模式,不只能提升併發率,並且進程之間相互獨立,一個 worker 進程掛了不會影響到其餘 worker 進程。nginx

驚羣現象

主進程(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 進程中處理。

這樣作帶來的好處:

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

二、獨立進程,減小風險。採用獨立的進程,可讓互相之間不會影響,一個進程退出後,其它進程還在工做,服務不會中斷,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.  
    的事件,這些事件都會掛載在紅黑樹中。
  3.  
    二、調用 int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event) 在 epoll 對象中爲 fd 註冊事件,全部添加到epoll中的件
  4.  
    都會與設備驅動程序創建回調關係,也就是說,當相應的事件發生時會調用這個sockfd的回調方法,將sockfd添加到eventpoll 中的雙鏈表
  5.  
    三、調用 int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout) 來等待事件的發生,timeout 爲 -1 時,該
  6.  
    調用會阻塞知道有事件發生
  7.  
     

這樣,註冊好事件以後,只要有 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

  1  for( ; ; )
 2     {
 3         nfds = epoll_wait(epfd,events,20,500);
 4         for(i=0;i<nfds;++i)
 5         {
 6             if(events[i].data.fd==listenfd) //有新的鏈接
 7             {
 8                 connfd = accept(listenfd,(sockaddr *)&clientaddr, &clilen); //accept這個鏈接
 9                 ev.data.fd=connfd;
10                 ev.events=EPOLLIN|EPOLLET;
11                 epoll_ctl(epfd,EPOLL_CTL_ADD,connfd,&ev); //將新的fd添加到epoll的監聽隊列中
12             }
13             else if( events[i].events&EPOLLIN ) //接收到數據,讀socket
14             {
15                 n = read(sockfd, line, MAXLINE)) < 0    //讀
16                 ev.data.ptr = md;     //md爲自定義類型,添加數據
17                 ev.events=EPOLLOUT|EPOLLET;
18                 epoll_ctl(epfd,EPOLL_CTL_MOD,sockfd,&ev);//修改標識符,等待下一個循環時發送數據,異步處理的精髓
19             }
20             else if(events[i].events&EPOLLOUT) //有數據待發送,寫socket
21             {
22                 struct myepoll_data* md = (myepoll_data*)events[i].data.ptr;    //取數據
23                 sockfd = md->fd;
24                 send( sockfd, md->ptr, strlen((char*)md->ptr), 0 );        //發送數據
25                 ev.data.fd=sockfd;
26                 ev.events=EPOLLIN|EPOLLET;
27                 epoll_ctl(epfd,EPOLL_CTL_MOD,sockfd,&ev); //修改標識符,等待下一個循環時接收數據
28             }
29             else
30             {
31                 //其餘的處理
32             }
33         }
34     }

下面給出一個完整的服務器端例子:

 1 #include <iostream>
  2 #include <sys/socket.h>
  3 #include <sys/epoll.h>
  4 #include <netinet/in.h>
  5 #include <arpa/inet.h>
  6 #include <fcntl.h>
  7 #include <unistd.h>
  8 #include <stdio.h>
  9 #include <errno.h>
 10 
 11 using namespace std;
 12 
 13 #define MAXLINE 5
 14 #define OPEN_MAX 100
 15 #define LISTENQ 20
 16 #define SERV_PORT 5000
 17 #define INFTIM 1000
 18 
 19 void setnonblocking(int sock)
 20 {
 21     int opts;
 22     opts=fcntl(sock,F_GETFL);
 23     if(opts<0)
 24     {
 25         perror("fcntl(sock,GETFL)");
 26         exit(1);
 27     }
 28     opts = opts|O_NONBLOCK;
 29     if(fcntl(sock,F_SETFL,opts)<0)
 30     {
 31         perror("fcntl(sock,SETFL,opts)");
 32         exit(1);
 33     }
 34 }
 35 
 36 int main(int argc, char* argv[])
 37 {
 38     int i, maxi, listenfd, connfd, sockfd,epfd,nfds, portnumber;
 39     ssize_t n;
 40     char line[MAXLINE];
 41     socklen_t clilen;
 42 
 43 
 44     if ( 2 == argc )
 45     {
 46         if( (portnumber = atoi(argv[1])) < 0 )
 47         {
 48             fprintf(stderr,"Usage:%s portnumber/a/n",argv[0]);
 49             return 1;
 50         }
 51     }
 52     else
 53     {
 54         fprintf(stderr,"Usage:%s portnumber/a/n",argv[0]);
 55         return 1;
 56     }
 57 
 58 
 59 
 60     //聲明epoll_event結構體的變量,ev用於註冊事件,數組用於回傳要處理的事件
 61 
 62     struct epoll_event ev,events[20];
 63     //生成用於處理accept的epoll專用的文件描述符
 64 
 65     epfd=epoll_create(256);
 66     struct sockaddr_in clientaddr;
 67     struct sockaddr_in serveraddr;
 68     listenfd = socket(AF_INET, SOCK_STREAM, 0);
 69     //把socket設置爲非阻塞方式
 70 
 71     //setnonblocking(listenfd);
 72 
 73     //設置與要處理的事件相關的文件描述符
 74 
 75     ev.data.fd=listenfd;
 76     //設置要處理的事件類型
 77 
 78     ev.events=EPOLLIN|EPOLLET;
 79     //ev.events=EPOLLIN;
 80 
 81     //註冊epoll事件
 82 
 83     epoll_ctl(epfd,EPOLL_CTL_ADD,listenfd,&ev);
 84     bzero(&serveraddr, sizeof(serveraddr));
 85     serveraddr.sin_family = AF_INET;
 86     char *local_addr="127.0.0.1";
 87     inet_aton(local_addr,&(serveraddr.sin_addr));//htons(portnumber);
 88 
 89     serveraddr.sin_port=htons(portnumber);
 90     bind(listenfd,(sockaddr *)&serveraddr, sizeof(serveraddr));
 91     listen(listenfd, LISTENQ);
 92     maxi = 0;
 93     for ( ; ; ) {
 94         //等待epoll事件的發生
 95 
 96         nfds=epoll_wait(epfd,events,20,500);
 97         //處理所發生的全部事件
 98 
 99         for(i=0;i<nfds;++i)
100         {
101             if(events[i].data.fd==listenfd)//若是新監測到一個SOCKET用戶鏈接到了綁定的SOCKET端口,創建新的鏈接。
102 
103             {
104                 connfd = accept(listenfd,(sockaddr *)&clientaddr, &clilen);
105                 if(connfd<0){
106                     perror("connfd<0");
107                     exit(1);
108                 }
109                 //setnonblocking(connfd);
110 
111                 char *str = inet_ntoa(clientaddr.sin_addr);
112                 cout << "accapt a connection from " << str << endl;
113                 //設置用於讀操做的文件描述符
114 
115                 ev.data.fd=connfd;
116                 //設置用於注測的讀操做事件
117 
118                 ev.events=EPOLLIN|EPOLLET;
119                 //ev.events=EPOLLIN;
120 
121                 //註冊ev
122 
123                 epoll_ctl(epfd,EPOLL_CTL_ADD,connfd,&ev);
124             }
125             else if(events[i].events&EPOLLIN)//若是是已經鏈接的用戶,而且收到數據,那麼進行讀入。
126 
127             {
128                 cout << "EPOLLIN" << endl;
129                 if ( (sockfd = events[i].data.fd) < 0)
130                     continue;
131                 if ( (n = read(sockfd, line, MAXLINE)) < 0) {
132                     if (errno == ECONNRESET) {
133                         close(sockfd);
134                         events[i].data.fd = -1;
135                     } else
136                         std::cout<<"readline error"<<std::endl;
137                 } else if (n == 0) {
138                     close(sockfd);
139                     events[i].data.fd = -1;
140                 }
141                 line[n] = '/0';
142                 cout << "read " << line << endl;
143                 //設置用於寫操做的文件描述符
144 
145                 ev.data.fd=sockfd;
146                 //設置用於注測的寫操做事件
147 
148                 ev.events=EPOLLOUT|EPOLLET;
149                 //修改sockfd上要處理的事件爲EPOLLOUT
150 
151                 //epoll_ctl(epfd,EPOLL_CTL_MOD,sockfd,&ev);
152 
153             }
154             else if(events[i].events&EPOLLOUT) // 若是有數據發送
155 
156             {
157                 sockfd = events[i].data.fd;
158                 write(sockfd, line, n);
159                 //設置用於讀操做的文件描述符
160 
161                 ev.data.fd=sockfd;
162                 //設置用於注測的讀操做事件
163 
164                 ev.events=EPOLLIN|EPOLLET;
165                 //修改sockfd上要處理的事件爲EPOLIN
166 
167                 epoll_ctl(epfd,EPOLL_CTL_MOD,sockfd,&ev);
168             }
169         }
170     }
171     return 0;
172 }

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

對於Apache,每一個請求都會獨佔一個工做線程,當併發數到達幾千時,就同時有幾千的線程在處理請求了。這對於操做系統來講,佔用的內存很是大,線程的上下文切換帶來的cpu開銷也很大,性能就難以上去,同時這些開銷是徹底沒有意義的。
     對於Nginx來說,一個進程只有一個主線程,經過異步非阻塞的事件處理機制,實現了循環處理多個準備好的事件,從而實現輕量級和高併發。

事件驅動適合於I/O密集型服務,多進程或線程適合於CPU密集型服務: 
一、Nginx 更主要是做爲反向代理,而非Web服務器使用。其模式是事件驅動。 
二、事件驅動服務器,最適合作的就是這種 I/O 密集型工做,如反向代理,它在客戶端與WEB服務器之間起一個數據中轉做用,純粹是 I/O 操做,自身並不涉及到複雜計算。由於進程在一個地方進行計算時,那麼這個進程就不能處理其餘事件了。 
三、Nginx 只須要少許進程配合事件驅動,幾個進程跑 libevent,不像 Apache 多進程模型那樣動輒數百的進程數。 
五、Nginx 處理靜態文件效果也很好,那是由於讀寫文件和網絡通訊其實都是 I/O操做,處理過程同樣。

 

參考文章:http://www.javashuo.com/article/p-eafyrgmp-mh.html

相關文章
相關標籤/搜索