在進入NIO之前,先回顧一下Java标準IO方式實作的網絡server端:
public class IOServerThreadPool {
private static final Logger LOGGER = LoggerFactory.getLogger(IOServerThreadPool.class);
public static void main(String[] args) {
ExecutorService executorService = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors());
ServerSocket serverSocket = null;
try {
serverSocket = new ServerSocket();
serverSocket.bind(new InetSocketAddress(2345));
} catch (IOException ex) {
LOGGER.error("Listen failed", ex);
return;
}
try{
while(true) {
Socket socket = serverSocket.accept();
executorService.submit(() -> {
try{
InputStream inputstream = socket.getInputStream();
LOGGER.info("Received message {}", IOUtils.toString(new InputStreamReader(inputstream)));
} catch (IOException ex) {
LOGGER.error("Read message failed", ex);
}
});
}
} catch(IOException ex) {
try {
serverSocket.close();
} catch (IOException e) {
}
LOGGER.error("Accept connection failed", ex);
}
}
}
這是一個經典的每連接配接每線程的模型,之是以使用多線程,主要原因在于socket.accept()、socket.read()、socket.write()三個主要函數都是同步阻塞的,當一個連接配接在處理I/O的時候,系統是阻塞的,如果是單線程的話必然就挂死在那裡;但CPU是被釋放出來的,開啟多線程,就可以讓CPU去處理更多的事情。其實這也是所有使用多線程的本質:
- 利用多核。
- 當I/O阻塞系統,但CPU空閑的時候,可以利用多線程使用CPU資源。
現在的多線程一般都使用線程池,可以讓線程的建立和回收成本相對較低。在活動連接配接數不是特别高(小于單機1000)的情況下,這種模型是比較不錯的,可以讓每一個連接配接專注于自己的I/O并且程式設計模型簡單,也不用過多考慮系統的過載、限流等問題。線程池本身就是一個天然的漏鬥,可以緩沖一些系統處理不了的連接配接或請求。
不過,這個模型最本質的問題在于,嚴重依賴于線程。但線程是很"貴"的資源,主要表現在:
- 線程的建立和銷毀成本很高,在Linux這樣的作業系統中,線程本質上就是一個程序。建立和銷毀都是重量級的系統函數。
- 線程本身占用較大記憶體,像Java的線程棧,一般至少配置設定512K~1M的空間,如果系統中的線程數過千,恐怕整個JVM的記憶體都會被吃掉一半。
- 線程的切換成本是很高的。作業系統發生線程切換的時候,需要保留線程的上下文,然後執行系統調用。如果線程數過高,可能執行線程切換的時間甚至會大于線程執行的時間,這時候帶來的表現往往是系統load偏高、CPU sy使用率特别高(超過20%以上),導緻系統幾乎陷入不可用的狀态。
- 容易造成鋸齒狀的系統負載。因為系統負載是用活動線程數或CPU核心數,一旦線程數量高但外部網絡環境不是很穩定,就很容易造成大量請求的結果同時傳回,激活大量阻塞線程進而使系統負載壓力過大。
是以,當面對十萬甚至百萬級連接配接的時候,傳統的BIO模型是無能為力的。随着移動端應用的興起和各種網絡遊戲的盛行,百萬級長連接配接日趨普遍,此時,必然需要一種更高效的I/O處理模型。
BIO弱在哪裡?
都說NIO更高效,那BIO怎麼就弱了呢?弱在哪裡呢?現在通過上面BIO方式編寫的server一探究竟。

