nginx 網絡框架淺析

nginx 事件驅動框架淺析

Nginx的網絡框架集成了各個操作系統的事件框架,這裏主要基於linux 的 epoll 講解。 一個事件處理框架所要解決的問題是如何收集、管理、分發事件。這裏所說的事件,主要以網絡事件和定時器事件爲主,而網絡事件中又以TCP網絡事件爲主。本文主要結合nginx源碼解析整個事件驅動框架的機制。
參考書目《深入理解nginx》。

nginx主循環框架 ngx_cycle_s

nginx 有一個全局的對象 ngx_cycle_s, 這個結構體囊括了所有的核心結構體,並控制這個進程的運行。比如 nginx 的配置文件,所有模塊,以及連接,事件,內存池,日誌等等,都存在 nginx_cycle_s 結構體中。由於這裏只介紹 nginx 的事件驅動機制,因此只列出與之相關的一部分字段。

struct ngx_cycle_s {
    ...
    //ngx_listening_t 動態數組,存儲需要監聽的端口,初始化時會根據數組內容監聽
    ngx_array_t               listening;
    //所有的連接
    ngx_connection_t         *connections;
    //所有註冊的讀事件
    ngx_event_t              *read_events;
    //所有註冊的寫事件
    ngx_event_t              *write_events;
    ...
};

以下是啓動流程
在這裏插入圖片描述

nginx事件 ngx_event_s

一個事件驅動框架包括,事件產生者,事件收集分發者,和事件消費者。

在這裏插入圖片描述

有了事件註冊分發器,還需要對每一個事件進行封裝。事件是註冊和分發的實體,網絡事件也好,定時器事件,信號事件也好,在 nginx 中都抽象成了結構體 ngx_event_s 。這裏列出了結構體一部分的字段和含義。

struct ngx_event_s {
    /*data通常指向一個ngx_connection_t連接對象。開啓文件異步I/O時,它可能會指向ngx_event_aio_t結構體*/
    void            *data;
    
    /*是否可寫標誌位。通常情況下表示對應的TCP連接目前狀態是可寫的,也就是連接處於可以發送網絡包的狀態*/
    unsigned         write:1;
    
    /*是否可接受連接標誌位。通常情況下,在ngx_cycle_t中的
listening動態數組中,每一個監聽對象ngx_listening_t對應的讀事件中的accept標誌位纔會是1*/
    unsigned         accept:1;

    /*當前事件是否過期標誌位。僅用於事件驅動模塊,事件消費模塊可不用關心。爲什麼需要這個標誌位呢?當開instance標誌位來避免處理後面的已經過期的事件。在9.6節中,將詳細描述ngx_epoll_module是如何使用instance標誌位區分過期事件的,這是一個巧妙的設計方法*/
    unsigned         instance:1;

    /*是否活躍標誌位。這個狀態對應着事件驅動模塊處理方式的不同。例如,在添加事件、刪除事件和處理事件時,active標誌位的不同都會對應着不同的處理方式。在使用事件時,一般不會直接改變active標誌位*/
    unsigned         active:1;
...
    /*最核心的事件處理函數:該回調函數由每個事件消費模塊自己實現。
    typedef void (*ngx_event_handler_pt)(ngx_event_t *ev);
    */
    ngx_event_handler_pt  handler;
....
};

nginx 爲了提高性能,讀寫事件都是預先生成的。在啓動過程中,會在 ngx_cycle_t 結構體中預先分配好讀事件和寫事件,存儲在 ngx_cycle_t->read_events 和 ngx_cycle_t->write_events 中。事實上,不僅僅是事件,連連接也是預先分配的,每一個連接會自動對應一個讀事件和一個寫事件。這個對應關係在後面會講到。
事件添加和刪除操作,可以調用事件模塊中的 add() 和 del() 來實現,但是並不推薦這樣做,因爲,事件類型的不同,事件模塊使用的add() del() 等操作也不一樣。nginx 提供了更簡單和簡潔的操作接口,會屏蔽掉底層事件的差異,大部分場景使用這2個接口就可以了。

