NIO究竟牛X在哪?

在進(jìn)入NIO之前,先回顧一下Java標(biāo)準(zhǔn)IO方式實現(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);
    }
  }
}

這是一個經(jīng)典的每連接每線程的模型,之所以使用多線程,主要原因在于socket.accept()、socket.read()、socket.write()三個主要函數(shù)都是同步阻塞的,當(dāng)一個連接在處理I/O的時候,系統(tǒng)是阻塞的,如果是單線程的話必然就掛死在那里;但CPU是被釋放出來的,開啟多線程,就可以讓CPU去處理更多的事情。其實這也是所有使用多線程的本質(zhì):

  1. 利用多核。
  2. 當(dāng)I/O阻塞系統(tǒng),但CPU空閑的時候,可以利用多線程使用CPU資源。

現(xiàn)在的多線程一般都使用線程池,可以讓線程的創(chuàng)建和回收成本相對較低。在活動連接數(shù)不是特別高(小于單機1000)的情況下,這種模型是比較不錯的,可以讓每一個連接專注于自己的I/O并且編程模型簡單,也不用過多考慮系統(tǒng)的過載、限流等問題。線程池本身就是一個天然的漏斗,可以緩沖一些系統(tǒng)處理不了的連接或請求。

不過,這個模型最本質(zhì)的問題在于,嚴(yán)重依賴于線程。但線程是很"貴"的資源,主要表現(xiàn)在:

  1. 線程的創(chuàng)建和銷毀成本很高,在Linux這樣的操作系統(tǒng)中,線程本質(zhì)上就是一個進(jìn)程。創(chuàng)建和銷毀都是重量級的系統(tǒng)函數(shù)。
  2. 線程本身占用較大內(nèi)存,像Java的線程棧,一般至少分配512K~1M的空間,如果系統(tǒng)中的線程數(shù)過千,恐怕整個JVM的內(nèi)存都會被吃掉一半。
  3. 線程的切換成本是很高的。操作系統(tǒng)發(fā)生線程切換的時候,需要保留線程的上下文,然后執(zhí)行系統(tǒng)調(diào)用。如果線程數(shù)過高,可能執(zhí)行線程切換的時間甚至?xí)笥诰€程執(zhí)行的時間,這時候帶來的表現(xiàn)往往是系統(tǒng)load偏高、CPU sy使用率特別高(超過20%以上),導(dǎo)致系統(tǒng)幾乎陷入不可用的狀態(tài)。
  4. 容易造成鋸齒狀的系統(tǒng)負(fù)載。因為系統(tǒng)負(fù)載是用活動線程數(shù)或CPU核心數(shù),一旦線程數(shù)量高但外部網(wǎng)絡(luò)環(huán)境不是很穩(wěn)定,就很容易造成大量請求的結(jié)果同時返回,激活大量阻塞線程從而使系統(tǒng)負(fù)載壓力過大。

所以,當(dāng)面對十萬甚至百萬級連接的時候,傳統(tǒng)的BIO模型是無能為力的。隨著移動端應(yīng)用的興起和各種網(wǎng)絡(luò)游戲的盛行,百萬級長連接日趨普遍,此時,必然需要一種更高效的I/O處理模型。

BIO弱在哪里?

都說NIO更高效,那BIO怎么就弱了呢?弱在哪里呢?現(xiàn)在通過上面BIO方式編寫的server一探究竟。


場景:假設(shè)客戶端在與server建立連接后,請求傳輸200M數(shù)據(jù)。
server端運行在某服務(wù)器操作系統(tǒng)上,JVM在該服務(wù)器操作系統(tǒng)內(nèi)核(OS kernel)之上,而BIO方式編寫的server程序(Java application)則是跑在JVM上。

將經(jīng)歷以下步驟:
1、client請求發(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ù)據(jù)需要完整地copy到操作系統(tǒng)內(nèi)核緩沖區(qū)中。這是第一次copy數(shù)據(jù),傳輸?shù)臅r間取決于傳輸數(shù)據(jù)的大小和網(wǎng)絡(luò)帶寬。(傳輸時間=數(shù)據(jù)大小/帶寬)

4、運行在JVM上的Java應(yīng)用程序,在接收客戶端發(fā)送到數(shù)據(jù)時調(diào)用getInputStream(),但并不是立馬就能get到,需要等待操作系統(tǒng)內(nèi)核(網(wǎng)卡)已經(jīng)把數(shù)據(jù)接收(copy)完畢,且內(nèi)核準(zhǔn)備就緒。

5、內(nèi)核準(zhǔn)備就緒,會通過管道將數(shù)據(jù)全部復(fù)制到JVM中,這一次是將內(nèi)核緩沖區(qū)中的數(shù)據(jù)copy到JVM中(JVM運行時數(shù)據(jù)區(qū))。

6、這時數(shù)據(jù)已全部存在在JVM中,server端應(yīng)用程序才能通過InputStream將數(shù)據(jù)傳輸?shù)絁ava application業(yè)務(wù)處理處,此時真正拿到client傳來的數(shù)據(jù)(也就是getInputStream()里面的內(nèi)容),執(zhí)行具體的業(yè)務(wù)邏輯處理。

