Selector空輪詢

JDK NIO的BUG,例如臭名昭著的epoll bug,它會導致Selector空輪詢,最終導致CPU 100%。官方聲稱在JDK1.6版本的update18修復了該問題,但是直到JDK1.7版本該問題仍舊存在,只不過該BUG發生概率降低了一些而已,它並沒有被根本解決。該BUG以及與該BUG相關的問題單可以參見以下鏈接內容。

https://bugs.java.com/bugdatabase/view_bug.do?bug_id=2147719

https://bugs.java.com/bugdatabase/view_bug.do?bug_id=6403933

參考:https://github.com/netty/netty/issues/327

參考:https://www.jianshu.com/p/d0f06b13e2fb

參考:http://blog.jobbole.com/105564/

參考:http://blog.csdn.net/xyls12345/article/details/26571699

Selector BUG出現的原因

若Selector的輪詢結果爲空,也沒有wakeup或新消息處理,則發生空輪詢,CPU使用率100%

Netty的解決辦法

  • 對Selector的select操作週期進行統計,每完成一次空的select操作進行一次計數,
  • 若在某個週期內連續發生N次空輪詢,則觸發了epoll死循環bug。
  • 重建Selector,判斷是否是其他線程發起的重建請求,若不是則將原SocketChannel從舊的Selector上去除註冊,重新註冊到新的Selector上,並將原來的Selector關閉。

參考:http://blog.csdn.net/baiye_xing/article/details/73351330

前面講到了epoll的一些機制,與select和poll等傳統古老的IO多路複用機制的一些區別,這些區別實質可以總結爲一句話,

就是epoll將重要的基於事件的fd集合放在了內核中來完成,因爲內核是高效的,所以很多關於fd事件監聽集合的操作也是高效的,

不方便的就是,因爲在內核中,所以我們需要通過系統調用來調用關於fd操作集合,而不是直接自己攢一個。

如果在linux中,epoll在JDK6中還需要配置,在後續的版本中爲JDK的NIO提供了默認的實現,但是epoll在JDK中的實現卻是漏洞百出的,

bug非常的多,比較容易復現並且被衆多人詬病的就是epoll輪詢的處理方法。

sun的bug列表爲:

JDK-6670302 (se) NIO selector wakes up with 0 selected keys infinitely [lnx 2.4]

JDK-6670302 : (se) NIO selector wakes up with 0 selected keys infinitely [lnx 2.4]

===》這個bug的描述內容爲,在NIO的selector中,即使是關注的select輪詢事件的key爲0的話,NIO照樣不斷的從select本應該阻塞的

情況中wake up出來,也就是下圖中的紅色阻塞的部分:

然後,因爲selector的select方法,返回numKeys是0,所以下面本應該對key值進行遍歷的事件處理根本執行不了,又回到最上面的while(true)循環,循環往復,不斷的輪詢,直到linux系統出現100%的CPU情況,其它執行任務幹不了活,

最終導致程序崩潰。

==》從這個bug上來看,這個絕對是JDK中的問題,select方法就應該是阻塞的,沒有key事件過來,那麼就不應該返回,和應用程序的寫法沒有任何的關係,與之相差不多的一個bug給出瞭解決的方案:

JDK-6403933 (se) Selector doesn't block on Selector.select(timeout) (lnx)

JDK-6403933 : (se) Selector doesn't block on Selector.select(timeout) (lnx)

這個bug的意思基本上和前面的JDK-6670302相差不大,也是Selector不阻塞,前一個bug說明的是最終的現象,

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

具體解釋爲,在部分Linux的2.6的kernel中,poll和epoll對於突然中斷的連接socket會對返回的eventSet事件集合置爲POLLHUP,也可能是POLLERR,eventSet事件集合發生了變化,這就可能導致Selector會被喚醒。==》這是與操作系統機制有關係的,JDK雖然僅僅

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

修復的方法,在這個bug中已經提到了:

上面是第一個建議,首先將SelectKey去除掉,然後“刷新”一下Selector,刷新的方式也就是調用Selector.selectNow方法,

這個示意的代碼如下:

這段代碼意味着重置,首先將SelectionKey註銷掉,然後重新調用非阻塞的selectNow來讓Selector換取“新生”。

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

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

1.多個線程中的SelectionKey的key的cancel,很可能和下面的Selector.selectNow同時併發,如果是導致key的cancel後運行很可能沒有效果

2.與其說第一點使得NIO空轉出現的機率大大降低,經過Jetty服務器的測試報告發現,這種重複利用Selector並清空SelectionKey的改法很可能沒有任何的效果,

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

具體的Jetty服務器的分析地址爲:

Jetty/Feature/JVM NIO Bug

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

  • org.mortbay.io.nio.JVMBUG_THRESHHOLD, defaults to 512 and is the number of zero select returns that must be exceeded in a period.
  • 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的模式

那麼,Jetty這時候就有所作爲了,我們看到Jetty的具體的代碼如下:

首先,根據-D參數判斷是否進入了JAVA NIO空轉的bug模式,一個是判斷時間,一個是判斷次數,次數通過-jvmBug作爲計數器進行統計;如果一旦確定是bug,可以看到上述代碼爲了防止併發出現,加了Sychronized鎖,接着開啓一個新的Selector,並將原有的SelectionKey的事件全部轉移到了新的Selector中,最後將-jvmBug計數器置0;

