淺嘗Java NIO與Tomcat鏈接調優

本文使用jdk1.8.0_45和spring boot 2.1.4.RELEASEjava

涉及源碼都放在https://github.com/sabersword/Niogit

前因

這周遇到一個鏈接斷開的問題,便沿着這條線學習了一下Java NIO,順便驗證一下Tomcat做爲spring boot默認的web容器,是怎樣管理空閒鏈接的。github

Java NIO(new IO/non-blocking IO)不一樣於BIO,BIO是堵塞型的,而且每一條學習路線的IO章節都會從BIO提及,所以你們很是熟悉。而NIO涉及Linux底層的select,poll,epoll等,要求對Linux的網絡編程有紮實功底,反正我是沒有搞清楚,在此推薦一篇通俗易懂的入門文章聊聊BIO,NIO和AIOweb

此處先引用文章的結論:spring

  • 對於socket的文件描述符纔有所謂BIO和NIO。
  • 多線程+BIO模式會帶來大量的資源浪費,而NIO+IO多路複用能夠解決這個問題。
  • 在Linux下,基於epoll的IO多路複用是解決這個問題的最佳方案;epoll相比select和poll有很大的性能優點和功能優點,適合實現高性能網絡服務。

底層的技術先交給大神們解決,咱們着重從Java上層應用的角度瞭解一下。編程

從JDK 1.5起使用epoll代替了傳統的select/poll,極大提高了NIO的通訊性能,所以下文提到Java NIO都是使用epoll的。tomcat

Java NIO涉及到的三大核心部分Channel、Buffer、Selector,它們都十分複雜,單單其中一部分都能寫成一篇文章,就不班門弄斧了。此處貼上一個本身學習NIO時設計的樣例,功能是服務器發佈服務,客戶端連上服務器,客戶端向服務器發送若干次請求,達到若干次答覆後,服務器率先斷開鏈接,隨後客戶端也斷開鏈接。服務器

NIO服務器核心代碼網絡

public void handleRead(SelectionKey key) {
    SocketChannel sc = (SocketChannel) key.channel();
    ByteBuffer buf = (ByteBuffer) key.attachment();
    try {
        long bytesRead = sc.read(buf);
        StringBuffer sb = new StringBuffer();
        while (bytesRead > 0) {
            buf.flip();
            while (buf.hasRemaining()) {
                sb.append((char) buf.get());
            }
            buf.clear();
            bytesRead = sc.read(buf);
        }
        LOGGER.info("收到客戶端的消息:{}", sb.toString());
        writeResponse(sc, sb.toString());
        if (sb.toString().contains("3")) {
            sc.close();
        }
    } catch (IOException e) {
        key.cancel();
        e.printStackTrace();
        LOGGER.info("疑似一個客戶端斷開鏈接");
        try {
            sc.close();
        } catch (IOException e1) {
            LOGGER.info("SocketChannel 關閉異常");
        }
    }
}
複製代碼

NIO客戶端核心代碼多線程

Iterator<SelectionKey> iter = selector.selectedKeys().iterator();
while (iter.hasNext()) {
    SelectionKey key = iter.next();
    if (key.isConnectable()) {
        while (!socketChannel.finishConnect()) ;
        socketChannel.configureBlocking(false);
        socketChannel.register(key.selector(), SelectionKey.OP_READ, ByteBuffer.allocateDirect(1024));
        LOGGER.info("與服務器鏈接成功,使用本地端口{}", socketChannel.socket().getLocalPort());
    }
    if (key.isReadable()) {
        SocketChannel sc = (SocketChannel) key.channel();
        ByteBuffer buf = (ByteBuffer) key.attachment();
        long bytesRead;
        try {
            bytesRead = sc.read(buf);
        } catch (IOException e) {
            e.printStackTrace();
            LOGGER.info("遠程服務器斷開了與本機的鏈接,本機也進行斷開");
            sc.close();
            continue;
        }
        while (bytesRead > 0) {
            buf.flip();
            while (buf.hasRemaining()) {
                System.out.print((char) buf.get());
            }
            System.out.println();
            buf.clear();
            bytesRead = sc.read(buf);
        }
        TimeUnit.SECONDS.sleep(2);
        String info = "I'm " + i++ + "-th information from client";
        buffer.clear();
        buffer.put(info.getBytes());
        buffer.flip();
        while (buffer.hasRemaining()) {
            sc.write(buffer);
        }
    }
    iter.remove();
}
複製代碼

