Nginx學(xué)習(xí)筆記之事件驅(qū)動(dòng)框架處理流程
ngx_event_core_module模塊的ngx_event_process_init方法對(duì)事件模塊做了一些初始化。其中包括將“請(qǐng)求連接”這樣一個(gè)讀事件對(duì)應(yīng)的處理方法(handler)設(shè)置為ngx_event_accept函數(shù),并將此事件添加到epoll模塊中。當(dāng)有新連接事件發(fā)生時(shí),ngx_event_accept就會(huì)被調(diào)用。大致流程是這樣:
worker進(jìn)程在ngx_worker_process_cycle方法中不斷循環(huán)調(diào)用ngx_process_events_and_timers函數(shù)處理事件,這個(gè)函數(shù)是事件處理的總?cè)肟凇?/p>
ngx_process_events_and_timers會(huì)調(diào)用ngx_process_events,這是一個(gè)宏,相當(dāng)于ngx_event_actions.process_events,ngx_event_actions是個(gè)全局的結(jié)構(gòu)體,存儲(chǔ)了對(duì)應(yīng)事件驅(qū)動(dòng)模塊(這里是epoll模塊)的10個(gè)函數(shù)接口。所以這里就是調(diào)用了ngx_epoll_module_ctx.actions.process_events函數(shù),也就是ngx_epoll_process_events函數(shù)來(lái)處理事件。
ngx_epoll_process_events調(diào)用Linux函數(shù)接口epoll_wait獲得“有新連接”這個(gè)事件,然后調(diào)用這個(gè)事件的handler處理函數(shù)來(lái)對(duì)這個(gè)事件進(jìn)行處理。
在上面已經(jīng)說(shuō)過(guò)handler已經(jīng)被設(shè)置成了ngx_event_accept函數(shù),所以就調(diào)用ngx_event_accept進(jìn)行實(shí)際的處理。
下面分析ngx_event_accept方法,它的流程圖如下所示:

經(jīng)過(guò)精簡(jiǎn)的代碼如下,注釋中的序號(hào)對(duì)應(yīng)上圖的序號(hào):
void
ngx_event_accept(ngx_event_t *ev)
{
socklen_t socklen;
ngx_err_t err;
ngx_log_t *log;
ngx_uint_t level;
ngx_socket_t s;
ngx_event_t *rev, *wev;
ngx_listening_t *ls;
ngx_connection_t *c, *lc;
ngx_event_conf_t *ecf;
u_char sa[NGX_SOCKADDRLEN];
if (ev->timedout) {
if (ngx_enable_accept_events((ngx_cycle_t *) ngx_cycle) != NGX_OK) {
return;
}
ev->timedout = 0;
}
ecf = ngx_event_get_conf(ngx_cycle->conf_ctx, ngx_event_core_module);
if (ngx_event_flags & NGX_USE_RTSIG_EVENT) {
ev->available = 1;
} else if (!(ngx_event_flags & NGX_USE_KQUEUE_EVENT)) {
ev->available = ecf->multi_accept;
}
lc = ev->data;
ls = lc->listening;
ev->ready = 0;
do {
socklen = NGX_SOCKADDRLEN;
/* 1、accept方法試圖建立連接,非阻塞調(diào)用 */
s = accept(lc->fd, (struct sockaddr *) sa, &socklen);
if (s == (ngx_socket_t) -1)
{
err = ngx_socket_errno;
if (err == NGX_EAGAIN)
{
/* 沒(méi)有連接,直接返回 */
return;
}
level = NGX_LOG_ALERT;
if (err == NGX_ECONNABORTED) {
level = NGX_LOG_ERR;
} else if (err == NGX_EMFILE || err == NGX_ENFILE) {
level = NGX_LOG_CRIT;
}
if (err == NGX_ECONNABORTED) {
if (ngx_event_flags & NGX_USE_KQUEUE_EVENT) {
ev->available--;
}
if (ev->available) {
continue;
}
}
if (err == NGX_EMFILE || err == NGX_ENFILE) {
if (ngx_disable_accept_events((ngx_cycle_t *) ngx_cycle)
!= NGX_OK)
{
return;
}
if (ngx_use_accept_mutex) {
if (ngx_accept_mutex_held) {
ngx_shmtx_unlock(&ngx_accept_mutex);
ngx_accept_mutex_held = 0;
}
ngx_accept_disabled = 1;
} else {
ngx_add_timer(ev, ecf->accept_mutex_delay);
}
}
return;
}
/* 2、設(shè)置負(fù)載均衡閾值 */
ngx_accept_disabled = ngx_cycle->connection_n / 8
- ngx_cycle->free_connection_n;
/* 3、從連接池獲得一個(gè)連接對(duì)象 */
c = ngx_get_connection(s, ev->log);
/* 4、為連接創(chuàng)建內(nèi)存池 */
c->pool = ngx_create_pool(ls->pool_size, ev->log);
c->sockaddr = ngx_palloc(c->pool, socklen);
ngx_memcpy(c->sockaddr, sa, socklen);
log = ngx_palloc(c->pool, sizeof(ngx_log_t));
/* set a blocking mode for aio and non-blocking mode for others */
/* 5、設(shè)置套接字屬性為阻塞或非阻塞 */
if (ngx_inherited_nonblocking) {
if (ngx_event_flags & NGX_USE_AIO_EVENT) {
if (ngx_blocking(s) == -1) {
ngx_log_error(NGX_LOG_ALERT, ev->log, ngx_socket_errno,
ngx_blocking_n " failed");
ngx_close_accepted_connection(c);
return;
}
}
} else {
if (!(ngx_event_flags & (NGX_USE_AIO_EVENT|NGX_USE_RTSIG_EVENT))) {
if (ngx_nonblocking(s) == -1) {
ngx_log_error(NGX_LOG_ALERT, ev->log, ngx_socket_errno,
ngx_nonblocking_n " failed");
ngx_close_accepted_connection(c);
return;
}
}
}
*log = ls->log;
c->recv = ngx_recv;
c->send = ngx_send;
c->recv_chain = ngx_recv_chain;
c->send_chain = ngx_send_chain;
c->log = log;
c->pool->log = log;
c->socklen = socklen;
c->listening = ls;
c->local_sockaddr = ls->sockaddr;
c->local_socklen = ls->socklen;
c->unexpected_eof = 1;
rev = c->read;
wev = c->write;
wev->ready = 1;
if (ngx_event_flags & (NGX_USE_AIO_EVENT|NGX_USE_RTSIG_EVENT)) {
/* rtsig, aio, iocp */
rev->ready = 1;
}
if (ev->deferred_accept) {
rev->ready = 1;
}
rev->log = log;
wev->log = log;
/*
* TODO: MT: - ngx_atomic_fetch_add()
* or protection by critical section or light mutex
*
* TODO: MP: - allocated in a shared memory
* - ngx_atomic_fetch_add()
* or protection by critical section or light mutex
*/
c->number = ngx_atomic_fetch_add(ngx_connection_counter, 1);
if (ls->addr_ntop) {
c->addr_text.data = ngx_pnalloc(c->pool, ls->addr_text_max_len);
if (c->addr_text.data == NULL) {
ngx_close_accepted_connection(c);
return;
}
c->addr_text.len = ngx_sock_ntop(c->sockaddr, c->socklen,
c->addr_text.data,
ls->addr_text_max_len, 0);
if (c->addr_text.len == 0) {
ngx_close_accepted_connection(c);
return;
}
}
/* 6、將新連接對(duì)應(yīng)的讀寫(xiě)事件添加到epoll對(duì)象中 */
if (ngx_add_conn && (ngx_event_flags & NGX_USE_EPOLL_EVENT) == 0) {
if (ngx_add_conn(c) == NGX_ERROR) {
ngx_close_accepted_connection(c);
return;
}
}
log->data = NULL;
log->handler = NULL;
/* 7、TCP建立成功調(diào)用的方法,這個(gè)方法在ngx_listening_t結(jié)構(gòu)體中 */
ls->handler(c);
} while (ev->available); /* available標(biāo)志表示一次盡可能多的建立連接,由配置項(xiàng)multi_accept決定 */
}
Nginx中的“驚群”問(wèn)題
Nginx一般會(huì)運(yùn)行多個(gè)worker進(jìn)程,這些進(jìn)程會(huì)同時(shí)監(jiān)聽(tīng)同一端口。當(dāng)有新連接到來(lái)時(shí),內(nèi)核將這些進(jìn)程全部喚醒,但只有一個(gè)進(jìn)程能夠和客戶端連接成功,導(dǎo)致其它進(jìn)程在喚醒時(shí)浪費(fèi)了大量開(kāi)銷,這被稱為“驚群”現(xiàn)象。Nginx解決“驚群”的方法是,讓進(jìn)程獲得互斥鎖ngx_accept_mutex,讓進(jìn)程互斥地進(jìn)入某一段臨界區(qū)。在該臨界區(qū)中,進(jìn)程將它所要監(jiān)聽(tīng)的連接對(duì)應(yīng)的讀事件添加到epoll模塊中,使得當(dāng)有“新連接”事件發(fā)生時(shí),該worker進(jìn)程會(huì)作出反應(yīng)。這段加鎖并添加事件的過(guò)程是在函數(shù)ngx_trylock_accept_mutex中完成的。而當(dāng)其它進(jìn)程也進(jìn)入該函數(shù)想要添加讀事件時(shí),發(fā)現(xiàn)互斥鎖被另外一個(gè)進(jìn)程持有,所以它只能返回,它所監(jiān)聽(tīng)的事件也無(wú)法添加到epoll模塊,從而無(wú)法響應(yīng)“新連接”事件。但這會(huì)出現(xiàn)一個(gè)問(wèn)題:持有互斥鎖的那個(gè)進(jìn)程在什么時(shí)候釋放互斥鎖呢?如果需要等待它處理完所有的事件才釋放鎖的話,那么會(huì)需要相當(dāng)長(zhǎng)的時(shí)間。而在這段時(shí)間內(nèi),其它worker進(jìn)程無(wú)法建立新連接,這顯然是不可取的。Nginx的解決辦法是:通過(guò)ngx_trylock_accept_mutex獲得了互斥鎖的進(jìn)程,在獲得就緒讀/寫(xiě)事件并從epoll_wait返回后,將這些事件歸類放入隊(duì)列中:
新連接事件放入ngx_posted_accept_events隊(duì)列
已有連接事件放入ngx_posted_events隊(duì)列
代碼如下:
if (flags & NGX_POST_EVENTS)
{
/* 延后處理這批事件 */
queue = (ngx_event_t **) (rev->accept ? &ngx_posted_accept_events : &ngx_posted_events);
/* 將事件添加到延后執(zhí)行隊(duì)列中 */
ngx_locked_post_event(rev, queue);
}
else
{
rev->handler(rev); /* 不需要延后,則立即處理事件 */
}
寫(xiě)事件做類似處理。進(jìn)程接下來(lái)處理ngx_posted_accept_events隊(duì)列中的事件,處理完后立即釋放互斥鎖,使該進(jìn)程占用鎖的時(shí)間降到了最低。
Nginx中的負(fù)載均衡問(wèn)題
Nginx中每個(gè)進(jìn)程使用了一個(gè)處理負(fù)載均衡的閾值ngx_accept_disabled,它在上圖的第2步中被初始化:
ngx_accept_disabled = ngx_cycle->connection_n / 8 - ngx_cycle->free_connection_n;
它的初值為一個(gè)負(fù)數(shù),該負(fù)數(shù)的絕對(duì)值等于總連接數(shù)的7/8.當(dāng)閾值小于0時(shí)正常響應(yīng)新連接事件,當(dāng)閾值大于0時(shí)不再響應(yīng)新連接事件,并將ngx_accept_disabled減1,代碼如下:
if (ngx_accept_disabled > 0)
{
ngx_accept_disabled--;
}
else
{
if (ngx_trylock_accept_mutex(cycle) == NGX_ERROR)
{
return;
}
....
}
這說(shuō)明,當(dāng)某個(gè)進(jìn)程當(dāng)前的連接數(shù)達(dá)到能夠處理的總連接數(shù)的7/8時(shí),負(fù)載均衡機(jī)制被觸發(fā),進(jìn)程停止響應(yīng)新連接。
參考:
《深入理解Nginx》 P328-P334.
相關(guān)文章
詳解Nginx幾種常見(jiàn)實(shí)現(xiàn)301重定向方法上的區(qū)別
本篇文章主要介紹了詳解Nginx幾種常見(jiàn)實(shí)現(xiàn)301重定向方法上的區(qū)別,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2017-06-06
nginx 開(kāi)啟 pathinfo的過(guò)程詳解
這篇文章主要介紹了nginx 開(kāi)啟 pathinfo的過(guò)程詳解,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2019-08-08
Nginx中日志模塊的應(yīng)用和配置應(yīng)用示例
Nginx是一款高性能的HTTP和反向代理服務(wù)器,廣泛應(yīng)用于互聯(lián)網(wǎng)領(lǐng)域,這篇文章主要介紹了Nginx中日志模塊的應(yīng)用和配置,下面通過(guò)一個(gè)簡(jiǎn)單的實(shí)例來(lái)演示Nginx日志模塊的應(yīng)用和配置,需要的朋友可以參考下2024-02-02
解讀Nginx和Apache的特點(diǎn)與區(qū)別
這篇文章主要介紹了解讀Nginx和Apache的特點(diǎn)與區(qū)別,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2023-03-03
nginx實(shí)現(xiàn)一個(gè)域名配置多個(gè)laravel項(xiàng)目的方法示例
這篇文章主要介紹了nginx實(shí)現(xiàn)一個(gè)域名配置多個(gè)laravel項(xiàng)目的方法示例,小編覺(jué)得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2019-01-01
通過(guò)nginx實(shí)現(xiàn)方向代理過(guò)程圖解
這篇文章主要介紹了通過(guò)nginx實(shí)現(xiàn)方向代理過(guò)程圖解,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2020-02-02
安裝OpenResty(Nginx倉(cāng)庫(kù))
這篇文章主要介紹了安裝OpenResty(Nginx倉(cāng)庫(kù)),需要的朋友可以參考下2023-06-06
Laravel的Nginx重寫(xiě)規(guī)則實(shí)例代碼
這篇文章主要介紹了Laravel的Nginx重寫(xiě)規(guī)則實(shí)例代碼,需要的朋友可以參考下2017-09-09
nginx打印請(qǐng)求頭日志方法(親測(cè)可用)
之前想用nginx打印收到的請(qǐng)求的請(qǐng)求頭,但是只找到打印請(qǐng)求體的,沒(méi)有打印請(qǐng)求頭的,本文就來(lái)介紹一下nginx打印請(qǐng)求頭日志方法,感興趣的可以了解一下2023-11-11

