NIO究竟牛X在哪?

在進(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ì):

  1. 利用多核。
  2. 當(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)在:

  1. 線程的創(chuàng)建和銷毀成本很高,在Linux這樣的操作系統(tǒng)中,線程本質(zhì)上就是一個(gè)進(jìn)程。創(chuàng)建和銷毀都是重量級(jí)的系統(tǒng)函數(shù)。
  2. 線程本身占用較大內(nèi)存,像Java的線程棧,一般至少分配512K~1M的空間,如果系統(tǒng)中的線程數(shù)過千,恐怕整個(gè)JVM的內(nèi)存都會(huì)被吃掉一半。
  3. 線程的切換成本是很高的。操作系統(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)。
  4. 容易造成鋸齒狀的系統(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

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

推薦閱讀更多精彩內(nèi)容

  • Spring Cloud為開發(fā)人員提供了快速構(gòu)建分布式系統(tǒng)中一些常見模式的工具(例如配置管理,服務(wù)發(fā)現(xiàn),斷路器,智...
    卡卡羅2017閱讀 134,868評(píng)論 18 139
  • NIO(Non-blocking I/O,在Java領(lǐng)域,也稱為New I/O),是一種同步非阻塞的I/O模型,也...
    閃電是只貓閱讀 3,139評(píng)論 0 7
  • 從三月份找實(shí)習(xí)到現(xiàn)在,面了一些公司,掛了不少,但最終還是拿到小米、百度、阿里、京東、新浪、CVTE、樂視家的研發(fā)崗...
    時(shí)芥藍(lán)閱讀 42,356評(píng)論 11 349
  • 今天學(xué)校舉行了畢業(yè)典禮,雖然說(shuō)是挺隆重的,但是明顯覺得是力不從心了。可能是因?yàn)榧倨诖蠹叶紱]有時(shí)間去排練節(jié)目,所以算...
    常樂閱讀 316評(píng)論 0 1
  • 他不愛你 只是需要你 李楠結(jié)婚了,在跟張潔分手三個(gè)月以后,毫無(wú)征兆地牽著另一個(gè)人的手走進(jìn)婚姻殿堂。六年的日夜享受,...
    哎喲音樂閱讀 260評(píng)論 0 0