詳解nginx驚群問題的解決方式

2022-09-24 18:39:12 字數 3752 閱讀 8159

對於nginx的驚群問題,我們首先需要理解的是,在nginx啟動過程中,master程序會監聽配置檔案中指定的各個埠,然後master程序就會呼叫fork()方法建立各個子程序,根據程序的工作原理,子程序是會繼承父程序的全部記憶體資料以及監聽的埠的,也就是說worker程序在啟動之後也是會監聽各個埠的。關於驚群,指的就是當客戶端有新建連線的請求到來時,就會觸發各個worker程序的連線建立事件,但是只有乙個worker程序能夠正常處理該事件,而其他的worker程序會發現事件已經失效,從而重新迴圈進入等待狀態。這種由於乙個事件而「驚」起了所有worker程序的現象就是驚群問題。很明顯,如果所有的worker程序都被觸發了,那麼這將消耗大量的資源,本文則主要講解nginx是如何處理驚群問題的。

1. 解決方式

在前面的文章中,我們講到,每個worker程序被建立的時候,都會呼叫ngx_worker_process_init()方法初始化當前worker程序,這個過程中有乙個非常重要的步驟,即每個worker程序都會呼叫epoll_create()方法為自己建立乙個獨有的epoll控制代碼。對於每乙個需要監聽的埠,都有乙個檔案描述符與之對應,而worker程序只有將該檔案描述符通過epoll_ctl()方法新增到當前程序的epoll控制代碼中,並且監聽accept事件,此時才會被客戶端的連線建立事件觸發,從而處理該事件。從這裡也可以看出,worker程序如果沒有將所需要監聽的埠對應的檔案描述符新增到該程序的epoll控制代碼中,那麼其是無法被觸發對應的事件的。基於這個原理,nginx就使用了乙個共享鎖來控制當前程序是否有許可權將需要監聽的埠新增到當前程序的epoll控制代碼中,也就是說,只有獲取鎖的程序才會監聽目標埠。通過這種方式,就保證了每次事件發生時,只有乙個worker程序會被觸發。如下圖所示為worker程序工作迴圈的乙個示意圖:

這裡關於圖中的流程,需要說明的一點是,每個worker程序在進入迴圈之後就會嘗試獲取共享鎖,如果沒有獲取到,就會將所監聽的埠的檔案描述符從當前程序的epoll控制代碼中移除(即使並不存在也會移除),這麼做的主要目的是防止丟失客戶端連線事件,即使這可能造成少量的驚群問題,但是並不嚴重。試想一下,如果按照理論,在當前程序釋放鎖的時候就將監聽的埠的檔案描述符從epoll控制代碼中移除,那麼在下乙個worker程序獲取鎖之前,這段時間各個埠對應的檔案描述符是沒有任何epoll控制代碼進行監聽的,此時就會造成事件的丟失。如果反過來,按照圖中的在獲取鎖失敗的時候才移除監聽的檔案描述符,由於獲取鎖失敗,則說明當前一定有乙個程序已經監聽了這些檔案描述符,因而此時移除是安全的。但是這樣會造成的乙個問題是,按照上圖,當前程序在乙個迴圈執行完畢的時候,會釋放鎖,然後處理其他的事件,注意這個過程中其是沒有釋放所監聽的檔案描述符的。此時,如果另乙個程序獲取到了鎖,並且監聽了程式設計客棧檔案描述符,那麼這個時候就有兩個程序監聽了檔案描述符,因而此時如果客戶端發生連線建立事件,那麼就會觸發兩個worker程序。這個問題是可以容忍的,主要原因有兩點:

2. 原始碼講解

worker程序初始事件的方法主要是在ngx_process_events_and_timers()方法中進行的,下面我們就來看看該方法是如何處理整個流程的,如下是該方法的原始碼:

void ngx_process_events_and_timers(ngx_cycle_t *cycle)

// 這裡開始處理事件,對於kqueue模型,其指向的是ngx_kqueue_process_events()方法,

// 而對於epoll模型,其指向的是ngx_epoll_process_events()方法

// 這個方法的主要作用是,在對應的事件模型中獲取事件列表,然後將事件新增到ngx_posted_accept_events

// 佇列或者ngx_posted_events佇列中

(void) ngx_process_events(cycle, timer, flags);

// 這裡開始處理accept事件,將其交由ngx_event_accept.c的ngx_event_accept()方法處理;

