亚洲国产成人精品在线播放_日韩第一页在线观看_人人插人人摸精品在线视频_日韩黄色成人电影_国产人成无码视频在线观看_国产91热爆ts人妖系列_免费观看欧美一级_午夜影院操一操黄片_午夜大片免费爽爽爽影院_日本少妇中文三级

聯(lián)系我們

公司地址: 上海市滬宜公路1188號(hào)4號(hào)樓
     一層
聯(lián)系電話:021-31080981
電子郵箱:soline@soline.com.cn
郵政編碼:201802

Nginx解決方案

驚群效應(yīng)結(jié)論

  • 不管是多進(jìn)程還是多線程,都存在驚群效應(yīng),本篇文章使用多進(jìn)程分析。

  • 在Linux2.6版本之后,已經(jīng)解決了系統(tǒng)調(diào)用accept的驚群效應(yīng)(前提是沒(méi)有使用select、poll、epoll等事件通知機(jī)制)。

  • 目前Linux已經(jīng)部分解決了epoll的驚群效應(yīng)(epoll在fork之前),Linux2.6是沒(méi)有解決的。

  • epoll在fork之后創(chuàng)建仍然存在驚群效應(yīng),Nginx使用自己實(shí)現(xiàn)的互斥鎖解決驚群效應(yīng)。

驚群效應(yīng)是什么

驚群效應(yīng)(thundering herd)是指多進(jìn)程(多線程)在同時(shí)阻塞等待同一個(gè)事件的時(shí)候(休眠狀態(tài)),如果等待的這個(gè)事件發(fā)生,那么他就會(huì)喚醒等待的所有進(jìn)程(或者線程),但是最終卻只能有一個(gè)進(jìn)程(線程)獲得這個(gè)時(shí)間的“控制權(quán)”,對(duì)該事件進(jìn)行處理,而其他進(jìn)程(線程)獲取“控制權(quán)”失敗,只能重新進(jìn)入休眠狀態(tài),這種現(xiàn)象和性能浪費(fèi)就叫做驚群效應(yīng)。

驚群效應(yīng)消耗了什么

  • Linux內(nèi)核對(duì)用戶進(jìn)程(線程)頻繁地做無(wú)效的調(diào)度、上下文切換等使系統(tǒng)性能大打折扣。上下文切換(context switch)過(guò)高會(huì)導(dǎo)致cpu像個(gè)搬運(yùn)工,頻繁地在寄存器和運(yùn)行隊(duì)列之間奔波,更多的時(shí)間花在了進(jìn)程(線程)切換,而不是在真正工作的進(jìn)程(線程)上面。直接的消耗包括cpu寄存器要保存和加載(例如程序計(jì)數(shù)器)、系統(tǒng)調(diào)度器的代碼需要執(zhí)行。間接的消耗在于多核cache之間的共享數(shù)據(jù)。

  • 為了確保只有一個(gè)進(jìn)程(線程)得到資源,需要對(duì)資源操作進(jìn)行加鎖保護(hù),加大了系統(tǒng)的開(kāi)銷。目前一些常見(jiàn)的服務(wù)器軟件有的是通過(guò)鎖機(jī)制解決的,比如Nginx(它的鎖機(jī)制是默認(rèn)開(kāi)啟的,可以關(guān)閉);還有些認(rèn)為驚群對(duì)系統(tǒng)性能影響不大,沒(méi)有去處理,比如Lighttpd。

Linux解決方案之Accept

  • Linux 2.6版本之前,監(jiān)聽(tīng)同一個(gè)socket的進(jìn)程會(huì)掛在同一個(gè)等待隊(duì)列上,當(dāng)請(qǐng)求到來(lái)時(shí),會(huì)喚醒所有等待的進(jìn)程。

  • Linux 2.6版本之后,通過(guò)引入一個(gè)標(biāo)記位WQ_FLAG_EXCLUSIVE,解決掉了Accept驚群效應(yīng)。

具體分析會(huì)在代碼注釋里面,accept代碼實(shí)現(xiàn)片段如下:

