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單個內核等。
這樣的模型更簡單,大連接量擴展性更好。
“偉大的東西,總是簡單的”,此言不虛。
注:引用本人文章請注明出處,謝謝。