//將讀事件註冊要事件驅動模塊中
ngx_int_t ngx_handle_read_event(ngx_event_t *rev, ngx_uint_t flags);
//將寫事件註冊要事件驅動模塊中
ngx_int_t ngx_handle_write_event(ngx_event_t *wev, size_t lowat);

nginx連接 ngx_connection_s

連接分爲2類,一類是被動連接,也就是服務端監聽時,accept() 收到的連接,屬於服務端連接,nginx 中用 ngx_connection_s 表示。一類是主動連接,就是 connect() 獲得的連接,屬於客戶端連接,用 ngx_peer_connection_s 。連接和事件的關係前文說了,每個連接都會對應一個讀事件和寫事件。那麼連接池如何和讀寫事件對應起來呢?答案是使用數組。連接池,讀事件,寫事件由3個大小相同的數組存儲,取不同數組的相同下標即可獲得一個連接以及對應的讀寫事件。

在這裏插入圖片描述

nginx事件模塊 ngx_event_module_s

事件模塊是對不同的操作系統事件驅動框架的抽象。因爲不同的系統平臺提供的事件驅動API不一樣,比如linux 下有 select/poll/epoll,macOS下有 kqueue, windows下是IOCP。ngx_event_module_s 提供了統一的模板,即所有的事件框架都是一個 ngx_event_module_s 類型的變量。

typedef struct {
    //事件模塊的名稱,比如 epoll/select 等
    ngx_str_t *name;
    //create_conf和init_conf方法的調用可參見圖9-3
    //在解析配置項前,這個回調方法用於創建存儲配置項參數的結構體
    void *(*create_conf)(ngx_cycle_t *cycle); 
    //在解析配置項完成後,init_conf方法會被調用,用以綜合處理當前事件模塊感興趣的全部配置項
    char *(*init_conf)(ngx_cycle_t cycle, void conf); 
    //對於不同的事件驅動框架需要實現的10個抽象方法
    ngx_event_actions_t actions;
} ngx_event_module_t;

每個事件驅動框架需要自定義對事件的 actions。ngx_event_actions_t 是一組操作集,包括對事件的10種操作。

