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ù)載均衡。
上一篇:利用ELK+Kafka解決方案
下一篇:Apache解決方案