==》這種處理方法要保險的多,基本上不會有任何的問題了,

即使上述的處理方式,對應極少的linux環境和JDK的版本,仍會出現一些問題,這主要是因爲網絡中斷的間隔時間太短造成的,需要給內核一定的時鐘週期進行緩衝,而上述的Jetty的org.mortbay.io.nio.BUSY_PAUSE這個參數就是起到間隔的作用,間隔多少微秒再調用Select,這樣基本上能最大程度上避免上述問題出現了。

從上面Jetty各種處理方法來看,基本能屏蔽低版本JDK和操作系統的epoll的影響,讓NIO可以無憂運行。當然,對於NIO框架也是修正了這些錯誤,前面提到的Griizzly和Netty都對這個問題採取了響應的策略。

以Netty爲例,具體位置在NioSelector的實現類AbsNioSelector中,思路和Jetty的處理方式幾乎是一樣的,就是netty講重建Selector的過程抽取成了一個方法,叫做rebuildSelector,可以看看其方法:

public void rebuildSelector() {
        if (!inEventLoop()) {
            execute(new Runnable() {
                @Override
                public void run() {
                    rebuildSelector();
                }
            });
            return;
        }

        final Selector oldSelector = selector;
        final Selector newSelector;

        if (oldSelector == null) {
            return;
        }

        try {
            newSelector = openSelector();
        } catch (Exception e) {
            logger.warn("Failed to create a new Selector.", e);
            return;
        }

        // Register all channels to the new Selector.
        int nChannels = 0;
        for (;;) {
            try {
                for (SelectionKey key: oldSelector.keys()) {
                    Object a = key.attachment();
                    try {
                        if (!key.isValid() || key.channel().keyFor(newSelector) != null) {
                            continue;
                        }

                        int interestOps = key.interestOps();
                        key.cancel();
                        SelectionKey newKey = key.channel().register(newSelector, interestOps, a);
                        if (a instanceof AbstractNioChannel) {
                            // Update SelectionKey
                            ((AbstractNioChannel) a).selectionKey = newKey;
                        }
                        nChannels ++;
                    } catch (Exception e) {
                        logger.warn("Failed to re-register a Channel to the new Selector.", e);
                        if (a instanceof AbstractNioChannel) {
                            AbstractNioChannel ch = (AbstractNioChannel) a;
                            ch.unsafe().close(ch.unsafe().voidPromise());
                        } else {
                            @SuppressWarnings("unchecked")
                            NioTask<SelectableChannel> task = (NioTask<SelectableChannel>) a;
                            invokeChannelUnregistered(task, key, e);
                        }
                    }
                }
            } catch (ConcurrentModificationException e) {
                // Probably due to concurrent modification of the key set.
                continue;
            }

            break;
        }

        selector = newSelector;

        try {
            // time to close the old selector as everything else is registered to the new one
            oldSelector.close();
        } catch (Throwable t) {
            if (logger.isWarnEnabled()) {
                logger.warn("Failed to close the old Selector.", t);
            }
        }

        logger.info("Migrated " + nChannels + " channel(s) to the new Selector.");
    }

基本上類似,這裏就不再綴餘。

分析到這裏,可以看到爲什麼NIO框架如Netty,Grizzly,還有最近的炒得很熱的Jboss的UnderTow,NIO遠遠不止這篇文章分析得這一個,還有很多,大可在JDK官網上去查,而這些框架都將NIO的很多不好用的問題,bug隱藏起來了,並加上諸如限流,字符轉換,基於設計模式等特性,讓開發人員更好的編寫高併發的程序,而不用過多的網絡的關注與細節。

由此可見,現在JAVA真是越來越危機了,從前幾年的SSH把java ee給替換掉,到現在jdk都時不時冒出一個bug來,而且最近JDK8中的一個bug大有超過這個bug之勢,jcp社區確實需要好好反省了,要不然java沒落了,一干程序員又得下崗再就業了。

總結:

NIO的空轉bug歷史悠久流傳廣泛,應用服務器的前端框架一般都採取換一個新Selector的方式對此進行處理,屏蔽掉了JDK5/6的問題,但對於此問題來講,還是儘量將JDK的版本更新到最新,或者使用NIO框架如Netty,Grizzly等進行研發,以免出更多的問題。

 epoll bug CPU空輪詢

SUN在解決該BUG的問題上不給力,只能從NIO框架層面進行問題規避,下面我們看下Netty是如何解決該問題的。

Netty的解決策略:

1) 根據該BUG的特徵,首先偵測該BUG是否發生;

2) 將問題Selector上註冊的Channel轉移到新建的Selector上;

3) 老的問題Selector關閉,使用新建的Selector替換。

下面具體看下代碼,首先檢測是否發生了該BUG:

圖2-27 epoll bug 檢測

一旦檢測發生該BUG,則重建Selector,代碼如下:

圖2-28 重建Selector

重建完成之後,替換老的Selector,代碼如下:

圖2-29 替換Selector

大量生產系統的運行表明,Netty的規避策略可以解決epoll bug 導致的IO線程CPU死循環問題。

netty的解決代碼在package io.netty.channel.nio.nioEventLoop這個類下面。

文章來源:https://www.cnblogs.com/JAYIT/p/8241634.html

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章