還需要注意的是:java.io.inputstream 傳輸數(shù)據(jù)時,數(shù)據(jù)必須是完整的。也就是說,上例中傳輸200M數(shù)據(jù),操作系統(tǒng)內(nèi)核必須全部接收好,一次性給我(JVM)。

看似簡單的serverSocket.accept()后,開啟子線程,執(zhí)行socket.getInputStream()拿client傳過來的數(shù)據(jù),其實經(jīng)歷上面的步驟,Java application需要借助OS kernel 完成2次copy。這也是為什么這種方式通常是一個連接一個線程,2次copy受到網(wǎng)絡(luò)擁塞、網(wǎng)絡(luò)波動等因素的影響。

基于事件、通知模型的NIO

提到事件、通知,大家自然會想到——觀察者模式,簡單描述如下:


觀察者模式中三個組成角色,觀察者、被觀察者(服務(wù)提供者)、觀察的主題,也就是事件。觀察者首先需要訂閱感興趣的事件,然后當(dāng)事件發(fā)生時,被觀察者會進(jìn)行通知。

基于事件、通知模型的NIO,就是基于此實現(xiàn)的。此實現(xiàn)非常巧妙,觀察者是JVM,被觀察者是OS kernel 。

JVM作為觀察者,它可以向OS kernel 訂閱連接事件、數(shù)據(jù)可讀事件、數(shù)據(jù)可寫事件。Java NIO提供了事件池Keys,當(dāng)訂閱的事件發(fā)生時,OS kernel 就會通知JVM,并將該事件放入事件池當(dāng)中,而運行在JVM上的Java application可以用NIO提供的selector從事件池中輪詢就緒的消息;輪詢到就緒的事件后即可直接執(zhí)行。

在JVM注冊事件后,只需要selector事件池就好了,select到就緒的事件就處理,整個過程就無其他需要阻塞等待執(zhí)行的地方。通常selector是一個單獨的線程。


還是以上面?zhèn)鬏?00M數(shù)據(jù)的場景,梳理下NIO的工作方式:

1、首先server端需要綁定IP+port,并向OS kernel 注冊連接事件,等待客戶端的連接請求。

2、client客戶端請求server地址,請求建立連接。

3、OS kernel 得知client網(wǎng)絡(luò)連接請求,并通知JVM,將連接事件放入事件池。操作系統(tǒng)內(nèi)核OS kernel 有專門負(fù)責(zé)網(wǎng)絡(luò)數(shù)據(jù)傳輸?shù)木W(wǎng)卡,對于即將發(fā)生的網(wǎng)絡(luò)傳輸事件,操作系統(tǒng)內(nèi)核會早于JVM得知;可讀可寫事件也類似。

4、運行在JVM上的Java application,selector線程select到連接事件,server端執(zhí)行建立連接(ssc.accept())。

5、client完成三次握手。建立連接完成,也有一個對應(yīng)的事件OP_CONNECT,OS kernel 也會把它放入事件池。

6、Java application的selector線程select到連接完成事件。

7、server端訂閱可讀事件(準(zhǔn)備接收數(shù)據(jù)),告訴OS kernel 等數(shù)據(jù)準(zhǔn)備好來通知我。

8、client發(fā)送200M數(shù)據(jù),數(shù)據(jù)由OS kernel 網(wǎng)卡接收到內(nèi)核緩沖區(qū)。

9、接收完成后,OS kernel 會通知JVM數(shù)據(jù)準(zhǔn)備就緒,將數(shù)據(jù)可讀事件放入事件池。此時數(shù)據(jù)在內(nèi)核緩沖區(qū),不在JVM中。

10、Java application的selector線程select到可讀事件,通過NIO提供的channel將200M數(shù)據(jù)(從內(nèi)核緩沖區(qū))接收到JVM運行時數(shù)據(jù)區(qū)。此時server端接收client發(fā)送的數(shù)據(jù)完畢。

Java application通過NIO提供的channel copy數(shù)據(jù),channel有網(wǎng)絡(luò)套接字/文件Chanel等多種類型,channel是類似于Linux系統(tǒng)里面的管道,是雙向通道。在使用channel時,Java application還會用到buffer,buffer也有多種類型。




Tomcat優(yōu)化配置

Tomcat 默認(rèn)單機配置下QPS 100-150
QPS150以上 延遲200ms
QPS300以上 延遲500ms 并有丟失連接。

Tomcat 可以配置成nio方式

config/server.xml中 將connector節(jié)點的protocol改成protocol="org.apache.coyote.http11.Http11NioProtocol"。

更高效的方式:
APR:通過JNI,用c語言實現(xiàn)的更高效的網(wǎng)絡(luò)數(shù)據(jù)交換方式。APR 是tomcat特有的。
AIO:和底層聯(lián)系更密切,selector都給省略了。



轉(zhuǎn)載請聯(lián)系原作者http://m.itdecent.cn/u/dd8907cc9fa5

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

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

友情鏈接更多精彩內(nèi)容