ngx_event_process_posted(cycle, &ngx_posted_accept_events);

// 開始釋放鎖

ifwww.cppcns.com (ngx_accept_mutex_held)

// 如果不需要在事件佇列中進行處理,則直接處理該事件

// 對於事件的處理,如果是accept事件,則將其交由ngx_event_accept.c的ngx_event_accept()方法處理;

// 如果是讀事件,則將其交由ngx_http_request.c的ngx_http_wait_request_handler()方法處理;

// 對於處理完成的事件,最後會交由ngx_http_request.c的ngx_http_keepalive_handler()方法處理。

// 這裡開始處理除accept事件外的其他事件

ngx_event_process_posted(cycle, &ngx_posted_events);

}上面的**中,我們省略了大部分的檢查工作,只留下了骨架**。首先,worker程序會呼叫ngx_trylock_accept_mutex()方法獲取鎖,這其中如果獲取到了鎖就會監聽各個埠對應的檔案描述符。然後會呼叫ngx_process_events()方法處理epoll控制代碼中監聽到的事件。接著會釋放共享鎖,最後就是處理已建立連線的客戶端的讀寫事件。下面我們來看一下ngx_trylock_accept_mutex()方法是如何獲取共享鎖的:

ngx_int_t ngx_trylock_accept_mutex(ngx_cycle_t *cycle)

// 這裡主要是將當前連線的檔案描述符註冊到對應事件的佇列中,比如kqueue模型的change_list陣列

// nginx在啟用各個worker程序的時候,預設情況下,worker程序是會繼承master程序所監聽的socket控制代碼的,

// 這就導致乙個問題,就是當某個埠有客戶端事件時,就會把監聽該埠的程序都給喚醒,

// 但是只有乙個worker程序能夠成功處理該事件,而其他的程序被喚醒之後發現事件已經過期,

// 因而會繼續進入等待狀態,這種現象稱為"驚群"現象。

// nginx解決驚群現象的方式一方面是通過這裡的共享鎖的方式,即只有獲取到鎖的worker程序才能處理

// 客戶端事件,但實際上,worker程序是通過在獲取鎖的過程中,為當前worker程序重新新增各個埠的監聽事件,

// 而其他worker程序則不會監聽。也就是說同一時間只有乙個worker程序會監聽各個埠,

// 這樣就避免了"驚群"問題。

// 這裡的ngx_enable_accept_events()方法就是為當前程序重新新增各個埠的監聽事件的。

if (ngx_enable_accept_events(cycle) == ngx_error)

// 標誌當前已經成功獲取到了鎖

ngx_accept_events = 0;

ngx_accept_mutex_held = 1;

return ngx_ok;

} // 前面獲取鎖失敗了,因而這裡需要重置ngx_accept_mutex_held的狀態,並且將當前連線的事件給清除掉

if (ngx_accept_mutex_held)

ngx_accept_mutex_held = 0;

} return ngx_ok;

}上面的**中,本質上主要做了三件事:

3. 小結

本文首先對驚群現象的產生原因進行了講解,然後介紹了nginx是如何解決驚群問題的,最後從原始碼角度對nginx處理驚群問題的方式進行了講解。

本文標題: 詳解nginx驚群問題的解決方式

本文位址: /jiqiao/fuwuqi/296967.html

nginx的驚群問題

問題 nginx是乙個高效能網路併發伺服器,它的程序模型是,多程序模型,有乙個master程序,會啟動多個worker程序,一般是根據cpu核心個數決定 然後每個程序又利用io多路復用技術,監聽多個socket,達到高併發的能力。它存在乙個問題就在於,每個worker子程序都會去accept 監聽套...

nginx的驚群問題

nginx採用master worker程序的模式,master負責解析配置,啟動worker程序和處理訊號,比如restart重啟worker程序,worker負責真正處理請求。當有多個worker程序時,乙個請求將被哪個worker程序處理呢?更具體一點,傳送請求的客戶端會與哪個worker程序...

「驚群」,看看nginx是怎麼解決它的

在說nginx前,先來看看什麼是 驚群 簡單說來,多執行緒 多程序 linux下執行緒程序也沒多大區別 等待同乙個socket事件,當這個事件發生時,這些執行緒 程序被同時喚醒,就是驚群。可以想見,效率很低下,許多程序被核心重新排程喚醒,同時去響應這乙個事件,當然只有乙個程序能處理事件成功,其他的程...