應用服務器中對JDK的epoll空轉bug的處理
原文鏈接:應用服務器中對JDK的epoll空轉bug的處理
前面講到了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;
==》這種處理方法要保險的多,基本上不會有任何的問題了,
Jetty在這個網頁中還提供了很多參數,如:
即使上述的處理方式,對應極少的linux環境和JDK的版本,仍會出現一些問題,這主要是因為網絡中斷的間隔時間太短造成的,需要給內核一定的時鐘周期進行緩沖,而上述的Jetty的org.mortbay.io.nio.BUSY_PAUSE這個參數就是起到間隔的作用,間隔多少微秒再調用Select,這樣基本上能最大程度上避免上述問題出現了。
從上面Jetty各種處理方法來看,基本能屏蔽低版本JDK和操作系統的epoll的影響,讓NIO可以無憂運行。當然,對於NIO框架也是修正了這些錯誤,前面提到的Griizzly和Netty都對這個問題采取了響應的策略。
以Netty為例,具體位置在NioSelector的實現類AbsNioSelector中:
上述的思路和Jetty的處理方式幾乎是一樣的,就是netty講重建Selector的過程抽取成了一個方法,叫做rebuildSelector,可以看看其方法:
基本上類似,這裏就不再綴余。
分析到這裏,可以看到為什麽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等進行研發,以免出更多的問題。
應用服務器中對JDK的epoll空轉bug的處理