微信公眾號:鄭爾多斯
關注可瞭解更多的Nginx
知識。任何問題或建議,請公眾號留言;
關注公眾號,有趣有內涵的文章第一時間送達!
本文章原作者aweth0me,原文地址在這裡點?我啊,我做了簡單的修改和整理
內容主題
本文分析了instance
的作用
什麼是 stale event
If you use an event cache or store all the fd’s returned from epoll_wait(2), then make sure to provide a way to mark its closure dynamically (ie: caused by a previous event’s processing). Suppose you receive 100 events from epoll_wait(2), and in event #47 a condition causes event #13 to be closed. If you remove the structure and close() the fd for event #13, then your event cache might still say there are events waiting for that fd causing confusion.One solution for this is to call, during the processing of event 47,epoll_ctl(EPOLL_CTL_DEL) to delete fd 13 and close(), then mark its associated data structure as removed and link it to a cleanup list. If you find another event for fd 13 in your batch processing, you will discover the fd had been previously removed and there will be no confusion.
上面的意思簡單的翻譯一下如下:
如果你把
epoll_wait()
返回的檔案描述符進行了cache,那麼你必須動態監測我們cache的檔案描述符是否被關閉了。設想有這麼一種情況,epoll_wait()
一次返回了100個準備就緒的事件,然後再第#47
號事件的處理函式中,關閉了第#13
個事件。如果你把第#13
號事件對應的結構體刪除,並且把#13
號事件對應的fd
關閉,但是你快取的event cache
在處理到這個事件的時候就會比較鬱悶了。一個解決的方案就是,在處理#47
號事件的時候,使用epoll_ctl()
關閉#13
號事件對應的fd
,然後把#13
號事件對應的資料結構放到cleanup list
連結串列中。
Nginx 如何處理
nginx ngx_event_t
結構中的instance
變數是處理stale event
的核心,這裡值得一提的是,connection
連線池(實際是個陣列)和event
陣列(分read
和write
)。他們都是在初始化時就固定下來,之後不會動態增加和釋放,請求處理中只是簡單的取出和放回。而且有個細節就是,假設connection
陣列的大小為n
,那麼read event
陣列和write event
陣列的數量同樣是n,數量上一樣,每個連線對應一個read
和write event
結構,在連結被回收的時候,他們也就不能使用了。
我們看看連線池取出和放回的動作:
先看放回,一個請求處理結束後,會通過ngx_free_connection
將其持有的連線結構還回連線池,動作很簡單:
1c->data = ngx_cycle->free_connections;
2ngx_cycle->free_connections = c;
3ngx_cycle->free_connection_n++;
複製程式碼
然後看下面的程式碼:
1/*
2這裡要主要到的是,c結構體中並沒有清空,各個成員值還在(除了fd被置為-1外),那麼新請求在從連線池裡拿連線時,獲得的結構都還是沒用清空的垃圾資料,我們看取的時候的細節:
3*/
4
5// 此時的c含有沒用的“垃圾”資料
6c = ngx_cycle->free_connections;
7......
8// rev和wev也基本上“垃圾”資料,既然是垃圾,那麼取他們還有什麼用?其實還有點利用價值。。
9rev = c->read;
10wev = c->write;
11
12// 現在才清空c,因為後面要用了,肯定不能有非資料存在,從這裡我們也多少可以看得出,nginx
13// 為什麼在還的時候不清,我認為有兩點:儘快還,讓請求有連線可用;延遲清理,直到必須清理時。
14// 總的來說還是為了效率。
15ngx_memzero(c, sizeof(ngx_connection_t));
16
17c->read = rev;
18c->write = wev;
19c->fd = s;
20
21// 原有event中的instance變數
22instance = rev->instance;
23
24// 這裡清空event結構
25ngx_memzero(rev, sizeof(ngx_event_t));
26ngx_memzero(wev, sizeof(ngx_event_t));
27
28// 新的event中的instance在原來的基礎上取反。意思是,該event被重用了。因為在請求處理完
29// 之前,instance都不會被改動,而且當前的instance也會放到epoll的附加資訊中,即請求中event
30// 中的instance跟從epoll裡得到的instance是相同的,不同則是異常了,需要處理。
31rev->instance = !instance;
32wev->instance = !instance;
複製程式碼
現在我們實際的問題:ngx_epoll_process_events
1// 當前epoll上報的事件挨著處理,有先後。
2 for (i = 0; i < events; i++) {
3 c = event_list[i].data.ptr;
4
5 // 難道epoll中附加的instance,這個instance是在剛獲取連線池時已經設定的,一般是不會變化的。
6 instance = (uintptr_t) c & 1;
7 c = (ngx_connection_t *) ((uintptr_t) c & (uintptr_t) ~1);
8
9 // 處理可讀事件
10 rev = c->read;
11 /*
12 fd在當前處理時變成-1,意味著在之前的事件處理時,把當前請求關閉了,
13 即close fd並且當前事件對應的連線已被還回連線池,此時該次事件就不應該處理了,作廢掉。
14 其次,如果fd > 0,那麼是否本次事件就可以正常處理,就可以認為是一個合法的呢?答案是否定的。
15 這裡我們給出一個情景:
16 當前的事件序列是: A ... B ... C ...
17 其中A,B,C是本次epoll上報的其中一些事件,但是他們此時卻相互牽扯:
18 A事件是向客戶端寫的事件,B事件是新連線到來,C事件是A事件中請求建立的upstream連線,此時需要讀源資料,
19 然後A事件處理時,由於種種原因將C中upstream的連線關閉了(比如客戶端關閉,此時需要同時關閉掉取源連線),自然
20 C事件中請求對應的連線也被還到連線池(注意,客戶端連線與upstream連線使用同一連線池),
21 而B事件中的請求到來,獲取連線池時,剛好拿到了之前C中upstream還回來的連線結構,當前需要處理C事件的時候,
22 c->fd != -1,因為該連線被B事件拿去接收請求了,而rev->instance在B使用時,已經將其值取反了,所以此時C事件epoll中
23 攜帶的instance就不等於rev->instance了,因此我們也就識別出該stale event,跳過不處理了。
24 */
25 if (c->fd == -1 || rev->instance != instance) {
26
27 /*
28 * the stale event from a file descriptor
29 * that was just closed in this iteration
30 */
31
32 ngx_log_debug1(NGX_LOG_DEBUG_EVENT, cycle->log, 0,
33 "epoll: stale event %p", c);
34 continue;
35 }
36
37 /*
38 我們看到在write事件處理時,沒用相關的處理。事實上這裡是有bug的,在比較新的nginx版本里才被修復。
39 國內nginx大牛agent_zh,最早發現了這個bug,在nginx forum上有Igor和他就這一問題的討論:
40 http://forum.nginx.org/read.php?29,217919,218752
41 */
42
43 ......
44 }
複製程式碼
補充:
為什麼簡單的將instance
取反,就可以有效的驗證該事件是否是stale event
?會不會出現這樣的情況:
事件序列:A
… B
… B'
… C
,其中A,B,C跟之前討論的情形一樣,我們現在很明確,B
中獲得A
中釋放的連線時,會將instance
取反,這樣在C
中處理時,就可以發現rev->instance != instance
,從而發現stale event
。那麼我們假設B中處理時,又將該connection
釋放,在B'
中再次獲得,同樣經過instance
取反,這時我們會發現,instance
經過兩次取反時,就跟原來一樣了,這就不能通過fd == -1
與rev->instance != instance
的驗證,因此會當做正常事件來處理,後果很嚴重!
不知道看到這裡的有沒有跟我有同樣的想法同學,其實這裡面有些細節沒有被抖出來,實際上,這裡是不會有問題的,原因如下:
新連線通過accept
來獲得,即函式ngx_event_accept
。在這個函式中會ngx_get_connection
,從而拿到一個連線,然後緊接著初始化這個連線,即呼叫ngx_http_init_connection
,在這個函式中通常是會此次事件掛到post_event
鏈上去:
1if (ngx_use_accept_mutex) {
2 ngx_post_event(rev, &ngx_posted_events);
3 return;
4}
複製程式碼
然後繼續accept
或者處理其他事件。而一個程式可以進行accept
,必然是拿到了程式間的accept
鎖。凡是程式拿到accept
鎖,那麼它就要儘快的處理事務,並釋放鎖,以讓其他程式可以accept
,儘快處理的辦法就是將epoll
此次上報的事件,掛到響應的連結串列或佇列上,等釋放accept
鎖之後在自己慢慢處理。所以從epoll_wait
返回到外層,才會對post
的這些事件來做處理。在正式處理之前,每個新建的連線都有自己的connection
,即B
和B'
肯定不會在connection
上有任何攙和,在後續的處理中,對C
的影響也只是由於B
或B'
從連線池中拿到了本應該屬於C
的connection
,從而導致fd
(被關閉)和instance
出現異常(被複用),所以現在看來,我們擔心的那個問題是多慮了。
喜歡本文的朋友們,歡迎長按下圖關注訂閱號鄭爾多斯,更多精彩內容第一時間送達