35

浅尝Java NIO与Tomcat简单连接调优

 3 years ago
source link: http://mp.weixin.qq.com/s?__biz=MzAwMDczMjMwOQ%3D%3D&%3Bmid=2247483924&%3Bidx=1&%3Bsn=153d0f01ac2611659b8dc82626950fe5
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

P本文使用jdk1.8.0_45

spring boot 2.1.4.RELEASE 

涉及源码都放在

https://github.com/sabersword/Nio

前因

这周遇到一个连接断开的问题,便沿着这条线学习了一下Java NIO,顺便验证一下Tomcat作为spring boot默认的web容器,是怎样管理空闲连接的。

Java NIO(new IO/non-blockingIO)不同于BIO,BIO是堵塞型的,并且每一条学习路线的IO章节都会从BIO说起,因此大家非常熟悉。而NIO涉及Linux底层的select,poll,epoll等,要求对Linux的网络编程有扎实功底,反正我是没有搞清楚,在此推荐一篇通俗易懂的入门文章:

https://www.jianshu.com/p/ef418ccf2f7d

此处先引用文章的结论:

  • 对于socket的文件描述符才有所谓BIO和NIO。

  • 多线程+BIO模式会带来大量的资源浪费,而NIO+IO多路复用可以解决这个问题。

  • 在Linux下,基于epoll的IO多路复用是解决这个问题的最佳方案;epoll相比select和poll有很大的性能优势和功能优势,适合实现高性能网络服务。

底层的技术先交给大神们解决,我们着重从 Java 上层应用的角度了解一下。

JDK 1.5 起使用 epoll 代替了传统的 select/poll ,极大提升了 NIO 的通信性能,因此下文提到 Java NIO 都是使用 epoll 的。

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();
}

服务器日志

qyeeI37.png!web

客户端日志

ueu6bq2.jpg!web

从这个样例可以看到,客户端和服务器都能根据自身的策略,与对端断开连接,本例中是服务器首先断开连接,根据TCP协议,必然有一个时刻服务器处于FIN_WAIT_2状态,而客户端处于CLOSE_WAIT状态

UjUZNjj.jpg!web

我们通过 netstat 命令找出这个状态,果不其然。

vEvIvqF.png!web

但是 JDK 提供的 NIO 接口还是很复杂很难写的,要用好它就必须借助于 Netty Mina 等第三方库的封装,这部分就先不写了。接下来考虑另外一个问题,在大并发的场景下,成千上万的客户端涌入与服务器连接,连接成功后不发送请求,浪费了服务器宝贵的资源,这时服务器该如何应对?

答案当然是设计合适的连接池来管理这些宝贵的资源,为此我们选用 Tomcat 作为学习对象,了解一下它是如何管理空闲连接的。

Tomcat Connector 组件用于管理连接, Tomcat8 默认使用 Http11NioProtocol ,它有一个属性 ConnectionTimeout ,注释如下: JrIBrqj.png!web

可以简单理解成空闲超时时间,超时后 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 的连接

R7Nremr.jpg!web

根据 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 秒)的时间。

UJ7jyeA.jpg!web


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK