nginx事件 -- 第六篇 stale event

鄭爾多斯發表於2018-12-27

微信公眾號:鄭爾多斯
關注可瞭解更多的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陣列(分readwrite)。他們都是在初始化時就固定下來,之後不會動態增加和釋放,請求處理中只是簡單的取出和放回。而且有個細節就是,假設connection陣列的大小為n,那麼read event陣列和write event陣列的數量同樣是n,數量上一樣,每個連線對應一個readwrite 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->log0,
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?會不會出現這樣的情況:
事件序列:ABB'C,其中A,B,C跟之前討論的情形一樣,我們現在很明確,B中獲得A中釋放的連線時,會將instance取反,這樣在C中處理時,就可以發現rev->instance != instance,從而發現stale event。那麼我們假設B中處理時,又將該connection釋放,在B'中再次獲得,同樣經過instance取反,這時我們會發現,instance經過兩次取反時,就跟原來一樣了,這就不能通過fd == -1rev->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,即BB'肯定不會在connection上有任何攙和,在後續的處理中,對C的影響也只是由於BB'從連線池中拿到了本應該屬於Cconnection,從而導致fd(被關閉)和instance出現異常(被複用),所以現在看來,我們擔心的那個問題是多慮了。


喜歡本文的朋友們,歡迎長按下圖關注訂閱號鄭爾多斯,更多精彩內容第一時間送達

鄭爾多斯
鄭爾多斯

相關文章