1. 程式人生 > >Epoll空輪詢bug

Epoll空輪詢bug

開發十年,就只剩下這套架構體系了! >>>   

bug表現

epoll bug

  • 正常情況下,selector.select()操作是阻塞的,只有被監聽的fd有讀寫操作時,才被喚醒
  • 但是,在這個bug中,沒有任何fd有讀寫請求,但是select()操作依舊被喚醒
  • 很顯然,這種情況下,selectedKeys()返回的是個空陣列
  • 然後按照邏輯執行到while(true)
    處,迴圈執行,導致死迴圈。

bug原因

JDK bug列表中有兩個相關的bug報告:

  1. JDK-6670302 : (se) NIO selector wakes up with 0 selected keys infinitely
  2. JDK-6403933 : (se) Selector doesn't block on Selector.select(timeout) (lnx)

JDK-6403933的bug說出了實質的原因:

This is an issue with poll (and epoll) on Linux. If a file descriptor for a connected socket is polled with a request event mask of 0, and if the connection is abruptly terminated (RST) then the poll wakes up with the POLLHUP (and maybe POLLERR) bit set in the returned event set. The implication of this behaviour is that Selector will wakeup and as the interest set for the SocketChannel is 0 it means there aren't any selected events and the select method returns 0.

具體解釋為:在部分Linux的2.6的kernel中,poll和epoll對於突然中斷的連線socket會對返回的eventSet事件集合置為POLLHUP,也可能是POLLERR,eventSet事件集合發生了變化,這就可能導致Selector會被喚醒。

這是與作業系統機制有關係的,JDK雖然僅僅是一個相容各個作業系統平臺的軟體,但很遺憾在JDK5和JDK6最初的版本中(嚴格意義上來將,JDK部分版本都是),這個問題並沒有解決,而將這個帽子拋給了作業系統方,這也就是這個bug最終一直到2013年才最終修復的原因,最終影響力太廣。

解決辦法

不完善的解決辦法

grizzly的commiteer們最先進行修改的,並且通過眾多的測試說明這種修改方式大大降低了JDK NIO的問題。

if (SelectionKey != null)  {  // the key you registered on the temporary selector
   SelectionKey.cancel();   // cancel the SelectionKey that was registered with the temporary selector
   // flush the cancelled key
   temporarySelector.selectNow();
} 

但是,這種修改仍然不是可靠的,一共有兩點:

  1. 多個執行緒中的SelectionKey的key的cancel,很可能和下面的Selector.selectNow同時併發,如果是導致key的cancel後執行很可能沒有效果
  2. 與其說第一點使得NIO空轉出現的機率大大降低,經過Jetty伺服器的測試報告發現,這種重複利用Selector並清空SelectionKey的改法很可能沒有任何的效果,

完善的解決辦法

最終的終極辦法是建立一個新的Selector:

Trash wasted Selector, creates a new one.

各應用具體解決方法

Jetty

Jetty首先定義兩了-D引數:

  • JVMBUG_THRESHHOLD

org.mortbay.io.nio.JVMBUG_THRESHHOLD, defaults to 512 and is the number of zero select returns that must be exceeded in a period.

  • threshhold

org.mortbay.io.nio.MONITOR_PERIOD defaults to 1000 and is the period over which the threshhold applies.

第一個引數是select返回值為0的計數,第二個是多長時間,整體意思就是控制在多長時間內,如果Selector.select不斷返回0,說明進入了JVM的bug的模式。

做法是:

  • 記錄select()返回為0的次數(記做jvmBug次數)
  • 在MONITOR_PERIOD時間範圍內,如果jvmBug次數超過JVMBUG_THRESHHOLD,則新建立一個selector

Jetty解決空輪詢bug

Netty

思路和Jetty的處理方式幾乎是一樣的,就是netty講重建Selector的過程抽取成了一個方法。

long currentTimeNanos = System.nanoTime();
for (;;) {
    // 1.定時任務截止事時間快到了,中斷本次輪詢
    ...
    // 2.輪詢過程中發現有任務加入,中斷本次輪詢
    ...
    // 3.阻塞式select操作
    selector.select(timeoutMillis);
    // 4.解決jdk的nio bug
    long time = System.nanoTime();
    if (time - TimeUnit.MILLISECONDS.toNanos(timeoutMillis) >= currentTimeNanos) {
        selectCnt = 1;
    } else if (SELECTOR_AUTO_REBUILD_THRESHOLD > 0 &&
            selectCnt >= SELECTOR_AUTO_REBUILD_THRESHOLD) {

        rebuildSelector();
        selector = this.selector;
        selector.selectNow();
        selectCnt = 1;
        break;
    }
    currentTimeNanos = time; 
    ...
 }

netty 會在每次進行 selector.select(timeoutMillis) 之前記錄一下開始時間currentTimeNanos,在select之後記錄一下結束時間,判斷select操作是否至少持續了timeoutMillis秒(這裡將time - TimeUnit.MILLISECONDS.toNanos(timeoutMillis) >= currentTimeNanos改成time - currentTimeNanos >= TimeUnit.MILLISECONDS.toNanos(timeoutMillis)或許更好理解一些),
如果持續的時間大於等於timeoutMillis,說明就是一次有效的輪詢,重置selectCnt標誌,否則,表明該阻塞方法並沒有阻塞這麼長時間,可能觸發了jdk的空輪詢bug,當空輪詢的次數超過一個閥值的時候,預設是512,就開始重建selector