在進(jìn)入NIO之前,先回顧一下Java標(biāo)準(zhǔn)IO方式實(shí)現(xiàn)的網(wǎng)絡(luò)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);
}
}
}
這是一個(gè)經(jīng)典的每連接每線程的模型,之所以使用多線程,主要原因在于socket.accept()、socket.read()、socket.write()三個(gè)主要函數(shù)都是同步阻塞的,當(dāng)一個(gè)連接在處理I/O的時(shí)候,系統(tǒng)是阻塞的,如果是單線程的話必然就掛死在那里;但CPU是被釋放出來(lái)的,開啟多線程,就可以讓CPU去處理更多的事情。其實(shí)這也是所有使用多線程的本質(zhì):
- 利用多核。
- 當(dāng)I/O阻塞系統(tǒng),但CPU空閑的時(shí)候,可以利用多線程使用CPU資源。
現(xiàn)在的多線程一般都使用線程池,可以讓線程的創(chuàng)建和回收成本相對(duì)較低。在活動(dòng)連接數(shù)不是特別高(小于單機(jī)1000)的情況下,這種模型是比較不錯(cuò)的,可以讓每一個(gè)連接專注于自己的I/O并且編程模型簡(jiǎn)單,也不用過多考慮系統(tǒng)的過載、限流等問題。線程池本身就是一個(gè)天然的漏斗,可以緩沖一些系統(tǒng)處理不了的連接或請(qǐng)求。
不過,這個(gè)模型最本質(zhì)的問題在于,嚴(yán)重依賴于線程。但線程是很"貴"的資源,主要表現(xiàn)在:
- 線程的創(chuàng)建和銷毀成本很高,在Linux這樣的操作系統(tǒng)中,線程本質(zhì)上就是一個(gè)進(jìn)程。創(chuàng)建和銷毀都是重量級(jí)的系統(tǒng)函數(shù)。
- 線程本身占用較大內(nèi)存,像Java的線程棧,一般至少分配512K~1M的空間,如果系統(tǒng)中的線程數(shù)過千,恐怕整個(gè)JVM的內(nèi)存都會(huì)被吃掉一半。
- 線程的切換成本是很高的。操作系統(tǒng)發(fā)生線程切換的時(shí)候,需要保留線程的上下文,然后執(zhí)行系統(tǒng)調(diào)用。如果線程數(shù)過高,可能執(zhí)行線程切換的時(shí)間甚至?xí)笥诰€程執(zhí)行的時(shí)間,這時(shí)候帶來(lái)的表現(xiàn)往往是系統(tǒng)load偏高、CPU sy使用率特別高(超過20%以上),導(dǎo)致系統(tǒng)幾乎陷入不可用的狀態(tài)。
- 容易造成鋸齒狀的系統(tǒng)負(fù)載。因?yàn)橄到y(tǒng)負(fù)載是用活動(dòng)線程數(shù)或CPU核心數(shù),一旦線程數(shù)量高但外部網(wǎng)絡(luò)環(huán)境不是很穩(wěn)定,就很容易造成大量請(qǐng)求的結(jié)果同時(shí)返回,激活大量阻塞線程從而使系統(tǒng)負(fù)載壓力過大。
所以,當(dāng)面對(duì)十萬(wàn)甚至百萬(wàn)級(jí)連接的時(shí)候,傳統(tǒng)的BIO模型是無(wú)能為力的。隨著移動(dòng)端應(yīng)用的興起和各種網(wǎng)絡(luò)游戲的盛行,百萬(wàn)級(jí)長(zhǎng)連接日趨普遍,此時(shí),必然需要一種更高效的I/O處理模型。
BIO弱在哪里?
都說(shuō)NIO更高效,那BIO怎么就弱了呢?弱在哪里呢?現(xiàn)在通過上面BIO方式編寫的server一探究竟。
場(chǎng)景:假設(shè)客戶端在與server建立連接后,請(qǐng)求傳輸200M數(shù)據(jù)。
server端運(yùn)行在某服務(wù)器操作系統(tǒng)上,JVM在該服務(wù)器操作系統(tǒng)內(nèi)核(OS kernel)之上,而BIO方式編寫的server程序(Java application)則是跑在JVM上。
將經(jīng)歷以下步驟:
1、client請(qǐng)求發(fā)送數(shù)據(jù)
2、server端的Java application并不能直接開始接收數(shù)據(jù),而是需要等待 OS kernel 接收網(wǎng)絡(luò)數(shù)據(jù)傳輸?shù)木W(wǎng)卡準(zhǔn)備就緒,網(wǎng)卡是專門負(fù)責(zé)網(wǎng)絡(luò)數(shù)據(jù)傳輸?shù)摹?/p>
3、網(wǎng)卡就緒,執(zhí)行接收數(shù)據(jù)到OS kernel,此時(shí)數(shù)據(jù)需要完整地copy到操作系統(tǒng)內(nèi)核緩沖區(qū)中。這是第一次copy數(shù)據(jù),傳輸?shù)臅r(shí)間取決于傳輸數(shù)據(jù)的大小和網(wǎng)絡(luò)帶寬。(傳輸時(shí)間=數(shù)據(jù)大小/帶寬)
4、運(yùn)行在JVM上的Java應(yīng)用程序,在接收客戶端發(fā)送到數(shù)據(jù)時(shí)調(diào)用getInputStream(),但并不是立馬就能get到,需要等待操作系統(tǒng)內(nèi)核(網(wǎng)卡)已經(jīng)把數(shù)據(jù)接收(copy)完畢,且內(nèi)核準(zhǔn)備就緒。
5、內(nèi)核準(zhǔn)備就緒,會(huì)通過管道將數(shù)據(jù)全部復(fù)制到JVM中,這一次是將內(nèi)核緩沖區(qū)中的數(shù)據(jù)copy到JVM中(JVM運(yùn)行時(shí)數(shù)據(jù)區(qū))。
6、這時(shí)數(shù)據(jù)已全部存在在JVM中,server端應(yīng)用程序才能通過InputStream將數(shù)據(jù)傳輸?shù)絁ava application業(yè)務(wù)處理處,此時(shí)真正拿到client傳來(lái)的數(shù)據(jù)(也就是getInputStream()里面的內(nèi)容),執(zhí)行具體的業(yè)務(wù)邏輯處理。
還需要注意的是:java.io.inputstream 傳輸數(shù)據(jù)時(shí),數(shù)據(jù)必須是完整的。也就是說(shuō),上例中傳輸200M數(shù)據(jù),操作系統(tǒng)內(nèi)核必須全部接收好,一次性給我(JVM)。
看似簡(jiǎn)單的serverSocket.accept()后,開啟子線程,執(zhí)行socket.getInputStream()拿client傳過來(lái)的數(shù)據(jù),其實(shí)經(jīng)歷上面的步驟,Java application需要借助OS kernel 完成2次copy。這也是為什么這種方式通常是一個(gè)連接一個(gè)線程,2次copy受到網(wǎng)絡(luò)擁塞、網(wǎng)絡(luò)波動(dòng)等因素的影響。
基于事件、通知模型的NIO
提到事件、通知,大家自然會(huì)想到——觀察者模式,簡(jiǎn)單描述如下:
觀察者模式中三個(gè)組成角色,觀察者、被觀察者(服務(wù)提供者)、觀察的主題,也就是事件。觀察者首先需要訂閱感興趣的事件,然后當(dāng)事件發(fā)生時(shí),被觀察者會(huì)進(jìn)行通知。
基于事件、通知模型的NIO,就是基于此實(shí)現(xiàn)的。此實(shí)現(xiàn)非常巧妙,觀察者是JVM,被觀察者是OS kernel 。
JVM作為觀察者,它可以向OS kernel 訂閱連接事件、數(shù)據(jù)可讀事件、數(shù)據(jù)可寫事件。Java NIO提供了事件池Keys,當(dāng)訂閱的事件發(fā)生時(shí),OS kernel 就會(huì)通知JVM,并將該事件放入事件池當(dāng)中,而運(yùn)行在JVM上的Java application可以用NIO提供的selector從事件池中輪詢就緒的消息;輪詢到就緒的事件后即可直接執(zhí)行。
在JVM注冊(cè)事件后,只需要selector事件池就好了,select到就緒的事件就處理,整個(gè)過程就無(wú)其他需要阻塞等待執(zhí)行的地方。通常selector是一個(gè)單獨(dú)的線程。
還是以上面?zhèn)鬏?00M數(shù)據(jù)的場(chǎng)景,梳理下NIO的工作方式:
1、首先server端需要綁定IP+port,并向OS kernel 注冊(cè)連接事件,等待客戶端的連接請(qǐng)求。
2、client客戶端請(qǐng)求server地址,請(qǐng)求建立連接。
3、OS kernel 得知client網(wǎng)絡(luò)連接請(qǐng)求,并通知JVM,將連接事件放入事件池。操作系統(tǒng)內(nèi)核OS kernel 有專門負(fù)責(zé)網(wǎng)絡(luò)數(shù)據(jù)傳輸?shù)木W(wǎng)卡,對(duì)于即將發(fā)生的網(wǎng)絡(luò)傳輸事件,操作系統(tǒng)內(nèi)核會(huì)早于JVM得知;可讀可寫事件也類似。
4、運(yùn)行在JVM上的Java application,selector線程select到連接事件,server端執(zhí)行建立連接(ssc.accept())。
5、client完成三次握手。建立連接完成,也有一個(gè)對(duì)應(yīng)的事件OP_CONNECT,OS kernel 也會(huì)把它放入事件池。
6、Java application的selector線程select到連接完成事件。
7、server端訂閱可讀事件(準(zhǔn)備接收數(shù)據(jù)),告訴OS kernel 等數(shù)據(jù)準(zhǔn)備好來(lái)通知我。
8、client發(fā)送200M數(shù)據(jù),數(shù)據(jù)由OS kernel 網(wǎng)卡接收到內(nèi)核緩沖區(qū)。
9、接收完成后,OS kernel 會(huì)通知JVM數(shù)據(jù)準(zhǔn)備就緒,將數(shù)據(jù)可讀事件放入事件池。此時(shí)數(shù)據(jù)在內(nèi)核緩沖區(qū),不在JVM中。
10、Java application的selector線程select到可讀事件,通過NIO提供的channel將200M數(shù)據(jù)(從內(nèi)核緩沖區(qū))接收到JVM運(yùn)行時(shí)數(shù)據(jù)區(qū)。此時(shí)server端接收client發(fā)送的數(shù)據(jù)完畢。
Java application通過NIO提供的channel copy數(shù)據(jù),channel有網(wǎng)絡(luò)套接字/文件Chanel等多種類型,channel是類似于Linux系統(tǒng)里面的管道,是雙向通道。在使用channel時(shí),Java application還會(huì)用到buffer,buffer也有多種類型。
Tomcat優(yōu)化配置
Tomcat 默認(rèn)單機(jī)配置下QPS 100-150
QPS150以上 延遲200ms
QPS300以上 延遲500ms 并有丟失連接。
Tomcat 可以配置成nio方式
config/server.xml中 將connector節(jié)點(diǎn)的protocol改成protocol="org.apache.coyote.http11.Http11NioProtocol"。
更高效的方式:
APR:通過JNI,用c語(yǔ)言實(shí)現(xiàn)的更高效的網(wǎng)絡(luò)數(shù)據(jù)交換方式。APR 是tomcat特有的。
AIO:和底層聯(lián)系更密切,selector都給省略了。
轉(zhuǎn)載請(qǐng)聯(lián)系原作者http://www.lxweimin.com/u/dd8907cc9fa5