Nginx 是以 event(事件)處理模型為基礎的模塊。它為了支持跨平臺,抽象出了 event 模塊。它支持的 event 處理類型有:AIO(異步IO),/dev/poll(Solaris 和 Unix 特有),epoll(Linux 特有),eventport(Solaris 10 特有),kqueue(BSD 特有),poll,rtsig(實時信號),select 等。
event 模塊的主要功能就是,監(jiān)聽 accept 后建立的連接,對讀寫事件進行添加刪除。事件處理模型和 Nginx 的非阻塞 IO 模型結合在一起使用。當 IO 可讀可寫的時候,相應的讀寫事件就會被喚醒,此時就會去處理事件的回調函數(shù)。
特別對于 Linux,Nginx 大部分 event 采用 epoll EPOLLET(邊沿觸發(fā))的方法來觸發(fā)事件,只有 listen 端口的讀事件是 EPOLLLT(水平觸發(fā))。對于邊沿觸發(fā),如果出現(xiàn)了可讀事件,必須及時處理,否則可能會出現(xiàn)讀事件不再觸發(fā),連接餓死的情況。
typedef struct {
/* 添加刪除事件 */
ngx_int_t (*add)(ngx_event_t *ev, ngx_int_t event, ngx_uint_t flags);
ngx_int_t (*del)(ngx_event_t *ev, ngx_int_t event, ngx_uint_t flags);
ngx_int_t (*enable)(ngx_event_t *ev, ngx_int_t event, ngx_uint_t flags);
ngx_int_t (*disable)(ngx_event_t *ev, ngx_int_t event, ngx_uint_t flags);
/* 添加刪除連接,會同時監(jiān)聽讀寫事件 */
ngx_int_t (*add_conn)(ngx_connection_t *c);
ngx_int_t (*del_conn)(ngx_connection_t *c, ngx_uint_t flags);
ngx_int_t (*process_changes)(ngx_cycle_t *cycle, ngx_uint_t nowait);
/* 處理事件的函數(shù) */
ngx_int_t (*process_events)(ngx_cycle_t *cycle, ngx_msec_t timer,
ngx_uint_t flags);
ngx_int_t (*init)(ngx_cycle_t *cycle, ngx_msec_t timer);
void (*done)(ngx_cycle_t *cycle);
} ngx_event_actions_t;
上述是 event 處理抽象出來的關鍵結構體,可以看到,每個 event 處理模型,都需要實現(xiàn)部分功能。最關鍵的是 add 和 del 功能,就是最基本的添加和刪除事件的函數(shù)。
Nginx 是多進程程序,80 端口是各進程所共享的,多進程同時 listen 80 端口,勢必會產(chǎn)生競爭,也產(chǎn)生了所謂的“驚群”效應。當內(nèi)核 accept 一個連接時,會喚醒所有等待中的進程,但實際上只有一個進程能獲取連接,其他的進程都是被無效喚醒的。所以 Nginx 采用了自有的一套 accept 加鎖機制,避免多個進程同時調用 accept。Nginx 多進程的鎖在底層默認是通過 CPU 自旋鎖來實現(xiàn)。如果操作系統(tǒng)不支持自旋鎖,就采用文件鎖。
Nginx 事件處理的入口函數(shù)是 ngx_process_events_and_timers(),下面是部分代碼,可以看到其加鎖的過程:
if (ngx_use_accept_mutex) {
if (ngx_accept_disabled > 0) {
ngx_accept_disabled--;
} else {
if (ngx_trylock_accept_mutex(cycle) == NGX_ERROR) {
return;
}
if (ngx_accept_mutex_held) {
flags |= NGX_POST_EVENTS;
} else {
if (timer == NGX_TIMER_INFINITE
|| timer > ngx_accept_mutex_delay)
{
timer = ngx_accept_mutex_delay;
}
}
}
}
在 ngx_trylock_accept_mutex()函數(shù)里面,如果拿到了鎖,Nginx 會把 listen 的端口讀事件加入 event 處理,該進程在有新連接進來時就可以進行 accept 了。注意 accept 操作是一個普通的讀事件。下面的代碼說明了這點:
(void) ngx_process_events(cycle, timer, flags);
if (ngx_posted_accept_events) {
ngx_event_process_posted(cycle, &ngx_posted_accept_events);
}
if (ngx_accept_mutex_held) {
ngx_shmtx_unlock(&ngx_accept_mutex);
}
ngx_process_events()函數(shù)是所有事件處理的入口,它會遍歷所有的事件。搶到了 accept 鎖的進程跟一般進程稍微不同的是,它被加上了 NGX_POST_EVENTS 標志,也就是說在 ngx_process_events() 函數(shù)里面只接受而不處理事件,并加入 post_events 的隊列里面。直到 ngx_accept_mutex 鎖去掉以后才去處理具體的事件。為什么這樣?因為 ngx_accept_mutex 是全局鎖,這樣做可以盡量減少該進程搶到鎖以后,從 accept 開始到結束的時間,以便其他進程繼續(xù)接收新的連接,提高吞吐量。
ngx_posted_accept_events 和 ngx_posted_events 就分別是 accept 延遲事件隊列和普通延遲事件隊列??梢钥吹?ngx_posted_accept_events 還是放到 ngx_accept_mutex 鎖里面處理的。該隊列里面處理的都是 accept 事件,它會一口氣把內(nèi)核 backlog 里等待的連接都 accept 進來,注冊到讀寫事件里。
而 ngx_posted_events 是普通的延遲事件隊列。一般情況下,什么樣的事件會放到這個普通延遲隊列里面呢?我的理解是,那些 CPU 耗時比較多的都可以放進去。因為 Nginx 事件處理都是根據(jù)觸發(fā)順序在一個大循環(huán)里依次處理的,因為 Nginx 一個進程同時只能處理一個事件,所以有些耗時多的事件會把后面所有事件的處理都耽擱了。
除了加鎖,Nginx 也對各進程的請求處理的均衡性作了優(yōu)化,也就是說,如果在負載高的時候,進程搶到的鎖過多,會導致這個進程被禁止接受請求一段時間。
比如,在 ngx_event_accept 函數(shù)中,有類似代碼:
ngx_accept_disabled = ngx_cycle->connection_n / 8
- ngx_cycle->free_connection_n;
ngx_cycle->connection_n 是進程可以分配的連接總數(shù),ngx_cycle->free_connection_n 是空閑的進程數(shù)。上述等式說明了,當前進程的空閑進程數(shù)小于 1/8 的話,就會被禁止 accept 一段時間。
Nginx 在需要用到超時的時候,都會用到定時器機制。比如,建立連接以后的那些讀寫超時。Nginx 使用紅黑樹來構造定期器,紅黑樹是一種有序的二叉平衡樹,其查找插入和刪除的復雜度都為 O(logn),所以是一種比較理想的二叉樹。
定時器的機制就是,二叉樹的值是其超時時間,每次查找二叉樹的最小值,如果最小值已經(jīng)過期,就刪除該節(jié)點,然后繼續(xù)查找,直到所有超時節(jié)點都被刪除。
更多建議: