Nginx 多進程連接請求/事件分發流程分析

Nginx 多進程連接請求/事件分發流程分析

https://www.cnblogs.com/NerdWill/p/4992345.html

Nginx使用多進程的方法進行任務處理,每個worker進程只有一個線程,單線程循環處理全部監聽的事件。本文重點分析一下多進程間的負載均衡問題以及Nginx多進程事件處理流程,方便大家自己寫程序的時候借鑑。

 

一、監聽建立流程

整個建立監聽socket到accept的過程如下圖:

 

說明:

1.main裏面調用ngx_init_cycle(src/core/ngx_cycle.c),ngx_init_cycle裏面完成很多基本的配置,如文件,共享內存,socket等。

2.上圖左上角是ngx_init_cycle裏面調用的ngx_open_listening_sockets(src/core/ngx_connection.c)主要完成的工作,包括基本的創建socket,setsockopt,bind和listen等。

3.然後是正常的子進程生成過程。在每個子worker進程的ngx_worker_process_cycle中,在調用ngx_worker_process_init裏面調用各模塊的初始化操作init_process。一epoll module爲例,這裏調用ngx_event_process_init,裏面初始化多個NGX_EVENT_MODULE類型的module.NGX_EVENT_MODULE類型的只有ngx_event_core_module和ngx_epoll_module。前一個module的actions部分爲空。ngx_epoll_module裏面的init函數就是ngx_epoll_init。ngx_epoll_init函數主要完成epoll部分相關的初始化,包括epoll_create,設置ngx_event_actions等。

4.初始化完ngx_epoll_module,繼續ngx_event_process_init,然後循環設置每個listening socket的read handler爲ngx_event_accept.最後將每個listening socket的READ事件添加到epoll進行等待。

5.ngx_event_process_init初始化完成後,每個worker process開始循環處理events&timers。最終調用的是epoll_wait。由於之前listening socket以及加入到epoll,所以如果監聽字有read消息,那麼久調用rev->handler進行處理,監聽字的handler之前已經設置爲ngx_event_accept。ngx_event_accept主要是調用accept函數來接受新的客戶端套接字client socket。

下面是監聽字的處理函數ngx_event_accept流程圖:

 

說明:

1.前半部分主要是通過accept接受新連接字,生成並設置相關結構,然後添加到epoll中。

2.後半部分調用connection中的listening對應的handler,即ngx_xxx_init_connection,其中xxx可以是mail,http和stream。顧名思義,該函數主要是做新的accepted連接字的初始化工作。上圖以http module爲例,初始化設置了連接字的read handler等。

 

二、負載均衡問題

 Nginx裏面通過一個變量ngx_accept_disabled來實施進程間獲取客戶端連接請求的負載均衡策略。ngx_accept_disabled使用流程圖:

 

說明:

1.ngx_process_events_and_timers函數中,通過ngx_accept_disabled的正負判斷當前進程負載高低(大於0,高負載;小於0,低負載)。如果低負載時,不做處理,進程去申請accept鎖,監聽並接受新的連接。

2.如果是高負載時,ngx_accept_disabled就發揮作用了。這時,不去申請accept鎖,讓出監聽和接受新連接的機會。同時ngx_accept_disabled減1,表示通過讓出一次accept申請的機會,該進程的負載將會稍微減輕,直到ngx_accept_disabled最後小於0,重新進入低負載的狀態,開始新的accept鎖競爭。

 

參考鏈接:http://www.jb51.net/article/52177.htm

 

三、“驚羣”問題

“驚羣”問題:多個進程同時監聽一個套接字,當有新連接到來時,會同時喚醒全部進程,但只能有一個進程與客戶端連接成功,造成資源的浪費。

Nginx通過進程間共享互斥鎖ngx_accept_mutex來控制多個worker進程對公共監聽套接字的互斥訪問,獲取鎖後調用accept取出與客戶端已經建立的連接加入epoll,然後釋放互斥鎖。

Nginx處理流程示意圖:

說明:

1.ngx_accept_disabled作爲單個進程負載較高(最大允許連接數的7/8)的標記,計算公式:

ngx_accept_disabled = ngx_cycle->connection_n/8 - ngx_cycle->free_connection_n;

即進程可用連接數free_connection_n小於總連接數connection_n的1/8時ngx_accept_disabled大於0;否則小於0.或者說ngx_accept_disabled小於0時,表示可用連接數較多,負載較低;ngx_accept_disabled大於0時,說明可用連接數較少,負載較高。

2.如果進程負載較低時,即ngx_accept_disabled 小於0,進程允許競爭accept鎖。

3.如果進程負載較高時,放棄競爭accept鎖,同時ngx_accept_disabled 減1,即認爲由於讓出一次競爭accept鎖的機會,負載稍微減輕(ngx_accept_disabled 小於0可用)。由於負載較高時(ngx_accept_disabled >0)只是將ngx_accept_disabled 減1,這裏不申請accept鎖,所以後續的accept函數會遭遇“驚羣”問題,返回錯誤errno=EAGAIN,直接返回(個人覺得這裏有改進的空間,見補充部分)。

ngx_process_events_and_timers函數部分代碼如下:

複製代碼

 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循環裏面,此次不參與事件處理,進行下一次循環。

5.如果競爭加鎖成功,設置NGX_POST_EVENTS標記,表示將事件先放入隊列中,稍後處理,優先釋放ngx_accept_mutex,防止單個進程過多佔用鎖時間,影響事件處理效率。ngx_epoll_process_events函數有如下部分(寫事件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放入隊列,稍後處理
6 
7 } else {
8     rev->handler(rev);
9 }

複製代碼

6.從ngx_epoll_process_events返回ngx_process_events_and_timers,然後是處理accept事件(下面代碼10行);處理完accept事件,馬上釋放鎖(下面代碼13-15行),給其他進程機會去監聽連接事件。最後處理一般的連接事件。

複製代碼

 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事件之前,先處理超時。
18 if (delta) {
19     ngx_event_expire_timers();
20 }
21 
22 //處理普通的connection事件請求
23 ngx_event_process_posted(cycle, &ngx_posted_events);

複製代碼

7.在處理accept事件時,handler是ngx_event_accept(src/event/ngx_event_accept.c),在這個函數裏面,每accept一個新的連接,就更新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)

複製代碼

 補充:

ngx_accept_disabled 減1這條路徑很明顯沒有申請accept鎖,所以後面的epoll_wait和accept函數會出現“驚羣”問題。建議按如下圖改進:

 

說明:

添加紅色框步驟,在負載過高時,ngx_accept_disabled 減1進行均衡操作同時,將accept事件從當前進程epoll中清除。這樣epoll當前循環只處理自己的普通connection事件。當然,左側路徑可能執行多次,ngx_disable_accept_events操作只需要執行一次即可。

如果過了一段時間,該進程負載降低,進入右側路徑,在申請accept鎖的函數中ngx_trylock_accept_mutex中,申請加鎖成功後,會調用ngx_enable_accept_events將accept事件再次加入到epoll中,這樣就可以監聽accept事件和普通connection事件了。

以上補充部分爲個人理解,有錯誤之處,歡迎指正。

 

四、多進程(每個進程單線程)高效的原因

 一點思考:

1.master/worker多進程模式,保證了系統的穩定。master對多個worker子進程和其他子進程的管理比較方便。由於一般worker進程數與cpu內核數一致,所以不存在大量的子進程生成和管理任務,避免了大量子進程的數據IPC共享開銷和切換競爭開銷。各worker進程之間也只是重複拷貝了監聽字,除了父子進程間傳遞控制消息,基本沒有IPC需求。

2.每個worker單線程,不存在大量線程的生成和同步開銷。

以上兩個方面都使Nginx避免了過多的同步、競爭、切換和IPC數據傳遞,即儘可能把cpu從不必要的計算開銷中解放出來,只專注於業務計算和流程處理。

解放了CPU之後,就是內存的高效操作了。像cache_manager_process,內存池ngx_pool_t等等。還有可以設置進程的affinity來綁定cpu單個內核等。

這樣的模型更簡單,大連接量擴展性更好。

 

“偉大的東西,總是簡單的”,此言不虛。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章