// 當(dāng)accept的時(shí)候,如果沒(méi)有連接則會(huì)一直阻塞(沒(méi)有設(shè)置非阻塞)// 其阻塞函數(shù)就是:inet_csk_accept(accept的原型函數(shù))  
struct sock *inet_csk_accept(struct sock *sk, int flags, int *err){
    ...  
    // 等待連接 
    error = inet_csk_wait_for_connect(sk, timeo); 
    ...  }static int inet_csk_wait_for_connect(struct sock *sk, long timeo){
    ...
    for (;;) {  
        // 只有一個(gè)進(jìn)程會(huì)被喚醒。        // 非exclusive的元素會(huì)加在等待隊(duì)列前頭,exclusive的元素會(huì)加在所有非exclusive元素的后頭。        prepare_to_wait_exclusive(sk_sleep(sk), &wait,TASK_INTERRUPTIBLE);  
    }  
    ...}void prepare_to_wait_exclusive(wait_queue_head_t *q, wait_queue_t *wait, int state)  {
    unsigned long flags;  
    // 設(shè)置等待隊(duì)列的flag為EXCLUSIVE,設(shè)置這個(gè)就是表示一次只會(huì)有一個(gè)進(jìn)程被喚醒,我們等會(huì)就會(huì)看到這個(gè)標(biāo)記的作用。  
    // 注意這個(gè)標(biāo)志,喚醒的階段會(huì)使用這個(gè)標(biāo)志。    wait->flags |= WQ_FLAG_EXCLUSIVE;  
    spin_lock_irqsave(&q->lock, flags);  
    if (list_empty(&wait->task_list))  
    // 加入等待隊(duì)列  
    __add_wait_queue_tail(q, wait);  
    set_current_state(state);  
    spin_unlock_irqrestore(&q->lock, flags);  }

喚醒阻塞的accept代碼片段如下:

// 當(dāng)有tcp連接完成,就會(huì)從半連接隊(duì)列拷貝socket到連接隊(duì)列,這個(gè)時(shí)候我們就可以喚醒阻塞的accept了。int tcp_v4_do_rcv(struct sock *sk, struct sk_buff *skb){
    ...
    // 關(guān)注此函數(shù)    if (tcp_child_process(sk, nsk, skb)) { 
        rsk = nsk;  
        goto reset;  
    }
    ...}int tcp_child_process(struct sock *parent, struct sock *child, struct sk_buff *skb){
    ...
    // Wakeup parent, send SIGIO 喚醒父進(jìn)程    if (state == TCP_SYN_RECV && child->sk_state != state)  
        // 調(diào)用sk_data_ready通知父進(jìn)程        // 查閱資料我們知道tcp中這個(gè)函數(shù)對(duì)應(yīng)是sock_def_readable        // 而sock_def_readable會(huì)調(diào)用wake_up_interruptible_sync_poll來(lái)喚醒隊(duì)列        parent->sk_data_ready(parent, 0);  
    }
    ...}void __wake_up_sync_key(wait_queue_head_t *q, unsigned int mode, int nr_exclusive, void *key)  {  
    ...  
    // 關(guān)注此函數(shù)    __wake_up_common(q, mode, nr_exclusive, wake_flags, key);  
    spin_unlock_irqrestore(&q->lock, flags);  
    ...  }static void __wake_up_common(wait_queue_head_t *q, unsigned int mode, int nr_exclusive, int wake_flags, void *key){
    ...
    // 傳進(jìn)來(lái)的nr_exclusive是1    // 所以flags & WQ_FLAG_EXCLUSIVE為真的時(shí)候,執(zhí)行一次,就會(huì)跳出循環(huán)    // 我們記得accept的時(shí)候,加到等待隊(duì)列的元素就是WQ_FLAG_EXCLUSIVE的    list_for_each_entry_safe(curr, next, &q->task_list, task_list) {  
        unsigned flags = curr->flags;  
        if (curr->func(curr, mode, wake_flags, key) 
        && (flags & WQ_FLAG_EXCLUSIVE) && !--nr_exclusive)
        break; 
    }
    ...}

Linux解決方案之Epoll

在使用select、poll、epoll、kqueue等IO復(fù)用時(shí),多進(jìn)程(線程)處理鏈接更加復(fù)雜。

因此在討論epoll的驚群效應(yīng)時(shí)候,需要分為兩種情況:

epoll_create在fork之前創(chuàng)建epoll_create在fork之后創(chuàng)建

