《深入剖析ngx》—— 事件管理


1. 綜述

ngx 是事件驅動,沒有事件,ngx會一直阻塞在 epoll_wait 或 sigsuspend 上,ngx的事件有 IO事件,定時器事件。

2. 多路IO模型

ngx對多路復用IO進行了封裝。
封裝為 ngx_event_action_t 結構體,該結構體主要屬性為 回調函數

為了方便使用,ngx定義了一些宏

如此使用多路IO時,無需關心具體的IO接口,只需要用 ngx_add_event() 等

ngx綁定epoll到 ngx_event_action_t,就是 給 ngx_event_action_t 屬性賦值。

如此, ngx_event_action_t 就是 ngx_epoll_module_ctx.actions。

而調用這個 init 函數是在

其中 ecf->use 保存 解析 配置文件 use 指令獲得的值,若使用 use epoll ,則這里 module 為 ngx_event_epoll_module,如此就調用了 ngx_event_epoll_module.init ,也就是綁定了 ngx_event_action_t 為 epoll.

所以我們得到如下框圖

3. epoll

接口如下

epoll有 LT 和 ET 模式,LT是默認工作模式,支持 no-block , block,ET 只支持 no-block,ET效率高,
為了避免 ET模式下,讀取所有數據,通常邏輯如下,
設置被監聽套接字為 no-block,加入epoll,
epoll_wait,得到事件,
讀取套接字,直到返回 EAGAIN(對於 面向包/令牌的文件,比如數據包套接字接口,規范式終端)或是 read(),write()返回的數據長度小於請求的數據長度(對於面向流的文件,比如pipe, FIFO,流套接口),才重新監聽epoll

4. 負載均衡

ngx是多工作進程模型,所以可能出現 一個進程負責 1 個請求,一個進程負責 400 個請求的情況,為了避免這種情況,需要實現負載均衡。

4.1 客戶端請求的負載均衡

ngx工作進程處理請求的源頭是 監聽套接字 接受客戶端請求,但是 若多個工作進程 擁有監聽套接字,當請求到來,則會出現進程同時搶請求的情況,這被稱為 驚群。
ngx通過給監聽套接字上鎖的方法解決了這個問題。

ngx 定義了名為 ngx_use_accept_mutex 的全局變量。他是負載均衡的關鍵。

這個變量在每個工作進程初始化時,完成初始化。

只有多進程環境下,並開啟了負載均衡才將 ngx_use_accept_mutex = 1

ngx默認開啟負載均衡,用戶若想避免負載均衡實現導致到開銷 可以手動關閉。

若開啟了負載均衡,則不會靜態 添加監聽套接字 到事件監控機制。

ngx_process_events_and_timers()完成 添加 監聽套接字 到監控機制。
本函數根據 進程的負載情況 進行 監聽套接字的 的持有和釋放,若本進程繁忙則,不持有監聽套接字,若空閑,則持有監聽套接字。
監聽套接字的持有 和 釋放 需要 鎖機制 實現同步和互斥。
ngx_process_events_and_timers 是位於 工作進程 的 大循環里,所以 持有監聽套接字 是動態的。

ngx_accept_disabled 是記錄當前進程的繁忙程度,若超過最大連接數的7/8,則為過載。
若進程過載,則不再持有監聽套接字。若沒有過載,則嘗試爭得鎖,也就是監聽套接字。

若爭鎖失敗,則將監聽套接字從事件監控中刪除(若以前已經過監聽套接字),若爭鎖成功,則添加監聽套接字到事件監控(若以前沒有得到監聽套接字)。

如下若, ngx_accept_mutex_held 表示 是否持有鎖。

  320 ngx_int_t
  321 ngx_trylock_accept_mutex(ngx_cycle_t *cycle)
  322 {
  323     if (ngx_shmtx_trylock(&ngx_accept_mutex)) {
  324
  325         ngx_log_debug0(NGX_LOG_DEBUG_EVENT, cycle->log, 0,
  326                        "accept mutex locked");
  327
  328         if (ngx_accept_mutex_held && ngx_accept_events == 0) {
  329             return NGX_OK;
  330         }
  331
  332         if (ngx_enable_accept_events(cycle) == NGX_ERROR) {
  333             ngx_shmtx_unlock(&ngx_accept_mutex);
  334             return NGX_ERROR;
  335         }
  336
  337         ngx_accept_events = 0;
  338         ngx_accept_mutex_held = 1;
  339
  340         return NGX_OK;
  341     }
  342
  343     ngx_log_debug1(NGX_LOG_DEBUG_EVENT, cycle->log, 0,
  344                    "accept mutex lock failed: %ui", ngx_accept_mutex_held);
  345
  346     if (ngx_accept_mutex_held) {
  347         if (ngx_disable_accept_events(cycle, 0) == NGX_ERROR) {
  348             return NGX_ERROR;
  349         }
  350
  351         ngx_accept_mutex_held = 0;
  352     }
  353
  354     return NGX_OK;
  355 }