typedef struct {
    /*添加事件方法,它將負責把1個感興趣的事件添加到操作系統提供的事件驅動機制(如 epoll、kqueue等)中,這樣,在事件發生後,將可以在調用下面的process_events時獲取這個事件*/
    ngx_int_t  (*add)(ngx_event_t *ev, ngx_int_t event, ngx_uint_t flags);
    /*刪除事件方法,它將把1個已經存在於事件驅動機制中的事件移除,這樣以後即使這個事件發生,調用process_events方法時也無法再獲取這個事件*/
    ngx_int_t  (*del)(ngx_event_t *ev, ngx_int_t event, ngx_uint_t flags);
    /*啓用1個事件,目前事件框架不會調用這個方法,大部分事件驅動模塊對於該方法的實現都是與上面的add方法完全一致的*/
    ngx_int_t  (*enable)(ngx_event_t *ev, ngx_int_t event, ngx_uint_t flags);
    /*禁用1個事件,目前事件框架不會調用這個方法,大部分事件驅動模塊對於該方法的實現都是與上面的del方法完全一致的*/
    ngx_int_t  (*disable)(ngx_event_t *ev, ngx_int_t event, ngx_uint_t flags);
    /*向事件驅動機制中添加一個新的連接*/
    ngx_int_t  (*add_conn)(ngx_connection_t *c);
    /*從事件驅動機制中移除一個連接的讀寫事件*/
    ngx_int_t  (*del_conn)(ngx_connection_t *c, ngx_uint_t flags);

    /*僅在多線程環境下會被調用。目前Nginx不會以多線程方式運行,忽略*/
    ngx_int_t  (*notify)(ngx_event_handler_pt handler);
    
    /*在正常的工作循環中,將通過調用process_events方法來處理事件。這個方法僅在ngx_process_events_and_timers方法中調用,它是處理、分發事件的核心*/
    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;

epoll事件模塊 ngx_epoll_module

瞭解 epoll 的都知道,epoll 編程中,需要事先生成一個 epoll_fd 的對象,然後在一個循環裏裏不斷調用 epoll_wait() 來獲取就緒的事件,並分發到不同的消費者上去處理。而每當有新的事件需要關注時,就繼續往 epoll_fd 註冊,這個系統生成的 epoll_fd 句柄實際上就是一個事件的收集器和分發器。所有的事件需要通過 epoll_fd 才能註冊到底層的網絡硬件上,而當網卡或磁盤等硬件接受了事件的發生時,就會回調給操作系統,操作系統的再通過 epoll_wait() 返回給應用層,通知就緒的事件。

//生成一個 epoll 對象,該對象負責收集事件,分發事件
int epoll_fd = epoll_create();
//向事件分發器註冊感興趣的事件
epoll_ctnl(epoll_fd, events...);
//在主循環中等待事件的到來,並分發給不同的事件消費者
while(1)
{
    //等到事件的到來,有事件的時候就會返回
    int nfds = epoll_wait(epoll_fd, events...);
    //遍歷就緒的事件
    for (int f=0;f<nfds;f++)
    {   //將返回的事件分發給不同的消費者去處理
        handle_every_event(events..);
    }
}

epoll事件模塊,基於上述的 ngx_event_module_s 結構實現了對 epoll 的封裝。

ngx_event_module_t  ngx_epoll_module_ctx = {
    &epoll_name,
    ngx_epoll_create_conf,               /* create configuration */
    ngx_epoll_init_conf,                 /* init configuration */

    {
        ngx_epoll_add_event,             /* add an event */
        ngx_epoll_del_event,             /* delete an event */
        ngx_epoll_add_event,             /* enable an event */
        ngx_epoll_del_event,             /* disable an event */
        ngx_epoll_add_connection,        /* add an connection */
        ngx_epoll_del_connection,        /* delete an connection */
        ngx_epoll_process_events,        /* process the events */
        //init其實就是調用 epoll_create()
        ngx_epoll_init,                  /* init the events */
        ngx_epoll_done,                  /* done the events */
    }
};

大部分對事件增刪改操作都是通過 epoll_cntl() 調用實現的,而init操作則是調用的 epoll_create(), 這裏最重要的是 ngx_epoll_process_events,裏面調用的是 epoll_wait(), 對事件的處理和分發都是在這個函數中進行的。

static ngx_int_t
ngx_epoll_process_events(ngx_cycle_t *cycle, ngx_msec_t timer, ngx_uint_t flags)
{
    int                events;
    uint32_t           revents;
    ngx_int_t          instance, i;
    ngx_uint_t         level;
    ngx_err_t          err;
    ngx_event_t       *rev, *wev;
    ngx_queue_t       *queue;
    ngx_connection_t  *c;
    /* NGX_TIMER_INFINITE == INFTIM */
    ...
    events = epoll_wait(ep, event_list, (int) nevents, timer);
    ...
    //更新緩存時間
    if (flags & NGX_UPDATE_TIME || ngx_event_timer_alarm) {
        ngx_time_update();
    }
    for (i = 0; i < events; i++) {
        //取出事件對應的連接
        c = event_list[i].data.ptr;
        instance = (uintptr_t) c & 1;
        c = (ngx_connection_t *) ((uintptr_t) c & (uintptr_t) ~1);
        rev = c->read;
        //判斷是否過期
        if (c->fd == -1 || rev->instance != instance) {
            continue;
        }
        revents = event_list[i].events;
        if (revents & (EPOLLERR|EPOLLHUP)) {
            ngx_log_debug2(NGX_LOG_DEBUG_EVENT, cycle->log, 0,
                           "epoll_wait() error on fd:%d ev:%04XD",
                           c->fd, revents);
        }
        if ((revents & (EPOLLERR|EPOLLHUP))
             && (revents & (EPOLLIN|EPOLLOUT)) == 0)
        {
        /*
         * if the error events were returned without EPOLLIN or EPOLLOUT,
         * then add these flags to handle the events at least in one
         * active handler
         */
            revents |= EPOLLIN|EPOLLOUT;
        }

        if ((revents & EPOLLIN) && rev->active) {

#if (NGX_HAVE_EPOLLRDHUP)
            if (revents & EPOLLRDHUP) {
                rev->pending_eof = 1;
            }
#endif

            rev->ready = 1;

            if (flags & NGX_POST_EVENTS) {
                queue = rev->accept ? &ngx_posted_accept_events
                                    : &ngx_posted_events;

                ngx_post_event(rev, queue);

            } else {
                rev->handler(rev);
            }
        }

        wev = c->write;

        if ((revents & EPOLLOUT) && wev->active) {

            if (c->fd == -1 || wev->instance != instance) {

                /*
                 * the stale event from a file descriptor
                 * that was just closed in this iteration
                 */

                ngx_log_debug1(NGX_LOG_DEBUG_EVENT, cycle->log, 0,
                               "epoll: stale event %p", c);
                continue;
            }

            wev->ready = 1;
#if (NGX_THREADS)
            wev->complete = 1;
#endif

            if (flags & NGX_POST_EVENTS) {
                ngx_post_event(wev, &ngx_posted_events);

            } else {
                wev->handler(wev);
            }
        }
    }

    return NGX_OK;
}

nginx 對過期事件的處理

什麼是過期事件?加入 epoll_wait() 一共返回了3個事件,在第一個事件的處理過程中,由於業務需要,關閉了某個連接,而該連接正好對應第3個事件,此時處理第3個事件時,該事件就已經過期了。繼續處理必然會出錯。因此我們必須確定當前連接上的事件是否過期。
那有個很簡單的方法,比如將連接的 fd 置爲-1不就可以了?這樣處理也還是有問題。
加入這個被關閉的連接對應的fd 是20, 當第一個連接處理完畢,將fd=20的連接關閉了,並置爲fd=-1, 由於連接是被複用的, fd=-1的連接會被還給連接池, 然後第2個事件處理過程中需要新建一個連接, 又從連接池中取出了剛剛被還回去的連接, 建立的新連接恰好分配的fd又是20。此時到第3個事件處理的過程中,會認爲連接始終沒有斷開過。而實際上舊的連接已經沒有了。這樣在新的連接上繼續處理事件是會有問題的。
因此需要一個標誌位來標識事件和連接是否過期。事件中 ngx_event_s->instance 字段就是這個作用。在連接中也需要一個標誌位。但是nginx 並沒有在 ngx_connection_s 結構上加一個 instance 標誌位。而是通過一個取巧的辦法來區分新舊連接的。由於CPU地址總是2的倍數,因此指針的取值,最後一位一定是0,nginx 就是用地址的最後一位來記錄連接是否過期的。

  1. 在 ngx_event_core_module 初始化過程中會預分配連接池和讀寫事件,此時將過期標誌位初始化位 1
//初始化事件循環
static ngx_int_t
ngx_event_process_init(ngx_cycle_t *cycle)
{   ...
    //預分配連接池
    cycle->connections = ngx_alloc(sizeof(ngx_connection_t) * cycle->connection_n, cycle->log);
    //預分配讀事件
    cycle->read_events = ngx_alloc(sizeof(ngx_event_t) * cycle->connection_n, cycle->log);
    rev = cycle->read_events;
    for (i = 0; i < cycle->connection_n; i++) {
        rev[i].closed = 1;
        //過期標誌位置1
        rev[i].instance = 1;
    }
    ...
    return NGX_OK;
}
  1. 註冊事件時, 將連接最後一位置爲事件的標誌位。
static ngx_int_t
ngx_epoll_add_event(ngx_event_t *ev, ngx_int_t event, ngx_uint_t flags)
{
    ngx_connection_t    *c;
    struct epoll_event   ee; //linux epoll 框架的結構體
    c = ev->data;
    ...
    /*data.ptr保存連接的地址。但是地址的最後一位是處理過的,和事件的instance位一致。*/
    ee.data.ptr = (void *) ((uintptr_t) c | ev->instance);
    ...
    return NGX_OK;
}
  1. 每次獲取新連接的時候,將標誌位置反
ngx_connection_t *
ngx_get_connection(ngx_socket_t s, ngx_log_t *log)
{
    ngx_uint_t         instance;
    ngx_event_t       *rev, *wev;
    ngx_connection_t  *c;
    /*獲取空閒連接*/
    c = ngx_cycle->free_connections;
    ngx_cycle->free_connection_n--;
    ...
    rev = c->read;//讀事件
    wev = c->write;//寫事件
    ...
    /*每次獲取連接,將讀寫事件的標誌位置反*/
    instance = rev->instance; 
    rev->instance = !instance;
    wev->instance = !instance;
    ...
    return c;
}
  1. 在事件處理過程中將連接的最後一位 instance 標誌位與事件的標誌位對比,如果不一致說明連接過期了。
static ngx_int_t
ngx_epoll_process_events(ngx_cycle_t *cycle, ngx_msec_t timer, ngx_uint_t flags)
{
    events = epoll_wait(ep, event_list, (int) nevents, timer);
    ...
    for (i = 0; i < events; i++) {
        c = event_list[i].data.ptr;
        //將連接的指針地址最後一位取出來,作爲連接的 instance 標誌位
        instance = (uintptr_t) c & 1;
        //將連接的指針地址最後一位置0,還原連接真正的地址
        c = (ngx_connection_t *) ((uintptr_t) c & (uintptr_t) ~1);

        rev = c->read;
        //事件的標誌位和連接的標誌位不一樣,說明過期了
        if (c->fd == -1 || rev->instance != instance) {
            continue;
        }
    }
    return NGX_OK;
}

總結一下過期事件的判斷流程。

  1. 初始化時,所有連接的地址最後一位默認爲0,讀寫事件的instance=1
  2. 從空閒連接列表中獲取新的可用連接時,將連接地址最後一位置反。
  3. 註冊事件時,將連接地址最後一位設爲事件的instance標誌位。
  4. 處理事件時,取連接地址最後一位,與事件的instance標誌位對比,不一致說明獲取過新的連接,說明舊連接已經不存在,則爲過期事件。(舊連接存在的情況下,不會放回空閒連接列表)

定時器事件

nginx 的定時器事件是自身實現的,與操作系統內核完全無關。nginx 的時間不是實時獲取的,而是取的緩存的時間

typedef struct {
    time_t      sec;
    ngx_uint_t  msec;
    ngx_int_t   gmtoff;
} ngx_time_t;
void ngx_time_init(void);
//執行系統調用 gettimeofday() 更新緩存時間
void ngx_time_update(void);
...

這個緩存時間什麼時候會更新呢?對於worker進程而言,除了Nginx啓動時更新一次時
間外,任何更新時間的操作都只能由ngx_epoll_process_events方法執行。當flags參數中有NGX_UPDATE_TIME標誌位,或者ngx_event_timer_alarm標誌位爲1時,就會調用ngx_time_update方法更新緩存時間。
緩存的時間精度與緩存更新的頻率有關,這個精度是可以配置的,nginx 通過系統調用啓動一個定時器,根據配置的時間間隔來更新更新緩存裏的時間。
所有的定時器事件,nginx 用紅黑樹來保存。紅黑樹的key爲超時時間,value 是一個ngx_event_timer_sentinel結構體,該結構體是ngx_event_t事件中的timer成員。

ngx_thread_volatile ngx_rbtree_t ngx_event_timer_rbtree; 
static ngx_rbtree_node_t ngx_event_timer_sentinel;

這樣,如果需要找出最有可能超時的事件,那麼將ngx_event_timer_rbtree樹中最左邊的節點取出來即可。只要用當前時間去比較這個最左邊節點的超時時間,就會知道這個事件有沒有觸發超時,如果還沒有觸發超時,那麼會知道最少還要經過多少毫秒滿足超時條件而觸發超時。

//初始戶定時器紅黑樹
ngx_int_t ngx_event_timer_init(ngx_log_t *log);
//找到紅黑樹最左邊的節點,如果最左邊的節點超時時間到則返回0,否則返回距離超時的時間差
ngx_msec_t ngx_event_find_timer(void);
//遍歷紅黑樹,處理所有超時的事件。(調用回調函數處理)
void ngx_event_expire_timers(void);
//刪除某個事件
void ngx_event_cancel_timers(void);
//新增超時事件到紅黑樹中
static ngx_inline void ngx_event_add_timer(ngx_event_t *ev, ngx_msec_t timer);
static ngx_inline void ngx_event_del_timer(ngx_event_t *ev);

在這裏插入圖片描述

超時事件對象的超時檢測有兩種方案:

  1. 定時檢測機制,通過設置定時器(通過系統調用),每過一定時間就對紅黑樹管理的所有超時事件進行一次超級掃描並處理超時事件。
  2. 是先計算出距離當前最快發生超時的時間是多久。然後等待這個時間之後再去進行一次超時檢測。
void ngx_process_events_and_timers(ngx_cycle_t *cycle)
{
    ngx_uint_t  flags;
    ngx_msec_t  timer, delta;

    if (ngx_timer_resolution) {
        timer = NGX_TIMER_INFINITE;
        flags = 0;

    } else {
        timer = ngx_event_find_timer();
        flags = NGX_UPDATE_TIME;
    }
    ...
    }

如果配置項ngx_timer_resolution 爲0,則執行方案2,從最近的超時事件開始處理。不爲0的話執行方案1。

nginx驚羣處理和負載均衡算法

驚羣效應和負載不均衡的由來

服務端服務端的併發模型通常有四種,多進程兩種,多線程兩種。

對於多進程模型來說 ( master -> workers ),worker 子進程是 master 通過 fork 調用產生的。一個 socket 建立的過程分爲 socket(),bind(),listen(),accept()。對accept() 的處理可以有兩種方式。

方式一是由父進程統一 listen() ,當有新的連接產生(accept() 有返回) 之後再 fork 一個子進程去處理 accept() 得到的 fd。或者預先fork 出一定數量的子進程,再將 accept() 得到的 fd 通過 RPC 等進程間通信的方式分發給子進程處理。這種方式要求父進程可以併發處理大量的 accpet() 工作,對單個進程的處理能力要求很高。
方式二是由父進程在 fork 之後在子進程中進行 accept(),這樣所有的子進程都在等待客戶端的連接到來。這種方式避免了將accept() 的工作全部壓到父進程上的問題,但是由於最終只會有一個子進程可以成功處理客戶端的連接,而其餘的子進程雖然被喚醒,但是由於沒有搶到 fd 只能再次進入休眠——這種場景成爲"驚羣"效應,會帶來很大的進程間切換的代價。(內核在收到TCP的SYN包時,會激活所有的休眠worker子進程,當然,此時只有最先開始執行accept的子進程可以成功建立新連接,而其他worker子進程都會accept失敗)。同時還有一個弊端就是可能導致子進程之間麗accept() 到的連接不均衡。因爲方式1是有父進程統一分發的,可以使用負載均衡算法,使得分配給各個子進程的連接都是均衡的,而方案2的情況下,各個子進程是不知道其餘子進程的存在的。
nginx 使用的是多進程模型中的第二種。由各個子進程調用accept().
那麼nginx 如果處理驚羣效應和負載均衡問題呢?

驚羣的處理

先來看看nginx的worker進程中一個連接建立的過程。

在這裏插入圖片描述

  1. 首先調用accept方法試圖建立新連接,如果沒有準備好的新連接事件,ngx_event_accept方法會直接返回。
  2. 設置負載均衡閾值ngx_accept_disabled。後面說。
  3. 調用ngx_get_connection方法由連接池中獲取一個ngx_connection_t連接對象。
  4. 爲ngx_connection_t中的pool指針建立內存池。並在連接釋放到空閒連接池時,釋放pool內存池。
  5. 設置socket的屬性,如設爲非阻塞套接字。
  6. 將這個新連接對應的讀事件添加到epoll等事件驅動模塊中,這樣,在這個連接上如果接收到用戶請求epoll_wait,就會收集到這個事件。
  7. 調用監聽對象ngx_listening_t中的handler回調方法。ngx_listening_t結構體的handler回調方法就是當新的TCP連接剛剛建立完成時在這裏調用的。

最後,如果監聽事件的available標誌位爲1,再次循環到第1步,否則ngx_event_accept方法結束。事件的available標誌位對應着multi_accept配置項。當available爲1時,告訴Nginx一次性儘量多地建立新連接,它的實現原理也就在這裏.

nginx 中解決驚羣問題需要打開accept_mutex鎖。解決驚羣問題的實質就是要保證在同一時刻僅能有一個子進程調用accept()。這其實就是進程鍵同步的問題。可以通過進程間的全局鎖來實現。每個worker進程調用accept之前需要先獲取accept_mutex 鎖。

ngx_int_t
ngx_trylock_accept_mutex(ngx_cycle_t *cycle)
{
    /*ngx_shmtx_trylock是非阻塞的調用,返回1表示成功拿到鎖,返回0表示獲取鎖失敗。*/
    if (ngx_shmtx_trylock(&ngx_accept_mutex)) {
        //成功獲取鎖
        ngx_log_debug0(NGX_LOG_DEBUG_EVENT, cycle->log, 0,
                       "accept mutex locked");
        if (ngx_accept_mutex_held && ngx_accept_events == 0) {
            return NGX_OK;
        }
        //禁止accpet新的連接時,釋放鎖。
        if (ngx_enable_accept_events(cycle) == NGX_ERROR) {
            ngx_shmtx_unlock(&ngx_accept_mutex);
            return NGX_ERROR;
        }
        ngx_accept_events = 0;
        ngx_accept_mutex_held = 1;
        return NGX_OK;
    }
    //沒有獲取到鎖時要將ngx_accept_mutex_held置爲0
    if (ngx_accept_mutex_held) {
        if (ngx_disable_accept_events(cycle, 0) == NGX_ERROR) {
            return NGX_ERROR;
        }
        ngx_accept_mutex_held = 0;
    }
    return NGX_OK;
}

如果ngx_trylock_accept_mutex方法沒有獲取到鎖,接下來調用事件驅動模塊的process_events方法時只能處理已有的連接上的事件;如果獲取到了鎖,調用process_events方法時就會既處理已有連接上的事件,也處理新連接的事件。這樣的話,問題又來了,什麼時
候釋放ngx_accept_mutex鎖呢?等到這批事件全部執行完嗎?這當然是不可取的,因爲這個worker進程上可能有許多活躍的連接,處理這些連接上的事件會佔用很長時間,也就是說,會有很長時間都沒有釋放ngx_accept_mutex鎖,這樣,其他worker進程就很難得到處理新連接的機會。
如何解決長時間佔用ngx_accept_mutex鎖的問題呢?這就要依靠ngx_posted_accept_events隊列和ngx_posted_events隊列了。

void
ngx_process_events_and_timers(ngx_cycle_t *cycle)
{
    ...
    //先調用獲取鎖的操作
    if (ngx_trylock_accept_mutex(cycle) == NGX_ERROR) 
        return;
    if (ngx_accept_mutex_held) 
    {/*如果獲取到鎖了,則ngx_accept_mutex_held爲1,此時僅僅將flag置爲NGX_POST_EVENTS,那麼後面在處理事件時,不會立刻調用handle, 而是先將事件放到 ngx_posted_accept_events 和 ngx_posted_events 2個隊列中*/
        flags |= NGX_POST_EVENTS;
    } 
    else 
    {/*如果沒有獲取到鎖,則定時在在某個時間點再次獲取, 但是這個時間不能太長也不能太短。這意味着,即使開啓了timer_resolution時間精度,也需要讓ngx_process_events方法在沒有新事件的時候至少等待ngx_accept_mutex_delay毫秒再去試圖搶鎖。而沒有開啓時間精度時,如果最近一個定時器事件的超時時間距離現在超過了ngx_accept_mutex_delay毫秒的話,也要把timer設置爲ngx_accept_mutex_delay毫秒,這是因爲當前進程雖然沒有搶到accept_mutex鎖,但也不能讓ngx_process_events方法在沒有新事件的時候等待的時間超過ngx_accept_mutex_delay毫秒,這會影響整個負載均衡機制。*/
        if (timer == NGX_TIMER_INFINITE //沒有開啓timer_resolution時間精度
            || timer > ngx_accept_mutex_delay)//開啓了但是超過了ngx_accept_mutex_delay
        {
            timer = ngx_accept_mutex_delay;
        }
    }
    /*調用事件處理函數,也就是 ngx_epoll_process_events, 也就是 epoll_wait() 等待事件
    *由於 flag 位的存在,到來的事件不會立刻被 handle 調用,而是先放到2個隊列中*/
    (void) ngx_process_events(cycle, timer, flags);

    /*先處理ngx_posted_accept_events隊列中事件,也就是建立連接的事件*/
    ngx_event_process_posted(cycle, &ngx_posted_accept_events);
    /*連接建立完之後就可以釋放鎖了。這樣保證了鎖的粒度足夠小。保證了不被普通事件的handler調用佔用時間過久*/
    if (ngx_accept_mutex_held) {
        ngx_shmtx_unlock(&ngx_accept_mutex);
    }
    ...
    //繼續處理普通的事件
    ngx_event_process_posted(cycle, &ngx_posted_events);
}

再看看 ngx_epoll_process_events 中對事件的處理:

static ngx_int_t
ngx_epoll_process_events(ngx_cycle_t *cycle, ngx_msec_t timer, ngx_uint_t flags)
{
    //epoll_wait()...
    if ((revents & EPOLLIN) && rev->active) {
        rev->ready = 1;       
        if (flags & NGX_POST_EVENTS) {
            /*通過 flag 標誌位,將事件先放到 2個隊列中,稍後延遲處理。*/
            queue = rev->accept ? &ngx_posted_accept_events
                                : &ngx_posted_events;
            ngx_post_event(rev, queue);
        } else {
            //沒有置位就直接處理了。
            rev->handler(rev);
        }
    }
}

負載均衡算法

/*
connection_n 是總連接數,free_connection_n是空閒連接數。當空閒連接爲總連接的1/8時,ngx_accept_disabled爲0。也就是已建立連接爲連接池大小的7/8。這個ngx_accept_disabled值每次更新連接池之後會更新,
*/
ngx_accept_disabled = ngx_cycle->connection_n/8 - ngx_cycle->free_connection_n;

/*
當ngx_accept_disabled<0時,也就是連接數還不夠多時(小於連接池大小的7/8),是不會阻止建立新連接的。
當ngx_accept_disabled>0, 表示此時進程的連接數已經達到了某個閾值,就不會再處理新的連接了。而是將ngx_accept_disabled減一,直到ngx_accept_disabled降到連接池大小的7/8以下時,纔會調用ngx_trylock_accept_mutex試圖去處理新連接事件。
*/
if (ngx_accept_disabled > 0) 
{
    ngx_accept_disabled--;
} 
else 
{
    if (ngx_trylock_accept_mutex(cycle) == NGX_ERROR) {
        return;
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章