epoll_create在fork之前創(chuàng)建

與accept驚群的原因類似,當(dāng)有事件發(fā)生時(shí),等待同一個(gè)文件描述符的所有進(jìn)程(線程)都將被喚醒,而且解決思路和accept一致。

為什么需要全部喚醒?因?yàn)閮?nèi)核不知道,你是否在等待文件描述符來(lái)調(diào)用accept()函數(shù),還是做其他事情(信號(hào)處理,定時(shí)事件)。

此種情況驚群效應(yīng)已經(jīng)被解決。

epoll_create在fork之后創(chuàng)建

epoll_create在fork之前創(chuàng)建的話,所有進(jìn)程共享一個(gè)epoll紅黑數(shù)。

如果我們只需要處理accept事件的話,貌似世界一片美好了。但是epoll并不是只處理accept事件,accept后續(xù)的讀寫(xiě)事件都需要處理,還有定時(shí)或者信號(hào)事件。

當(dāng)連接到來(lái)時(shí),我們需要選擇一個(gè)進(jìn)程來(lái)accept,這個(gè)時(shí)候,任何一個(gè)accept都是可以的。當(dāng)連接建立以后,后續(xù)的讀寫(xiě)事件,卻與進(jìn)程有了關(guān)聯(lián)。一個(gè)請(qǐng)求與a進(jìn)程建立連接后,后續(xù)的讀寫(xiě)也應(yīng)該由a進(jìn)程來(lái)做。

當(dāng)讀寫(xiě)事件發(fā)生時(shí),應(yīng)該通知哪個(gè)進(jìn)程呢?epoll并不知道,因此,事件有可能錯(cuò)誤通知另一個(gè)進(jìn)程,這是不對(duì)的。所以一般在每個(gè)進(jìn)程(線程)里面會(huì)再次創(chuàng)建一個(gè)epoll事件循環(huán)機(jī)制,每個(gè)進(jìn)程的讀寫(xiě)事件只注冊(cè)在自己進(jìn)程的epoll種。

我們知道epoll對(duì)驚群效應(yīng)的修復(fù),是建立在共享在同一個(gè)epoll結(jié)構(gòu)上的。epoll_create在fork之后執(zhí)行,每個(gè)進(jìn)程有單獨(dú)的epoll紅黑樹(shù),等待隊(duì)列,ready事件列表。因此,驚群效應(yīng)再次出現(xiàn)了。有時(shí)候喚醒所有進(jìn)程,有時(shí)候喚醒部分進(jìn)程,可能是因?yàn)槭录呀?jīng)被某些進(jìn)程處理掉了,因此不用在通知另外還未通知到的進(jìn)程了。

Nginx解決方案之鎖的設(shè)計(jì)

首先我們要知道在用戶空間進(jìn)程間鎖實(shí)現(xiàn)的原理,起始原理很簡(jiǎn)單,就是能弄一個(gè)讓所有進(jìn)程共享的東西,比如mmap的內(nèi)存,比如文件,然后通過(guò)這個(gè)東西來(lái)控制進(jìn)程的互斥。

Nginx中使用的鎖是自己來(lái)實(shí)現(xiàn)的,這里鎖的實(shí)現(xiàn)分為兩種情況,一種是支持原子操作的情況,也就是由NGX_HAVE_ATOMIC_OPS這個(gè)宏來(lái)進(jìn)行控制的,一種是不支持原子操作,這是是使用文件鎖來(lái)實(shí)現(xiàn)。

鎖結(jié)構(gòu)體

如果支持原子操作,則我們可以直接使用mmap,然后lock就保存mmap的內(nèi)存區(qū)域的地址。

如果不支持原子操作,則我們使用文件鎖來(lái)實(shí)現(xiàn),這里fd表示進(jìn)程間共享的文件句柄,name表示文件名。

typedef struct {  
    #if (NGX_HAVE_ATOMIC_OPS)  
        ngx_atomic_t  *lock;  
    #else  
        ngx_fd_t       fd;  
        u_char        *name;  
    #endif  
} ngx_shmtx_t;

原子鎖創(chuàng)建

// 如果支持原子操作的話,非常簡(jiǎn)單,就是將共享內(nèi)存的地址付給loc這個(gè)域ngx_int_t ngx_shmtx_create(ngx_shmtx_t *mtx, void *addr, u_char *name)  {  
    mtx->lock = addr;
    return NGX_OK;  }

