Nginx工作原理(Master+Worker)
Nginx
首先要明白,Nginx 采用的是多進(jìn)程(單線程) & 多路IO復(fù)用模型。使用了 I/O 多路復(fù)用技術(shù)的 Nginx,就成了”并發(fā)事件驅(qū)動(dòng)“的服務(wù)器。
多進(jìn)程的工作模式
1、Nginx 在啟動(dòng)后,會(huì)有一個(gè) master 進(jìn)程和多個(gè)相互獨(dú)立的 worker 進(jìn)程。 2、接收來自外界的信號,向各worker進(jìn)程發(fā)送信號,每個(gè)進(jìn)程都有可能來處理這個(gè)連接。 3、 master 進(jìn)程能監(jiān)控 worker 進(jìn)程的運(yùn)行狀態(tài),當(dāng) worker 進(jìn)程退出后(異常情況下),會(huì)自動(dòng)啟動(dòng)新的 worker 進(jìn)程。- 1
- 2
- 3
- 4
注意 worker 進(jìn)程數(shù),一般會(huì)設(shè)置成機(jī)器 cpu 核數(shù)。因?yàn)楦嗟膚orker 數(shù),只會(huì)導(dǎo)致進(jìn)程相互競爭 cpu,從而帶來不必要的上下文切換。
使用多進(jìn)程模式,不僅能提高并發(fā)率,而且進(jìn)程之間相互獨(dú)立,一個(gè) worker 進(jìn)程掛了不會(huì)影響到其他 worker 進(jìn)程。
驚群現(xiàn)象
主進(jìn)程(master 進(jìn)程)首先通過 socket() 來創(chuàng)建一個(gè) sock 文件描述符用來監(jiān)聽,然后fork生成子進(jìn)程(workers 進(jìn)程),子進(jìn)程將繼承父進(jìn)程的 sockfd(socket 文件描述符),之后子進(jìn)程 accept() 后將創(chuàng)建已連接描述符(connected descriptor)),然后通過已連接描述符來與客戶端通信。
那么,由于所有子進(jìn)程都繼承了父進(jìn)程的 sockfd,那么當(dāng)連接進(jìn)來時(shí),所有子進(jìn)程都將收到通知并“爭著”與它建立連接,這就叫“驚群現(xiàn)象”。大量的進(jìn)程被激活又掛起,只有一個(gè)進(jìn)程可以accept() 到這個(gè)連接,這當(dāng)然會(huì)消耗系統(tǒng)資源。
Nginx對驚群現(xiàn)象的處理:
Nginx 提供了一個(gè) accept_mutex 這個(gè)東西,這是一個(gè)加在accept上的一把互斥鎖。即每個(gè) worker 進(jìn)程在執(zhí)行 accept 之前都需要先獲取鎖,獲取不到就放棄執(zhí)行 accept()。有了這把鎖之后,同一時(shí)刻,就只會(huì)有一個(gè)進(jìn)程去 accpet(),這樣就不會(huì)有驚群問題了。accept_mutex 是一個(gè)可控選項(xiàng),我們可以顯示地關(guān)掉,默認(rèn)是打開的。
worker進(jìn)程工作流程
當(dāng)一個(gè) worker 進(jìn)程在 accept() 這個(gè)連接之后,就開始讀取請求,解析請求,處理請求,產(chǎn)生數(shù)據(jù)后,再返回給客戶端,最后才斷開連接,一個(gè)完整的請求。一個(gè)請求,完全由 worker 進(jìn)程來處理,而且只能在一個(gè) worker 進(jìn)程中處理。
這樣做帶來的好處:
1、節(jié)省鎖帶來的開銷。每個(gè) worker 進(jìn)程都是獨(dú)立的進(jìn)程,不共享資源,不需要加鎖。同時(shí)在編程以及問題查上時(shí),也會(huì)方便很多。
2、獨(dú)立進(jìn)程,減少風(fēng)險(xiǎn)。采用獨(dú)立的進(jìn)程,可以讓互相之間不會(huì)影響,一個(gè)進(jìn)程退出后,其它進(jìn)程還在工作,服務(wù)不會(huì)中斷,master 進(jìn)程則很快重新啟動(dòng)新的 worker 進(jìn)程。當(dāng)然,worker 進(jìn)程的也能發(fā)生意外退出。
多進(jìn)程模型每個(gè)進(jìn)程/線程只能處理一路IO,那么 Nginx是如何處理多路IO呢?
如果不使用 IO 多路復(fù)用,那么在一個(gè)進(jìn)程中,同時(shí)只能處理一個(gè)請求,比如執(zhí)行 accept(),如果沒有連接過來,那么程序會(huì)阻塞在這里,直到有一個(gè)連接過來,才能繼續(xù)向下執(zhí)行。
而多路復(fù)用,允許我們只在事件發(fā)生時(shí)才將控制返回給程序,而其他時(shí)候內(nèi)核都掛起進(jìn)程,隨時(shí)待命。
核心:Nginx采用的 IO多路復(fù)用模型epoll
epoll通過在Linux內(nèi)核中申請一個(gè)簡易的文件系統(tǒng)(文件系統(tǒng)一般用什么數(shù)據(jù)結(jié)構(gòu)實(shí)現(xiàn)?B+樹),其工作流程分為三部分:
1、調(diào)用 int epoll_create(int size)建立一個(gè)epoll對象,內(nèi)核會(huì)創(chuàng)建一個(gè)eventpoll結(jié)構(gòu)體,用于存放通過epoll_ctl()向epoll對象中添加進(jìn)來的事件,這些事件都會(huì)掛載在紅黑樹中。 2、調(diào)用 int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event) 在 epoll 對象中為 fd 注冊事件,所有添加到epoll中的事件都會(huì)與設(shè)備驅(qū)動(dòng)程序建立回調(diào)關(guān)系,也就是說,當(dāng)相應(yīng)的事件發(fā)生時(shí)會(huì)調(diào)用這個(gè)sockfd的回調(diào)方法,將sockfd添加到eventpoll 中的雙鏈表。 3、調(diào)用 int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout) 來等待事件的發(fā)生,timeout 為 -1 時(shí),該調(diào)用會(huì)阻塞知道有事件發(fā)生- 1
- 2
- 3
- 4
這樣,注冊好事件之后,只要有 fd 上事件發(fā)生,epoll_wait() 就能檢測到并返回給用戶,用戶就能”非阻塞“地進(jìn)行 I/O 了。
epoll() 中內(nèi)核則維護(hù)一個(gè)鏈表,epoll_wait 直接檢查鏈表是不是空就知道是否有文件描述符準(zhǔn)備好了。(epoll 與 select 相比最大的優(yōu)點(diǎn)是不會(huì)隨著 sockfd 數(shù)目增長而降低效率,使用 select() 時(shí),內(nèi)核采用輪訓(xùn)的方法來查看是否有fd 準(zhǔn)備好,其中的保存 sockfd 的是類似數(shù)組的數(shù)據(jù)結(jié)構(gòu) fd_set,key 為 fd,value 為 0 或者 1。)
能達(dá)到這種效果,是因?yàn)樵趦?nèi)核實(shí)現(xiàn)中 epoll 是根據(jù)每個(gè) sockfd 上面的與設(shè)備驅(qū)動(dòng)程序建立起來的回調(diào)函數(shù)實(shí)現(xiàn)的。那么,某個(gè) sockfd 上的事件發(fā)生時(shí),與它對應(yīng)的回調(diào)函數(shù)就會(huì)被調(diào)用,來把這個(gè) sockfd 加入鏈表,其他處于“空閑的”狀態(tài)的則不會(huì)。在這點(diǎn)上,epoll 實(shí)現(xiàn)了一個(gè)”偽”AIO。但是如果絕大部分的 I/O 都是“活躍的”,每個(gè) socket 使用率很高的話,epoll效率不一定比 select 高(可能是要維護(hù)隊(duì)列復(fù)雜)。
可以看出,因?yàn)橐粋€(gè)進(jìn)程里只有一個(gè)線程,所以一個(gè)進(jìn)程同時(shí)只能做一件事,但是可以通過不斷地切換來“同時(shí)”處理多個(gè)請求。
例子:Nginx 會(huì)注冊一個(gè)事件:“如果來自一個(gè)新客戶端的連接請求到來了,再通知我”,此后只有連接請求到來,服務(wù)器才會(huì)執(zhí)行 accept() 來接收請求。又比如向上游服務(wù)器(比如 PHP-FPM)轉(zhuǎn)發(fā)請求,并等待請求返回時(shí),這個(gè)處理的 worker 不會(huì)在這阻塞,它會(huì)在發(fā)送完請求后,注冊一個(gè)事件:“如果緩沖區(qū)接收到數(shù)據(jù)了,告訴我一聲,我再將它讀進(jìn)來”,于是進(jìn)程就空閑下來等待事件發(fā)生。
這樣,基于?多進(jìn)程+epoll, Nginx 便能實(shí)現(xiàn)高并發(fā)。
使用 epoll 處理事件的一個(gè)框架,代碼轉(zhuǎn)自:http://www.cnblogs.com/fnlingnzb-learner/p/5835573.html
for( ; ; ) // 無限循環(huán){nfds = epoll_wait(epfd,events,20,500); // 最長阻塞 500sfor(i=0;i<nfds;++i){if(events[i].data.fd==listenfd) //有新的連接{connfd = accept(listenfd,(sockaddr *)&clientaddr, &clilen); //accept這個(gè)連接ev.data.fd=connfd;ev.events=EPOLLIN|EPOLLET;epoll_ctl(epfd,EPOLL_CTL_ADD,connfd,&ev); //將新的fd添加到epoll的監(jiān)聽隊(duì)列中}else if( events[i].events&EPOLLIN ) //接收到數(shù)據(jù),讀socket{n = read(sockfd, line, MAXLINE)) < 0 //讀ev.data.ptr = md; //md為自定義類型,添加數(shù)據(jù)ev.events=EPOLLOUT|EPOLLET;epoll_ctl(epfd,EPOLL_CTL_MOD,sockfd,&ev);//修改標(biāo)識(shí)符,等待下一個(gè)循環(huán)時(shí)發(fā)送數(shù)據(jù),異步處理的精髓}else if(events[i].events&EPOLLOUT) //有數(shù)據(jù)待發(fā)送,寫socket{struct myepoll_data* md = (myepoll_data*)events[i].data.ptr; //取數(shù)據(jù)sockfd = md->fd;send( sockfd, md->ptr, strlen((char*)md->ptr), 0 ); //發(fā)送數(shù)據(jù)ev.data.fd=sockfd;ev.events=EPOLLIN|EPOLLET;epoll_ctl(epfd,EPOLL_CTL_MOD,sockfd,&ev); //修改標(biāo)識(shí)符,等待下一個(gè)循環(huán)時(shí)接收數(shù)據(jù)}else{//其他的處理}}}- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
Nginx 與 多進(jìn)程模式 Apache 的比較:
事件驅(qū)動(dòng)適合于I/O密集型服務(wù),多進(jìn)程或線程適合于CPU密集型服務(wù):?
1、Nginx 更主要是作為反向代理,而非Web服務(wù)器使用。其模式是事件驅(qū)動(dòng)。?
2、事件驅(qū)動(dòng)服務(wù)器,最適合做的就是這種 I/O 密集型工作,如反向代理,它在客戶端與WEB服務(wù)器之間起一個(gè)數(shù)據(jù)中轉(zhuǎn)作用,純粹是 I/O 操作,自身并不涉及到復(fù)雜計(jì)算。因?yàn)檫M(jìn)程在一個(gè)地方進(jìn)行計(jì)算時(shí),那么這個(gè)進(jìn)程就不能處理其他事件了。?
3、Nginx 只需要少量進(jìn)程配合事件驅(qū)動(dòng),幾個(gè)進(jìn)程跑 libevent,不像 Apache 多進(jìn)程模型那樣動(dòng)輒數(shù)百的進(jìn)程數(shù)。?
5、Nginx 處理靜態(tài)文件效果也很好,那是因?yàn)樽x寫文件和網(wǎng)絡(luò)通信其實(shí)都是 I/O操作,處理過程一樣。
總結(jié)
以上是生活随笔為你收集整理的Nginx工作原理(Master+Worker)的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 利用线性代数的方法求斐波那契数列的通项
- 下一篇: Nginx工作原理和优化