Nginx 多进程连接请求/事件分发流程分析
Nginx使用多進(jìn)程的方法進(jìn)行任務(wù)處理,每個(gè)worker進(jìn)程只有一個(gè)線程,單線程循環(huán)處理全部監(jiān)聽的事件。本文重點(diǎn)分析一下多進(jìn)程間的負(fù)載均衡問題以及Nginx多進(jìn)程事件處理流程,方便大家自己寫程序的時(shí)候借鑒。
?
一、監(jiān)聽建立流程
整個(gè)建立監(jiān)聽socket到accept的過程如下圖:
?
說明:
1.main里面調(diào)用ngx_init_cycle(src/core/ngx_cycle.c),ngx_init_cycle里面完成很多基本的配置,如文件,共享內(nèi)存,socket等。
2.上圖左上角是ngx_init_cycle里面調(diào)用的ngx_open_listening_sockets(src/core/ngx_connection.c)主要完成的工作,包括基本的創(chuàng)建socket,setsockopt,bind和listen等。
3.然后是正常的子進(jìn)程生成過程。在每個(gè)子worker進(jìn)程的ngx_worker_process_cycle中,在調(diào)用ngx_worker_process_init里面調(diào)用各模塊的初始化操作init_process。一epoll module為例,這里調(diào)用ngx_event_process_init,里面初始化多個(gè)NGX_EVENT_MODULE類型的module.NGX_EVENT_MODULE類型的只有ngx_event_core_module和ngx_epoll_module。前一個(gè)module的actions部分為空。ngx_epoll_module里面的init函數(shù)就是ngx_epoll_init。ngx_epoll_init函數(shù)主要完成epoll部分相關(guān)的初始化,包括epoll_create,設(shè)置ngx_event_actions等。
4.初始化完ngx_epoll_module,繼續(xù)ngx_event_process_init,然后循環(huán)設(shè)置每個(gè)listening socket的read handler為ngx_event_accept.最后將每個(gè)listening socket的READ事件添加到epoll進(jìn)行等待。
5.ngx_event_process_init初始化完成后,每個(gè)worker process開始循環(huán)處理events&timers。最終調(diào)用的是epoll_wait。由于之前l(fā)istening socket以及加入到epoll,所以如果監(jiān)聽字有read消息,那么久調(diào)用rev->handler進(jìn)行處理,監(jiān)聽字的handler之前已經(jīng)設(shè)置為ngx_event_accept。ngx_event_accept主要是調(diào)用accept函數(shù)來接受新的客戶端套接字client socket。
下面是監(jiān)聽字的處理函數(shù)ngx_event_accept流程圖:
?
說明:
1.前半部分主要是通過accept接受新連接字,生成并設(shè)置相關(guān)結(jié)構(gòu),然后添加到epoll中。
2.后半部分調(diào)用connection中的listening對應(yīng)的handler,即ngx_xxx_init_connection,其中xxx可以是mail,http和stream。顧名思義,該函數(shù)主要是做新的accepted連接字的初始化工作。上圖以http module為例,初始化設(shè)置了連接字的read handler等。
?
二、負(fù)載均衡問題
?Nginx里面通過一個(gè)變量ngx_accept_disabled來實(shí)施進(jìn)程間獲取客戶端連接請求的負(fù)載均衡策略。ngx_accept_disabled使用流程圖:
?
說明:
1.ngx_process_events_and_timers函數(shù)中,通過ngx_accept_disabled的正負(fù)判斷當(dāng)前進(jìn)程負(fù)載高低(大于0,高負(fù)載;小于0,低負(fù)載)。如果低負(fù)載時(shí),不做處理,進(jìn)程去申請accept鎖,監(jiān)聽并接受新的連接。
2.如果是高負(fù)載時(shí),ngx_accept_disabled就發(fā)揮作用了。這時(shí),不去申請accept鎖,讓出監(jiān)聽和接受新連接的機(jī)會(huì)。同時(shí)ngx_accept_disabled減1,表示通過讓出一次accept申請的機(jī)會(huì),該進(jìn)程的負(fù)載將會(huì)稍微減輕,直到ngx_accept_disabled最后小于0,重新進(jìn)入低負(fù)載的狀態(tài),開始新的accept鎖競爭。
?
參考鏈接:http://www.jb51.net/article/52177.htm
?
三、“驚群”問題
“驚群”問題:多個(gè)進(jìn)程同時(shí)監(jiān)聽一個(gè)套接字,當(dāng)有新連接到來時(shí),會(huì)同時(shí)喚醒全部進(jìn)程,但只能有一個(gè)進(jìn)程與客戶端連接成功,造成資源的浪費(fèi)。
Nginx通過進(jìn)程間共享互斥鎖ngx_accept_mutex來控制多個(gè)worker進(jìn)程對公共監(jiān)聽套接字的互斥訪問,獲取鎖后調(diào)用accept取出與客戶端已經(jīng)建立的連接加入epoll,然后釋放互斥鎖。
Nginx處理流程示意圖:
說明:
1.ngx_accept_disabled作為單個(gè)進(jìn)程負(fù)載較高(最大允許連接數(shù)的7/8)的標(biāo)記,計(jì)算公式:
ngx_accept_disabled = ngx_cycle->connection_n/8?- ngx_cycle->free_connection_n;
即進(jìn)程可用連接數(shù)free_connection_n小于總連接數(shù)connection_n的1/8時(shí)ngx_accept_disabled大于0;否則小于0.或者說ngx_accept_disabled小于0時(shí),表示可用連接數(shù)較多,負(fù)載較低;ngx_accept_disabled大于0時(shí),說明可用連接數(shù)較少,負(fù)載較高。
2.如果進(jìn)程負(fù)載較低時(shí),即ngx_accept_disabled 小于0,進(jìn)程允許競爭accept鎖。
3.如果進(jìn)程負(fù)載較高時(shí),放棄競爭accept鎖,同時(shí)ngx_accept_disabled 減1,即認(rèn)為由于讓出一次競爭accept鎖的機(jī)會(huì),負(fù)載稍微減輕(ngx_accept_disabled 小于0可用)。由于負(fù)載較高時(shí)(ngx_accept_disabled >0)只是將ngx_accept_disabled 減1,這里不申請accept鎖,所以后續(xù)的accept函數(shù)會(huì)遭遇“驚群”問題,返回錯(cuò)誤errno=EAGAIN,直接返回(個(gè)人覺得這里有改進(jìn)的空間,見補(bǔ)充部分)。
ngx_process_events_and_timers函數(shù)部分代碼如下:
1 if (ngx_use_accept_mutex) { 2 if (ngx_accept_disabled > 0) { 3 ngx_accept_disabled--; 4 5 } else { 6 if (ngx_trylock_accept_mutex(cycle) == NGX_ERROR) { 7 return; 8 } 9 10 if (ngx_accept_mutex_held) { 11 flags |= NGX_POST_EVENTS; 12 13 } else { 14 if (timer == NGX_TIMER_INFINITE 15 || timer > ngx_accept_mutex_delay) 16 { 17 timer = ngx_accept_mutex_delay; 18 } 19 } 20 } 21 }4.如果競爭加鎖失敗(6-7行),直接返回,返回到ngx_worker_process_cycle的for循環(huán)里面,此次不參與事件處理,進(jìn)行下一次循環(huán)。
5.如果競爭加鎖成功,設(shè)置NGX_POST_EVENTS標(biāo)記,表示將事件先放入隊(duì)列中,稍后處理,優(yōu)先釋放ngx_accept_mutex,防止單個(gè)進(jìn)程過多占用鎖時(shí)間,影響事件處理效率。ngx_epoll_process_events函數(shù)有如下部分(寫事件wev部分也一樣):
1 if (flags & NGX_POST_EVENTS) { 2 queue = rev->accept ? &ngx_posted_accept_events 3 : &ngx_posted_events; 4 5 ngx_post_event(rev, queue);//先將event放入隊(duì)列,稍后處理 6 7 } else { 8 rev->handler(rev); 9 }6.從ngx_epoll_process_events返回ngx_process_events_and_timers,然后是處理accept事件(下面代碼10行);處理完accept事件,馬上釋放鎖(下面代碼13-15行),給其他進(jìn)程機(jī)會(huì)去監(jiān)聽連接事件。最后處理一般的連接事件。
1 delta = ngx_current_msec; 2 3 (void) ngx_process_events(cycle, timer, flags); 4 5 delta = ngx_current_msec - delta; 6 7 ngx_log_debug1(NGX_LOG_DEBUG_EVENT, cycle->log, 0, 8 "timer delta: %M", delta); 9 10 ngx_event_process_posted(cycle, &ngx_posted_accept_events);//這里處理ngx_process_events 里面post的accept事件 11 12 //處理完accept事件,馬上釋放鎖 13 if (ngx_accept_mutex_held) { 14 ngx_shmtx_unlock(&ngx_accept_mutex); 15 } 16 17 //在處理一般的connection事件之前,先處理超時(shí)。 18 if (delta) { 19 ngx_event_expire_timers(); 20 } 21 22 //處理普通的connection事件請求 23 ngx_event_process_posted(cycle, &ngx_posted_events);7.在處理accept事件時(shí),handler是ngx_event_accept(src/event/ngx_event_accept.c),在這個(gè)函數(shù)里面,每accept一個(gè)新的連接,就更新ngx_accept_disabled。
1 do { 2 ... 3 //接受新連接 4 accept(); 5 ... 6 //更新ngx_accept_disabled 7 ngx_accept_disabled = ngx_cycle->connection_n / 8 8 - ngx_cycle->free_connection_n; 9 10 ... 11 12 }while(ev->available)?補(bǔ)充:
ngx_accept_disabled 減1這條路徑很明顯沒有申請accept鎖,所以后面的epoll_wait和accept函數(shù)會(huì)出現(xiàn)“驚群”問題。建議按如下圖改進(jìn):
?
說明:
添加紅色框步驟,在負(fù)載過高時(shí),ngx_accept_disabled 減1進(jìn)行均衡操作同時(shí),將accept事件從當(dāng)前進(jìn)程epoll中清除。這樣epoll當(dāng)前循環(huán)只處理自己的普通connection事件。當(dāng)然,左側(cè)路徑可能執(zhí)行多次,ngx_disable_accept_events操作只需要執(zhí)行一次即可。
如果過了一段時(shí)間,該進(jìn)程負(fù)載降低,進(jìn)入右側(cè)路徑,在申請accept鎖的函數(shù)中ngx_trylock_accept_mutex中,申請加鎖成功后,會(huì)調(diào)用ngx_enable_accept_events將accept事件再次加入到epoll中,這樣就可以監(jiān)聽accept事件和普通connection事件了。
以上補(bǔ)充部分為個(gè)人理解,有錯(cuò)誤之處,歡迎指正。
?
四、多進(jìn)程(每個(gè)進(jìn)程單線程)高效的原因
?一點(diǎn)思考:
1.master/worker多進(jìn)程模式,保證了系統(tǒng)的穩(wěn)定。master對多個(gè)worker子進(jìn)程和其他子進(jìn)程的管理比較方便。由于一般worker進(jìn)程數(shù)與cpu內(nèi)核數(shù)一致,所以不存在大量的子進(jìn)程生成和管理任務(wù),避免了大量子進(jìn)程的數(shù)據(jù)IPC共享開銷和切換競爭開銷。各worker進(jìn)程之間也只是重復(fù)拷貝了監(jiān)聽字,除了父子進(jìn)程間傳遞控制消息,基本沒有IPC需求。
2.每個(gè)worker單線程,不存在大量線程的生成和同步開銷。
以上兩個(gè)方面都使Nginx避免了過多的同步、競爭、切換和IPC數(shù)據(jù)傳遞,即盡可能把cpu從不必要的計(jì)算開銷中解放出來,只專注于業(yè)務(wù)計(jì)算和流程處理。
解放了CPU之后,就是內(nèi)存的高效操作了。像cache_manager_process,內(nèi)存池ngx_pool_t等等。還有可以設(shè)置進(jìn)程的affinity來綁定cpu單個(gè)內(nèi)核等。
這樣的模型更簡單,大連接量擴(kuò)展性更好。
?
“偉大的東西,總是簡單的”,此言不虛。
?
?
注:引用本人文章請注明出處,謝謝。
轉(zhuǎn)載于:https://www.cnblogs.com/NerdWill/p/4992345.html
總結(jié)
以上是生活随笔為你收集整理的Nginx 多进程连接请求/事件分发流程分析的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 做梦梦到好多蛇是不是怀孕了
- 下一篇: Nginx_查看并发连接数