原子鎖獲取

trylock它是非阻塞的,也就是說(shuō)它會(huì)嘗試的獲得鎖,如果沒(méi)有獲得的話,它會(huì)直接返回錯(cuò)誤。

lock它也會(huì)嘗試獲得鎖,而當(dāng)沒(méi)有獲得他不會(huì)立即返回,而是開(kāi)始進(jìn)入循環(huán)然后不停的去獲得鎖,知道獲得。不過(guò)Nginx這里還有用到一個(gè)技巧,就是每次都會(huì)讓當(dāng)前的進(jìn)程放到cpu的運(yùn)行隊(duì)列的最后一位,也就是自動(dòng)放棄cpu。

原子鎖實(shí)現(xiàn)

如果系統(tǒng)庫(kù)支持的情況,此時(shí)直接調(diào)用OSAtomicCompareAndSwap32Barrier即CAS。

#define ngx_atomic_cmp_set(lock, old, new)                                   
    OSAtomicCompareAndSwap32Barrier(old, new, (int32_t *) lock)

如果系統(tǒng)庫(kù)不支持這個(gè)指令的話,Nginx自己還用匯編實(shí)現(xiàn)了一個(gè)。

static ngx_inline ngx_atomic_uint_t ngx_atomic_cmp_set(ngx_atomic_t *lock, ngx_atomic_uint_t old,  
    ngx_atomic_uint_t set)  {  
    u_char  res;

    __asm__ volatile (

         NGX_SMP_LOCK  
    "    cmpxchgl  %3, %1;   "  
    "    sete      %0;       "

    : "=a" (res) : "m" (*lock), "a" (old), "r" (set) : "cc", "memory");

    return res;  }

原子鎖釋放

unlock比較簡(jiǎn)單,和當(dāng)前進(jìn)程id比較,如果相等,就把lock改為0,說(shuō)明放棄這個(gè)鎖。

#define ngx_shmtx_unlock(mtx) 
    (void) ngx_atomic_cmp_set((mtx)->lock, ngx_pid, 0)

Nginx解決方案之驚群效應(yīng)

變量分析

// 如果使用了master worker,并且worker個(gè)數(shù)大于1 // 同時(shí)配置文件里面有設(shè)置使用accept_mutex.的話,設(shè)置ngx_use_accept_mutex  
 if (ccf->master && ccf->worker_processes > 1 && ecf->accept_mutex) 
 { 
    ngx_use_accept_mutex = 1;  
    // 下面這兩個(gè)變量后面會(huì)解釋。  
    ngx_accept_mutex_held = 0;  
    ngx_accept_mutex_delay = ecf->accept_mutex_delay;  
 } else {  
    ngx_use_accept_mutex = 0;  
 }ngx_use_accept_mutex:這個(gè)變量,如果有這個(gè)變量,說(shuō)明nginx有必要使用accept互斥體,這個(gè)變量的初始化在ngx_event_process_init中。ngx_accept_mutex_held:表示當(dāng)前是否已經(jīng)持有鎖。ngx_accept_mutex_delay:表示當(dāng)獲得鎖失敗后,再次去請(qǐng)求鎖的間隔時(shí)間,這個(gè)時(shí)間可以在配置文件中設(shè)置的。ngx_accept_disabled = ngx_cycle->connection_n / 8  
                              - ngx_cycle->free_connection_n;ngx_accept_disabled:這個(gè)變量是一個(gè)閾值,如果大于0,說(shuō)明當(dāng)前的進(jìn)程處理的連接過(guò)多。

是否使用鎖