服務器日誌

客戶端日誌

從這個樣例能夠看到,客戶端和服務器都能根據自身的策略,與對端斷開鏈接,本例中是服務器首先斷開鏈接,根據TCP協議,必然有一個時刻服務器處於FIN_WAIT_2狀態,而客戶端處於CLOSE_WAIT狀態

咱們經過netstat命令找出這個狀態,果不其然。

可是JDK提供的NIO接口仍是很複雜很難寫的,要用好它就必須藉助於Netty、Mina等第三方庫的封裝,這部分就先不寫了。

接下來考慮另一個問題,在大併發的場景下,成千上萬的客戶端涌入與服務器鏈接,鏈接成功後不發送請求,浪費了服務器寶貴的資源,這時服務器該如何應對?

答案固然是設計合適的鏈接池來管理這些寶貴的資源,爲此咱們選用Tomcat做爲學習對象,瞭解一下它是如何管理空閒鏈接的。

Tomcat的Connector組件用於管理鏈接,Tomcat8默認使用Http11NioProtocol,它有一個屬性ConnectionTimeout,註釋以下:

/* * When Tomcat expects data from the client, this is the time Tomcat will * wait for that data to arrive before closing the connection. */
複製代碼

能夠簡單理解成空閒超時時間,超時後Tomcat會主動關閉該鏈接來回收資源。 咱們將它修改成10秒,獲得以下配置類,並將該spring boot應用打包成tomcat-server.jar

@Component
public class MyEmbeddedServletContainerFactory extends TomcatServletWebServerFactory {

    public WebServer getWebServer(ServletContextInitializer... initializers) {
        // 設置端口
        this.setPort(8080);
        return super.getWebServer(initializers);
    }

    protected void customizeConnector(Connector connector) {
        super.customizeConnector(connector);
        Http11NioProtocol protocol = (Http11NioProtocol) connector.getProtocolHandler();
        // 設置最大鏈接數
        protocol.setMaxConnections(2000);
        // 設置最大線程數
        protocol.setMaxThreads(2000);
        // 設置鏈接空閒超時
        protocol.setConnectionTimeout(10 * 1000);
    }
}
複製代碼

咱們將上文的NIO客戶端略微修改一下造成TomcatClient,功能就是連上服務器後什麼都不作。

Iterator<SelectionKey> iter = selector.selectedKeys().iterator();
while (iter.hasNext()) {
    SelectionKey key = iter.next();
    if (key.isConnectable()) {
        while (!socketChannel.finishConnect()) ;
        socketChannel.configureBlocking(false);
        socketChannel.register(key.selector(), SelectionKey.OP_READ, ByteBuffer.allocateDirect(1024));
        LOGGER.info("與遠程服務器鏈接成功,使用本地端口{}", socketChannel.socket().getLocalPort());
    }
    if (key.isReadable()) {
        SocketChannel sc = (SocketChannel) key.channel();
        ByteBuffer buf = (ByteBuffer) key.attachment();
        long readCount;
        readCount = sc.read(buf);
        while (readCount > 0) {
            buf.flip();
            while (buf.hasRemaining()) {
                System.out.print((char) buf.get());
            }
            System.out.println();
            buf.clear();
            readCount = sc.read(buf);
        }
        // 遠程服務器斷開鏈接後會不停觸發OP_READ,並收到-1表明End-Of-Stream
        if (readCount == -1) {
            LOGGER.info("遠程服務器斷開了與本機的鏈接,本機也進行斷開");
            sc.close();
        }
    }
    iter.remove();
}
複製代碼

分別運行服務器和客戶端,能夠看到客戶端打印以下日誌

30:27連上服務器,不進行任何請求,通過10秒後到30:37被服務器斷開了鏈接。

此時netstat會發現還有一個TIME_WAIT的鏈接

根據TCP協議主動斷開方必須等待2MSL才能關閉鏈接,Linux默認的2MSL=60秒(順帶說一句網上不少資料說CentOS的/proc/sys/net/ipv4/tcp_fin_timeout能修改2MSL的時間,實際並無效果,這個參數應該是被寫進內核,必須從新編譯內核才能修改2MSL)。持續觀察netstat發現31:36的時候TIME_WAIT鏈接還在,到了31:38鏈接消失了,能夠認爲是31:37關閉鏈接,對比上文30:37恰好通過了2MSL(默認60秒)的時間。

相關文章
相關標籤/搜索