1. 程式人生 > >菜鳥學習nginx之驚群處理

菜鳥學習nginx之驚群處理

“驚群”這個名詞是我閱讀Nginx時第一次接觸到的,也算是學到了一點點知識吧。

一、驚群

1.1、驚群定義

對於驚群的概念簡單描述一下:通常場景一個埠P1只能被一個程序A監聽,所以埠P1發的事件都會被該程序A所處理。但是,如果程序A通過系統呼叫fork(),建立子程序B,那麼程序B也能夠監聽埠P1。這樣就可以實現多程序監聽同一個埠並且進入阻塞狀態。這樣就引發了一個問題,當客戶端發起TCP連線的時候,那麼到底由誰來負責處理Accept事件呢?總不能多個程序同時處理?最終只能有一個程序來處理Accept事件,也就是說當Accept事件來了,作業系統會把所有程序都喚醒(之前是阻塞狀態),這麼多程序同時去搶佔,搶到程序處理後續流程,沒有搶到的程序繼續阻塞。就是所謂的驚群。

這種方式白白浪費cpu資源,切換程序/執行緒上下文。

1.2、如何解決?

既然在同一時刻只能有一個程序能夠處理,那麼何不加鎖進行同步操作呢?對,這就是Nginx實現的方式,而且這是目前僅有的方式。其實Linux在2.6以後的版本已經完美解決了驚群問題,所以我們在編寫服務端程式時,可以忽律該問題。但是Nginx是跨平臺的一個軟體,為了保證有效性,Nginx自己實現了一套機制。

二、Nginx驚群解決方案

Nginx為了解決驚群問題從兩個方面做了工作:負載均衡和互斥鎖。

2.1、負載均衡

在Nginx中有兩種負載均衡:

類別 作用
程序級負載均衡(前端負載均衡) 主要用於接收客戶端連線,即Accept事件。這個是為了解決驚群問題的一個優化點。
服務級負載均衡(後端負載均衡) 主要用於訪問後臺服務,例如mysql,apache等。這個是我們通常所說的負載均衡。

 Nginx解決驚群相關程式碼如下:

    /* 解決驚群 */
    if (ngx_use_accept_mutex)
    {
        if (ngx_accept_disabled > 0)
        {//實現worker程序間負載均衡
            ngx_accept_disabled--;
        }
        else
        {//解決驚群,通過程序間同步鎖
            if (ngx_trylock_accept_mutex(cycle) == NGX_ERROR)
            {
                return;
            }

            if (ngx_accept_mutex_held)
            {
                flags |= NGX_POST_EVENTS;
            }
            else
            {
                if (timer == NGX_TIMER_INFINITE || timer > ngx_accept_mutex_delay)
                {
                    timer = ngx_accept_mutex_delay;
                }
            }
        }
    }

只有ngx_use_accept_mutex是1時表示開啟負載均衡和驚群處理。 為什麼說負載均衡能夠減少驚群衝突呢?

Nginx內部實現,當一個worker程序已經服務連線數達到7/8*connetctions(最大連線數的八分之七)時,不在處理新的連線事件(Accept事件),也就是說不會去競爭鎖,即不會把listening socket新增到自己的事件驅動中。也就能夠減少驚群衝突。

全域性變數ngx_accept_disabled初始值為負數,當處理一個新的Accept事件則變數就加1。具體程式碼如下:

void
ngx_event_accept(ngx_event_t *ev)
{
...
        /* 負數 */       
        ngx_accept_disabled = ngx_cycle->connection_n / 8
                              - ngx_cycle->free_connection_n;
        c = ngx_get_connection(s, ev->log);//獲取新連線 並且free_connection_n減一
...
}

 對於新的連線請求(Accept事件)處理函式是ngx_event_accept,當成功獲取connection物件後free_connection_n就是減1,其中connection_n始終不變。舉例說明:在Nginx剛啟動完畢時(沒有處理一個新連線)最大處理連線數connection_n=1024,free_connection_n=1024,那麼ngx_accept_disabled=-896(負數,八分之七)。當處理一個新的連線之後,free_connection_n變為1023,那麼ngx_accept_disabled=-895。

2.2、加鎖

上面的負載均衡只是減少衝突的可能性,但是並不能徹底解決問題,因此Nginx通過互斥鎖(Nginx鎖採用的共享記憶體方式)解決驚群問題。其原理是:只有獲取到鎖的那個程序才能接受新的TCP連線事件(Accept事件),具體實現如下:

ngx_int_t
ngx_trylock_accept_mutex(ngx_cycle_t *cycle)
{
    if (ngx_shmtx_trylock(&ngx_accept_mutex)) {//非同步方式 嘗試加鎖 加鎖成功返回1

        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;
        }
        /* 只有獲取到鎖 才能將listen socket 新增到自己的事件驅動中 */
        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_log_debug1(NGX_LOG_DEBUG_EVENT, cycle->log, 0,
                   "accept mutex lock failed: %ui", ngx_accept_mutex_held);
    /**
     * 表示獲取鎖失敗,這個時候有就有兩種場景
     * ngx_accept_mutex_held = 0 表示上一次沒有獲得鎖(非本次) 也就是說該程序
     *   連續兩次獲取鎖失敗
     * ngx_accept_mutex_held = 1 表示上一次獲得鎖但是本次獲得鎖失敗,這個時候需要
     *   將listen socket 移除事件驅動本程序不得繼續accept事件
     */
    if (ngx_accept_mutex_held) {        
        if (ngx_disable_accept_events(cycle, 0)==NGX_ERROR) {//將listen socket移除時間迴圈
            return NGX_ERROR;
        }

        ngx_accept_mutex_held = 0;//修改標誌位
    }

    return NGX_OK;
}

舉例說明:經過這個函式處理之後,程序B獲得了鎖,會把listen socket加入到自己的事件驅動中,以後新連線均由該程序B服務而原先獲得鎖的程序A要把listen socket從自己的事件驅動中刪除。

2.3、什麼場景開啟互斥鎖

開啟互斥鎖有三個條件:

1、Nginx服務模式必須是master/worker模式

2、worker程序數大於1

3、nginx.conf配置檔案開啟,accept_mutex配置,舉例說明如下:

//nginx.conf配置檔案
worker_processes  5;
events {
    worker_connections  1024;
	accept_mutex on;
}

程式碼邏輯判斷如下所示: 

    if (ccf->master && ccf->worker_processes > 1 && ecf->accept_mutex)
    {
        ngx_use_accept_mutex = 1;
        ngx_accept_mutex_held = 0;
        ngx_accept_mutex_delay = ecf->accept_mutex_delay;
    }
    else
    {
        ngx_use_accept_mutex = 0;
    }

 三、疑問

【前提】程序A是上次競爭鎖成功(ngx_accept_mutex_held=1),程序B上次競爭鎖失敗(ngx_accept_mutex_held=0)。

【目前情況】此時程序A和程序B同時競爭鎖,即同時執行ngx_trylock_accept_mutex->ngx_shmtx_trylock。競爭結果是程序A失敗,程序B成功,那麼程序B需要的做的工作是將listen socket加入自己的事件驅動epoll中。

【問題】當程序B把listen socket加到epoll完成後且程序B還沒有把listen socket從epoll中移除,就在這個時候客戶端發起新連線請求(Accept事件),此應該由誰處理呢?

希望有了解的網友能夠留言給我,謝謝。

四、總結

這裡介紹了Nginx解決驚群的原理,後面開始介紹Nginx核心內容--HTTP框架。