場景:假設用戶端在與server建立連接配接後,請求傳輸200M資料。
server端運作在某伺服器作業系統上,JVM在該伺服器作業系統核心(OS kernel)之上,而BIO方式編寫的server程式(Java application)則是跑在JVM上。
将經曆以下步驟:
1、client請求發送資料
2、server端的Java application并不能直接開始接收資料,而是需要等待 OS kernel 接收網絡資料傳輸的網卡準備就緒,網卡是專門負責網絡資料傳輸的。
3、網卡就緒,執行接收資料到OS kernel,此時資料需要完整地copy到作業系統核心緩沖區中。這是第一次copy資料,傳輸的時間取決于傳輸資料的大小和網絡帶寬。(傳輸時間=資料大小/帶寬)
4、運作在JVM上的Java應用程式,在接收用戶端發送到資料時調用getInputStream(),但并不是立馬就能get到,需要等待作業系統核心(網卡)已經把資料接收(copy)完畢,且核心準備就緒。
5、核心準備就緒,會通過管道将資料全部複制到JVM中,這一次是将核心緩沖區中的資料copy到JVM中(JVM運作時資料區)。
6、這時資料已全部存在在JVM中,server端應用程式才能通過InputStream将資料傳輸到Java application業務處理處,此時真正拿到client傳來的資料(也就是getInputStream()裡面的内容),執行具體的業務邏輯處理。
還需要注意的是:java.io.inputstream 傳輸資料時,資料必須是完整的。也就是說,上例中傳輸200M資料,作業系統核心必須全部接收好,一次性給我(JVM)。
看似簡單的serverSocket.accept()後,開啟子線程,執行socket.getInputStream()拿client傳過來的資料,其實經曆上面的步驟,Java application需要借助OS kernel 完成2次copy。這也是為什麼這種方式通常是一個連接配接一個線程,2次copy受到網絡擁塞、網絡波動等因素的影響。
基于事件、通知模型的NIO
提到事件、通知,大家自然會想到——觀察者模式,簡單描述如下:
觀察者模式中三個組成角色,觀察者、被觀察者(服務提供者)、觀察的主題,也就是事件。觀察者首先需要訂閱感興趣的事件,然後當事件發生時,被觀察者會進行通知。
基于事件、通知模型的NIO,就是基于此實作的。此實作非常巧妙,觀察者是JVM,被觀察者是OS kernel 。
JVM作為觀察者,它可以向OS kernel 訂閱連接配接事件、資料可讀事件、資料可寫事件。Java NIO提供了事件池Keys,當訂閱的事件發生時,OS kernel 就會通知JVM,并将該事件放入事件池當中,而運作在JVM上的Java application可以用NIO提供的selector從事件池中輪詢就緒的消息;輪詢到就緒的事件後即可直接執行。
在JVM注冊事件後,隻需要selector事件池就好了,select到就緒的事件就處理,整個過程就無其他需要阻塞等待執行的地方。通常selector是一個單獨的線程。
還是以上面傳輸200M資料的場景,梳理下NIO的工作方式:
1、首先server端需要綁定IP+port,并向OS kernel 注冊連接配接事件,等待用戶端的連接配接請求。
2、client用戶端請求server位址,請求建立連接配接。
3、OS kernel 得知client網絡連接配接請求,并通知JVM,将連接配接事件放入事件池。作業系統核心OS kernel 有專門負責網絡資料傳輸的網卡,對于即将發生的網絡傳輸事件,作業系統核心會早于JVM得知;可讀可寫事件也類似。
4、運作在JVM上的Java application,selector線程select到連接配接事件,server端執行建立連接配接(ssc.accept())。
5、client完成三次握手。建立連接配接完成,也有一個對應的事件OP_CONNECT,OS kernel 也會把它放入事件池。
6、Java application的selector線程select到連接配接完成事件。
7、server端訂閱可讀事件(準備接收資料),告訴OS kernel 等資料準備好來通知我。
8、client發送200M資料,資料由OS kernel 網卡接收到核心緩沖區。
9、接收完成後,OS kernel 會通知JVM資料準備就緒,将資料可讀事件放入事件池。此時資料在核心緩沖區,不在JVM中。
10、Java application的selector線程select到可讀事件,通過NIO提供的channel将200M資料(從核心緩沖區)接收到JVM運作時資料區。此時server端接收client發送的資料完畢。
Java application通過NIO提供的channel copy資料,channel有網絡套接字/檔案Chanel等多種類型,channel是類似于Linux系統裡面的管道,是雙向通道。在使用channel時,Java application還會用到buffer,buffer也有多種類型。
Tomcat優化配置
Tomcat 預設單機配置下QPS 100-150
QPS150以上 延遲200ms
QPS300以上 延遲500ms 并有丢失連接配接。
Tomcat 可以配置成nio方式
config/server.xml中 将connector節點的protocol改成protocol="org.apache.coyote.http11.Http11NioProtocol"。
更高效的方式:
APR:通過JNI,用c語言實作的更高效的網絡資料交換方式。APR 是tomcat特有的。
AIO:和底層聯系更密切,selector都給省略了。
轉載請聯系原作者
https://www.jianshu.com/u/dd8907cc9fa5