隨著並發數量提高,傳統 NIO 框架採用單一 Selector 支撐大量連接事件管理與觸發已遇瓶頸,因此各種 NIO 框架新版本都採用多 Selector 並存結構,由多個 Selector 均衡管理大量連接。以 Mina 和 Grizzly 實現為例。
在 Mina 2.0 中,Selector 管理由 org.apache.mina.transport.socket.nio.NioProcessor 處理,每個 NioProcessor 物件保存一個 Selector,負責具體 select、wakeup、Channel 註冊與取消、讀寫事件註冊與判斷、實際 IO 讀寫操作等。核心代碼如下:
public NioProcessor(Executor executor) {
super(executor);
try {
// Open a new selector
selector = Selector.open();
} catch (IOException e) {
throw new RuntimeIoException("Failed to open a selector.", e);
}
}
protected int select(long timeout) throws Exception {
return selector.select(timeout);
}
protected boolean isInterestedInRead(NioSession session) {
SelectionKey key = session.getSelectionKey();
return key.isValid() && (key.interestOps() & SelectionKey.OP_READ) != 0;
}
protected boolean isInterestedInWrite(NioSession session) {
SelectionKey key = session.getSelectionKey();
return key.isValid() && (key.interestOps() & SelectionKey.OP_WRITE) != 0;
}
protected int read(NioSession session, IoBuffer buf) throws Exception {
return session.getChannel().read(buf.buf());
}
protected int write(NioSession session, IoBuffer buf, int length) throws Exception {
if (buf.remaining() <= length) {
return session.getChannel().write(buf.buf());
} else {
int oldLimit = buf.limit();
buf.limit(buf.position() + length);
try {
return session.getChannel().write(buf.buf());
} finally {
buf.limit(oldLimit);
}
}
}
這些方法調用都通過 AbstractPollingIoProcessor 處理,此類可見 NIO 框架核心邏輯:註冊、select、分發。NioProcessor 初始化在 NioSocketAcceptor 建構方法中調用:
public NioSocketAcceptor() {
super(new DefaultSocketSessionConfig(), NioProcessor.class);
((DefaultSocketSessionConfig) getSessionConfig()).init(this);
}
直接調用父類 AbstractPollingIoAcceptor 建構函數,其中可見預設啟動一個 SimpleIoProcessorPool 包裝 NioProcessor:
protected AbstractPollingIoAcceptor(IoSessionConfig sessionConfig,
Class<? extends IoProcessor<T>> processorClass) {
this(sessionConfig, null, new SimpleIoProcessorPool<T>(processorClass), true);
}
這其實是組合模式,SimpleIoProcessorPool 與 NioProcessor 都實作 Processor 介面,一個是組合形成的 Processor 池,另一個是單獨類。調用 SimpleIoProcessorPool 建構函數:
private static final int DEFAULT_SIZE = Runtime.getRuntime().availableProcessors() + 1;
public SimpleIoProcessorPool(Class<? extends IoProcessor<T>> processorType) {
this(processorType, null, DEFAULT_SIZE);
}
可見預設池大小為 CPU 核心數 + 1,即創建 CPU+1 個 Selector 物件。重載建構函數裡創建陣列,啟動 CachedThreadPool 運行 NioProcessor,透過反射創建具體 Processor 物件。
Mina 有新連接建立時,創建 NioSocketSession 並傳入上述 SimpleIoProcessorPool,連接初始化時將 Session 加入池:
protected NioSession accept(IoProcessor<NioSession> processor, ServerSocketChannel handle) throws Exception {
SelectionKey key = handle.keyFor(selector);
if ((key == null) || (!key.isValid()) || (!key.isAcceptable())) { return null; }
SocketChannel ch = handle.accept();
if (ch == null) { return null; }
return new NioSocketSession(this, processor, ch);
}
private void processHandles(Iterator<H> handles) throws Exception {
while (handles.hasNext()) {
H handle = handles.next();
handles.remove();
T session = accept(processor, handle);
if (session == null) { break; }
initSession(session, null, null);
session.getProcessor().add(session);
}
}
加入操作遞增整型變量並模陣列大小後對應 NioProcessor 註冊到 session:
private IoProcessor<T> nextProcessor() {
checkDisposal();
return pool[Math.abs(processorDistributor.getAndIncrement()) % pool.length];
}
if (p == null) {
p = nextProcessor();
IoProcessor<T> oldp = (IoProcessor<T>) session.setAttributeIfAbsent(PROCESSOR, p);
if (oldp != null) { p = oldp; }
}
這樣每個連接關聯一個 NioProcessor,即關聯一個 Selector,避免所有連接共用一個 Selector 負載過高導致 server 回應變慢。但注意 NioSocketAcceptor 也有一個 Selector,這是集中處理 OP_ACCEPT 事件的 Selector,主要用於連接接入,不與處理讀寫事件的 Selector 混在一起,故 Mina 預設開啟的 Selector 是 CPU+2 個。
Grizzly 較保守,預設啟動兩個 Selector,一個專門負責 accept,另一個負責連接 IO 讀寫事件管理。Selector 管理由 SelectorRunner 類封裝,含 Selector 物件與核心分發註冊邏輯,可理解為 Mina 中的 NioProcessor。核心代碼:
protected boolean doSelect() {
selectorHandler = transport.getSelectorHandler();
selectionKeyHandler = transport.getSelectionKeyHandler();
strategy = transport.getStrategy();
try {
if (isResume) {
isResume = false;
if (keyReadyOps != 0) { if (!iterateKeyEvents()) return false; }
if (!iterateKeys()) return false;
}
lastSelectedKeysCount = 0;
selectorHandler.preSelect(this);
readyKeys = selectorHandler.select(this);
if (stateHolder.getState(false) == State.STOPPING) return false;
lastSelectedKeysCount = readyKeys.size();
if (lastSelectedKeysCount != 0) {
iterator = readyKeys.iterator();
if (!iterateKeys()) return false;
}
selectorHandler.postSelect(this);
} catch (ClosedSelectorException e) { ... } catch (Exception e) { ... } catch (Throwable t) { ... }
return true;
}
基本是 Reactor 實現樣子。AbstractNIOTransport 維護 SelectorRunner 陣列,TCPNIOTransport 繼承它,在 start 方法調用 startSelectorRunners 創建並啟動陣列:
private static final int DEFAULT_SELECTOR_RUNNERS_COUNT = 2;
@Override
public void start() throws IOException {
if (selectorRunnersCount <= 0) { selectorRunnersCount = DEFAULT_SELECTOR_RUNNERS_COUNT; }
startSelectorRunners();
}
protected void startSelectorRunners() throws IOException {
selectorRunners = new SelectorRunner[selectorRunnersCount];
synchronized(selectorRunners) {
for (int i = 0; i < selectorRunnersCount; i++) {
SelectorRunner runner = new SelectorRunner(this, SelectorFactory.instance().create());
runner.start();
selectorRunners[i] = runner;
}
}
}
可見 Grizzly 未採用單獨池物件管理 SelectorRunner,直接用陣列管理,預設大小 2。SelectorRunner 實作 Runnable,其 start 調用執行緒池運行自身。剛提到 Grizzly Accept 由單獨 Selector 管理,如何表現?答案在 RoundRobinConnectionDistributor,用於分發註冊事件到相應 SelectorRunner,分發方式:
public Future<RegisterChannelResult> registerChannelAsync(SelectableChannel channel, int interestOps, Object attachment, CompletionHandler completionHandler) throws IOException {
SelectorRunner runner = getSelectorRunner(interestOps);
return transport.getSelectorHandler().registerChannelAsync(runner, channel, interestOps, attachment, completionHandler);
}
private SelectorRunner getSelectorRunner(int interestOps) {
SelectorRunner[] runners = getTransportSelectorRunners();
int index;
if (interestOps == SelectionKey.OP_ACCEPT || runners.length == 1) { index = 0; }
else { index = (counter.incrementAndGet() % (runners.length - 1)) + 1; }
return runners[index];
}
getSelectorRunner 道出秘密:若是 OP_ACCEPT 則都用陣列第一個 SelectorRunner,否則透過取模運算結果 +1 從後面 SelectorRunner 取一個註冊。
Selector 優於單 Selector。Selector 情況下,處理 OP_READ/OP_WRITE 的 Selector 應與處理 OP_ACCEPT 的 Selector 分離,即接入應由單獨 Selector 處理,避免 IO 讀寫事件影響接入速度。Selector 數目問題:Mina 預設 CPU+2,Grizzly 總共 2 個。我傾向 Mina 策略,但認為應對 CPU 核心數做判斷,若超過 8 核,更多 Selector 執行緒可能帶來較大切換開銷,Mina 預設策略並非絕對合適,幸好可配置此數值。