這些都是在實踐中踩過雷的,今天某應用再次踩雷,把遇到的幾個雷都收集一下,給後來者參考。
1.即使是accept事件,沒有真正的read和write,Channel也要關閉,否則unix domain socket會被泄漏(WINDOWS更可怕),因為NIO的每個
Channel上都有兩個FD用來監聽事件(接收和發送走不同的FD)。
2.cancel事件導致CPU佔用100%,http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=6403933
其原因就是調用key.cancel()時底層在下一次seelect前並沒有真正的取消。導致等待select事件返回卻又沒有返回我們註冊的key.這個事件不斷地
迴圈觸發,CPU一直處理返回 key為0的select()調用。解決方案有兩種,一是在key.cancel()後立即selectNow();但是如果是多線程並行作業,有
可能這兩行語句中間線程被切換,使得key.cancel()後沒有立即執行 selectNow().這在多Selector情況下是可能的。另一種就是jetty處理方式,如果
select()返回0且連續幾次出現這樣的情況(有事件觸發返回,卻不是返回我們註冊的KEY),就將有效key重新註冊到一個新的selector上。其實
glassfish在處理多次次次次write返回為0的情況時也是這種策略。
範例程式碼:(真實的項目中)
int selectTimeout = connectionConfig.getSelectTimeout(); int allProcessMaxTime = connectionConfig.getAllProcessMaxTime(); //selector在實現時有bug,epool底層可能會發送一個錯誤的訊號導致select方法提前返回,但沒有 //返回註冊的事件,而且不斷迴圈造成CPU100% int slelectZeroCount = 0; int maxZeroCount = 20; int fixed = 0; while (selector.isOpen() && selector.keys().size() != 0 && allProcessMaxTime > 0) { long start = System.currentTimeMillis(); // 查詢看是否有已經準備好的通道,指定逾時時間 int count = selector.select(selectTimeout); if (count == 0) { slelectZeroCount++; } else { slelectZeroCount = 0; //保證是連續的count==0時才將slelectZeroCount++,如果其中有一次返回註冊事件測已經正常 } if (slelectZeroCount > maxZeroCount && fixed == 0) { //沒有嘗試修複動作,則先進行修複幹預 for (SelectionKey key : selector.keys()) { if (key.isValid() && key.interestOps() == 0) { key.cancel(); } } fixed = 1; } else if (slelectZeroCount > maxZeroCount && fixed == 1) { //如果已經幹預過仍然連續返回0,注意如果不返回0的話slelectZeroCount就被置0. //重新擷取一個selector,將當前事件重新註冊到新的selector上。並銷毀當前selector Selector newSelector = this.getSelector(); this.changeSelector(selector, newSelector); selector = newSelector; } //對channel進行正常處理 }
重新註冊的代碼:
private synchronized void changeSelector(Selector oldSelector, Selector newSelector) { for (SelectionKey key : oldSelector.keys()) { if (!key.isValid() || key.interestOps() == 0) { continue; } Object att = key.attachment(); try { if (att == null) { key.channel().register(newSelector, key.interestOps()); } else { key.channel().register(newSelector, key.interestOps(), att); } } catch (ClosedChannelException e) { SocketChannel sc = (SocketChannel) key.channel(); sc.close(); } } try { oldSelector.close(); } catch (IOException e) { logger.error(e.getMessage()); } }
同樣對於網路狀態不好時,連續寫操作返回0的處理:
private void flushData(Selector selector, SocketChannel socketChannel, ByteBuffer byteBuffer) throws IOException { int count = 0; int maxCount = 20; while (byteBuffer.hasRemaining()) { int len = socketChannel.write(byteBuffer); if (len < 0) { throw new EOFException("write channel is closed."); } // 如果不對len==0(即當前網路不可用)的情況處理,則while(byteBuffer.hasRemaining())可能一直 // 迴圈下去而消耗大量的CPU. if (len == 0) { count++; } else { count = 0; } if (count > maxCount) { throw new IOException("can't connect to target."); } } }