// 如果有使用mutex,則才會(huì)進(jìn)行處理。  
if (ngx_use_accept_mutex) {  
    // 如果大于0,則跳過(guò)下面的鎖的處理,并減一。  
    if (ngx_accept_disabled > 0) {  
        ngx_accept_disabled--; 
    } else {  
        // 試著獲得鎖,如果出錯(cuò)則返回。  
        if (ngx_trylock_accept_mutex(cycle) == NGX_ERROR) {  
            return;  
        }  
        // 如果ngx_accept_mutex_held為1,則說(shuō)明已經(jīng)獲得鎖,此時(shí)設(shè)置flag,這個(gè)flag后面會(huì)解釋。        if (ngx_accept_mutex_held) {  
            flags |= NGX_POST_EVENTS;  
        } else {  
            // 否則,設(shè)置timer,也就是定時(shí)器。接下來(lái)會(huì)解釋這段。  
            if (timer == NGX_TIMER_INFINITE  
                 || timer > ngx_accept_mutex_delay) {  
                timer = ngx_accept_mutex_delay;  
            }  
        }  
    }  }// 如果ngx_posted_accept_events不為NULL,則說(shuō)明有accept event需要nginx處理。  
if (ngx_posted_accept_events) {  
        ngx_event_process_posted(cycle, &ngx_posted_accept_events);  }

NGX_POST_EVENTS標(biāo)記,設(shè)置了這個(gè)標(biāo)記就說(shuō)明當(dāng)socket有數(shù)據(jù)被喚醒時(shí),我們并不會(huì)馬上accept或者說(shuō)讀取,而是將這個(gè)事件保存起來(lái),然后當(dāng)我們釋放鎖之后,才會(huì)進(jìn)行accept或者讀取這個(gè)句柄。

如果沒(méi)有設(shè)置NGX_POST_EVENTS標(biāo)記的話,nginx會(huì)立即accept或者讀取句柄。

定時(shí)器這里如果nginx沒(méi)有獲得鎖,并不會(huì)馬上再去獲得鎖,而是設(shè)置定時(shí)器,然后在epoll休眠(如果沒(méi)有其他的東西喚醒).此時(shí)如果有連接到達(dá),當(dāng)前休眠進(jìn)程會(huì)被提前喚醒,然后立即accept。否則,休眠 ngx_accept_mutex_delay時(shí)間,然后繼續(xù)try lock。

獲取鎖來(lái)解決驚群

ngx_int_t ngx_trylock_accept_mutex(ngx_cycle_t *cycle)  { 
    // 嘗試獲得鎖  
    if (ngx_shmtx_trylock(&ngx_accept_mutex)) {  
        // 如果本來(lái)已經(jīng)獲得鎖,則直接返回Ok  
        if (ngx_accept_mutex_held  
            && ngx_accept_events == 0  
            && !(ngx_event_flags & NGX_USE_RTSIG_EVENT))  
        {  
            return NGX_OK;  
        }

        // 到達(dá)這里,說(shuō)明重新獲得鎖成功,因此需要打開(kāi)被關(guān)閉的listening句柄。  
        if (ngx_enable_accept_events(cycle) == NGX_ERROR) {  
            ngx_shmtx_unlock(&ngx_accept_mutex);  
            return NGX_ERROR;  
        }

        ngx_accept_events = 0;  
        // 設(shè)置獲得鎖的標(biāo)記。  
        ngx_accept_mutex_held = 1;

        return NGX_OK;  
    }

    // 如果我們前面已經(jīng)獲得了鎖,然后這次獲得鎖失敗    // 則說(shuō)明當(dāng)前的listen句柄已經(jīng)被其他的進(jìn)程鎖監(jiān)聽(tīng)    // 因此此時(shí)需要從epoll中移出調(diào)已經(jīng)注冊(cè)的listen句柄    // 這樣就很好的控制了子進(jìn)程的負(fù)載均衡  
    if (ngx_accept_mutex_held) {  
        if (ngx_disable_accept_events(cycle) == NGX_ERROR) {  
            return NGX_ERROR;  
        } 
        // 設(shè)置鎖的持有為0.  
        ngx_accept_mutex_held = 0;  
    }

    return NGX_OK;  }

如上代碼,當(dāng)一個(gè)連接來(lái)的時(shí)候,此時(shí)每個(gè)進(jìn)程的epoll事件列表里面都是有該fd的。搶到該連接的進(jìn)程先釋放鎖,在accept。沒(méi)有搶到的進(jìn)程把該fd從事件列表里面移除,不必再調(diào)用accept,造成資源浪費(fèi)。 同時(shí)由于鎖的控制(以及獲得鎖的定時(shí)器),每個(gè)進(jìn)程都能相對(duì)公平的accept句柄,也就是比較好的解決了子進(jìn)程負(fù)載均衡。