若持有鎖,則將 flag 變量添加 NGX_POST_EVENT標記。表示將所有事件延后處理。
因為 持有鎖的,應該盡快使用資源,盡快釋放鎖,所以 將所有事件延后處理,可以對事件進行優先級排序,以盡快處理 鎖相關事件,完成后立即釋放鎖,再處理其他事件。
沒有 持有鎖的,將 事件阻塞 設置較短時間,以盡快跳出阻塞,嘗試獲得鎖。

若持有鎖,將事件添加到鏈表,延遲處理,這里分了兩個類別的事件,監聽套接字事件(加入ngx_posted_accept_events鏈表),其他事件(加入ngx_posted_events鏈表)

ngx_process_events() 已經將事件緩存,若有監聽套接字事件,則快於其他事件處理,完成后立即釋放鎖。

5. 多核綁定

為了提高cache命中率, ngx 提供 worker_cpu_affinity 指令,提高cpu親和度。
使用 ps -F 選項可以看到 進程使用的cpu,PSR域

6. 超時管理

對於有些事件需進行超時管理,即 等待的事件沒有在指定時間到達,就需要對這些情況做些處理。
如,客戶端請求建立連接后,ngx等待讀取報文頭,但可能一直沒有獲得報文頭,則應該認為這是非法請求,直接返回錯誤信息404,並釋放資源。

超時管理有兩個要點:

  1. 超時事件對象的組織,ngx使用紅黑樹。
  2. 超時事件對象的超時檢測。有兩種方法:
    (1) 設置一個定時器,每隔一段時間遍歷樹
    (2) 根據當前最早超時時間,設置一個定時器。

ngx為事件對象添加了如下超時屬性

timedout 表示是否已經超時
timer_set 表示是否已經加入樹,進行超時監控。
timer 是紅黑樹的節點

為了遍歷樹,ngx每個工作定義了如下變量。

ngx_event_timer_rbtree 是樹對象,可以得到根節點。
ngx_event_timer_sentinel 是哨兵節點

初始化紅黑樹是在 ngx_event_process_init 內,所以每個工作進程有自己的樹。

對於每個新建立的連接,會將connect對象加入超時管理
ngx_http_init_connection()

將事件對象加入超時管理,設置超時時間 c->listening->post_accept_timeout 。

ngx_add_timer() 完成將一個超時管理加入紅黑樹,首先比較key字段記錄的超時時刻,判斷超時事件是否已經加入樹,若已經加入則調用 ngx_del_timer() 刪除節點,在調用 ngx_rbtree_insert() 將超時事件對象加入樹。

對於樹上超時節點的管理有兩種方法,
使用哪種方法取決於配置指令 timer_resolution

當 ngx_timer_resolution 不為0,則使用方案1(周期檢查超時)

可以看出設置了兩個變量:
flags 為 0 ,表示沒有附加邏輯
timer 為 NGX_TIMER_INFINITE, 表示 事件阻塞為永久,
當 事件阻塞為永久時,由於 ngx 設置了定時器,所以能實現周期檢查超時。

ngx_event_process_init() 設置了定時器

SIGALRM 的回調函數會設置 ngx_event_timer_alarm = 1。

由於 ngx_event_timer_alarm 為 1 , 所以會調用 ngx_time_update()

由於更新了時間,所以 ngx_event_expire_timers() 會執行

方案2
將 timer設置為最快方法超時事件的值,具體在 ngx_event_find_timer() ,用這個值設置 事件阻塞的超時時間,由於 flag 為 NGX_UPDATE_TIME,所以 ngx_time_update() 會執行,所以每次事件處理都會更新時間,若 客戶端請求頻率高,則導致高頻率調用 gettimeofday() ,這是方案2的缺點。

那么如何管理超時事件節點?
由於使用紅黑樹,所以不需要遍歷所有節點,只需要找到最近即將超時的對象,判斷是否超時,若超時將其移出樹,並設置超時標記(ev->timeout = 1),並調用超時回調函數。再處理下一個即將超時的節點,直到某個節點未超時 ,所有檢測完畢。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM