NioEndpoint核心组件,tomcat参数调优你用得着

8 篇文章 1 订阅
2 篇文章 0 订阅


本文参考Tomcat源码版本9.0.37

前言

NioEndpoint在Tomcat中扮演的角色

负责接收请求的连接,并封装成SocketProcessor,最后交给线程池去执行;NioEndpoint 组件是 I/O 多路复用模型的一种实现。其主体结构Acceptor+Poller+线程池是典型的主从Reactor多线程模型,其中Acceptor为主Reactor,Poller为从Reactor

Tomcat连接器模块下的三大核心功能之一,主要负责网络通信;连接器的另外两个核心功能分别是 应用层协议解析-ProcessorTomcat Request/Response与ServletRequest/ServletResponse的转化-Adapter

Acceptor与Poller
9.0.37版本来看,NioEndpoint结构中Acceptor与Poller仅支持定义单个Acceptor和单个Poller;在之前的版本中Acceptor和Poller都可以定义多个(比如8.5.38版本)


一、NioEndpoint总体概览

类结构

java.lang.Object
      org.apache.tomcat.util.net.AbstractEndpoint<S,U>
          org.apache.tomcat.util.net.AbstractJsseEndpoint<NioChannel,SocketChannel>
               org.apache.tomcat.util.net.NioEndpoint

Tomcat 的 NioEndpoint 包含 LimitLatchAcceptorPollerSocketProcessorExecutor 共 5 个组件;典型的主从Reactor多线程模型实现,

  • 其中LimitLatch负责限制连接请求;
  • Acceptor是主从结构中的“主”结构,仅负责接收并分发连接;
  • Poller是主从结构中的“从”结构,负责监听Acceptor分发连接的I/O事件,本质也就是Selector,当监听有I/O事件就绪后,将对应的连接封装成SocketProcessor交给线程池处理
  • SocketProcessor:具体连接的封装
  • Executor:真正处理连接的线程池
    在这里插入图片描述

二、NioEndpoint核心实现

1. LimitLatch组件

LimitLatch 用来控制连接个数,当连接数到达最大时阻塞线程,直到后续组件处理完一个连接后将连接数减 1。到达最大连接数后操作系统底层还是会接收客户端连接,但用户层已经不再接收,其中操作系统可接收的数量可通过acceptCount参数限制;

LimitLatch核心源码:

public class LimitLatch {
    private final LimitLatch.Sync sync;
    private final AtomicLong count;
    private volatile long limit;
    private volatile boolean released = false;
    // 新增一个连接累加1,线程可能会阻塞
    public void countUpOrAwait() throws InterruptedException {
        if (log.isDebugEnabled()) {
            log.debug("Counting up[" + Thread.currentThread().getName() + "] latch=" + this.getCount());
        }

        this.sync.acquireSharedInterruptibly(1);
    }
    // 释放一个连接 -1,前面阻塞的线程可能被唤醒
    public long countDown() {
        this.sync.releaseShared(0);
        long result = this.getCount();
        if (log.isDebugEnabled()) {
            log.debug("Counting down[" + Thread.currentThread().getName() + "] latch=" + result);
        }

        return result;
    }
     
    ...
     
    private class Sync extends AbstractQueuedSynchronizer {
        private static final long serialVersionUID = 1L;

        public Sync() {
        }
        // 如果当前连接数 count 小于 limit,线程能获取锁,返回 1,否则返回 -1
        protected int tryAcquireShared(int ignored) {
            long newCount = LimitLatch.this.count.incrementAndGet();
            if (!LimitLatch.this.released && newCount > LimitLatch.this.limit) {
                LimitLatch.this.count.decrementAndGet();
                return -1;
            } else {
                return 1;
            }
        }

        protected boolean tryReleaseShared(int arg) {
            LimitLatch.this.count.decrementAndGet();
            return true;
        }
    }
}

LimitLatch 内步自定义扩展了 AQSSync内部类

2. Acceptor组件

Acceptor 实现了 Runnable 接口,因此可以跑在单独线程里。一个端口号只能对应一个 ServerSocketChannel,因此这个 ServerSocketChannel 是在多个 Acceptor 线程之间共享的,它是 Endpoint 的属性,由 Endpoint 完成初始化和端口绑定,初始化过程:

    protected void initServerSocket() throws Exception {
        if (!this.getUseInheritedChannel()) {
            this.serverSock = ServerSocketChannel.open();
            this.socketProperties.setProperties(this.serverSock.socket());
            InetSocketAddress addr = new InetSocketAddress(this.getAddress(), this.getPortWithOffset());
            this.serverSock.socket().bind(addr, this.getAcceptCount());
        } else {
           ...
        }
        this.serverSock.configureBlocking(true);
    }

初始化反映的两个重要信息

  • bind 方法的第二个参数表示操作系统的等待队列长度;当应用层面的连接数到达最大值时,操作系统可以继续接收连接,那么操作系统能继续接收的最大连接数就是这个队列长度,可以通过 acceptCount 参数配置,默认是 100。
  • ServerSocketChannel 被设置成阻塞模式,它是以阻塞的方式接收连接的

Acceptor核心源码:

public class Acceptor<U> implements Runnable {

    private final AbstractEndpoint<?, U> endpoint;
    private String threadName;
    protected volatile Acceptor.AcceptorState state;

    ...
    
    public void run() {
        while(this.endpoint.isRunning()) {
            while(this.endpoint.isPaused() && this.endpoint.isRunning()) {
 
            ...

            try {
                // 由limitLatch限制连接,可能阻塞
                this.endpoint.countUpOrAwaitConnection();
                if (!this.endpoint.isPaused()) {
                    Object socket = null;

                    try {
                        // 阻塞的接受socket连接(初始化的时候设置成阻塞的)
                        socket = this.endpoint.serverSocketAccept();
                    } catch (Exception var6) {
                        this.endpoint.countDownConnection();
                        if (!this.endpoint.isRunning()) {
                            break;
                        }

                        this.handleExceptionWithDelay(errorDelay);
                        throw var6;
                    }

                    errorDelay = 0;
                    if (this.endpoint.isRunning() && !this.endpoint.isPaused()) {
                        // 实质上就是将新连接放入poller队列
                        if (!this.endpoint.setSocketOptions(socket)) {
                            this.endpoint.closeSocket(socket);
                        }
                    } else {
                        this.endpoint.destroySocket(socket);
                    }
                }
            } catch (Throwable var7) {
            
              ...
              
            }
        }

        this.state = Acceptor.AcceptorState.ENDED;
    }
    ...
}

ServerSocketChannel 通过 accept() 接受新的连接,accept() 方法返回获得 SocketChannel 对象,然后将 SocketChannel 对象封装在一个 PollerEvent 对象中,并将 PollerEvent 对象压入 Poller 的 Queue

3. Poller组件

Poller 本质是一个 Selector,它内部维护一个 Queue

 private final SynchronizedQueue<NioEndpoint.PollerEvent> events = new SynchronizedQueue();
  • SynchronizedQueue 的方法比如 offer、poll、size 和 clear 方法,都使用了 synchronized 关键字进行修饰,用来保证同一时刻只有一个 Acceptor 线程对 Queue 进行读写。同时有多个 Poller 线程在运行,每个 Poller 线程都有自己的 Queue。每个 Poller 线程可能同时被多个 Acceptor 线程调用来注册 PollerEvent。同样 Poller 的个数可以通过 pollers 参数配置。

  • Poller 不断的通过内部的 Selector 对象向内核查询 Channel 的状态,一旦可读就生成任务类 SocketProcessor 交给 Executor 去处理。Poller 的另一个重要任务是循环遍历检查自己所管理的 SocketChannel 是否已经超时,如果有超时就关闭这个 SocketChannel

Poller核心实现:

    public class Poller implements Runnable {
        private Selector selector = Selector.open();
        private final SynchronizedQueue<NioEndpoint.PollerEvent> events = new SynchronizedQueue();

        ...
        // 将socket包装成PollerEvent事件放入队列
        public void register(NioChannel socket, NioEndpoint.NioSocketWrapper socketWrapper) {
            
            ...
            
            if (event == null) {
                event = new NioEndpoint.PollerEvent(socket, 256);
            } else {
                event.reset(socket, 256);
            }

            this.addEvent(event);
        }

        // poller核心实现,也就是这里扮演着selector的角色,IO多路复用的select()实现
        public void run() {
            while(true) {
                boolean hasEvents = false;
                // 第一步,检测是否有准备就绪的IO事件
                label59: {
                    try {
                        if (!this.close) {
                            hasEvents = this.events();
                            if (this.wakeupCounter.getAndSet(-1L) > 0L) {
                                this.keyCount = this.selector.selectNow();
                            } else {
                                this.keyCount = this.selector.select(NioEndpoint.this.selectorTimeout);
                            }

                            this.wakeupCounter.set(0L);
                        }

                        ...
                        
                    } catch (Throwable var6) {
                        ...
                    }
                }
                ...
            
                // 第二步处理已经就绪的IO事件
                Iterator iterator = this.keyCount > 0 ? this.selector.selectedKeys().iterator() : null;
                while(iterator != null && iterator.hasNext()) {
                    SelectionKey sk = (SelectionKey)iterator.next();
                    NioEndpoint.NioSocketWrapper socketWrapper = (NioEndpoint.NioSocketWrapper)sk.attachment();
                    if (socketWrapper == null) {
                        iterator.remove();
                    } else {
                        iterator.remove();
                        // 封装成NioSocketWrapper,最终交给线程池处理
                        this.processKey(sk, socketWrapper);
                    }
                }
                ...
            }
        }

    }

4. SocketProcessor

Poller 会创建 SocketProcessor 任务类交给线程池处理,而 SocketProcessor 实现了 Runnable 接口,用来定义 Executor 中线程所执行的任务,主要就是调用 Http11Processor 组件来处理请求。Http11Processor 读取 Channel 的数据来生成 ServletRequest 对象,

Http11Processor 并不是直接读取 Channel 的。这是因为 Tomcat 支持同步非阻塞 I/O 模型和异步 I/O 模型,在 Java API 中,相应的 Channel 类也是不一样的,比如有 AsynchronousSocketChannelSocketChannel,为了对 Http11Processor 屏蔽这些差异,Tomcat 设计了一个包装类叫作 SocketWrapper,Http11Processor 只调用 SocketWrapper 的方法去读写数据。

5. Executor

Executor 是 Tomcat 定制版的线程池,它负责创建真正干活的工作线程,执行 SocketProcessorrun 方法,也就是解析请求并通过容器来处理请求,最终会调用到我们的 Servlet

三、总结

1、NioEndpoint 组件的主要工作就是处理 I/O,也就是Tomcat服务中工作第一站 - 网络通信

2、NioEndpoint 利用 Java NIO API 实现了多路复用 I/O 模型

3、连接控制器 - LimitLatch,作为一个服务器端的一个服务,不可能无限制地接收客户端的连接;Tomcat使用LimitLatch限制连接数,BIO与NIO都是用这个组件,它是基于AQS并发框架实现的

4、Acceptor的主要职责是监听是否有客户端进来并接收连接,为操作简单,作为服务端通道的ServerSocketChannel默认设置为阻塞模式,也就是说accept操作是阻塞的;

Acceptor负责从ACCEPT队列中取出连接,当Acceptor处理不过来时,连接就堆积在ACCEPT队列中,这个队列长度也可以通过acceptCount参数设置;

TCP三次握手建立连接的过程中,内核通常会为每一个LISTEN状态的Socket维护两个队列:

  • SYN队列(半连接队列):这些连接已经接到客户端SYN;
  • ACCEPT队列(全连接队列):这些连接已经接到客户端的ACK,完成了三次握手,等待被accept系统调用取走;其中这个等待队列长度可以通过参数acceptCount控制

5、连接轮询器 - Poller,负责轮事件列表(连接),一旦发现相应的IO事件则状态就绪就封装成SocketProcessor,并交给线程池处理

读写数据的线程自己不会阻塞在 I/O 等待上,而是把这个工作交给 Selector(也就是Poller组件来完成)

6、任务处理器 - SocketProcessor

7、任务执行器 - Executor











相关参考:

极客时间 深入拆解Tomcat&Jetty[李号双]
Tomcat架构解析[刘光瑞]
Tomcat内核设计剖析[汪建]

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
Tomcat 的性能参数可以提高其稳定性和响应速度,以下是一些常用的方法: 1. 增加 JVM 内存:通过增加 JVM 内存可以提高 Tomcat 的性能。可以通过设置JAVA_OPTS环境变量或修改 catalina.sh/catalina.bat 文件来增加内存。 2. 整线程池大小:Tomcat 默认的线程池大小为 200,可以根据具体需求整线程池大小。可以通过修改 server.xml 文件中的 Connector 元素的 maxThreads 属性来实现。 3. 启用 HTTP/2:HTTP/2 可以提高网站的性能和响应速度。Tomcat 8 及以上版本支持 HTTP/2,可以通过修改 server.xml 文件中的 Connector 元素的 protocol 属性来启用。 4. 使用 GZIP 压缩:启用 GZIP 压缩可以减少页面加载时间。可以通过修改 server.xml 文件中的 Connector 元素的 compression 属性来启用。 5. 整缓存大小:Tomcat 默认的缓存大小为 10MB,可以根据具体需求整缓存大小。可以通过修改 server.xml 文件中的 Context 元素的 cacheMaxSize 属性来实现。 6. 禁用 DNS 反向解析:DNS 反向解析可能会导致性能问题,可以通过修改 server.xml 文件中的 Connector 元素的 enableLookups 属性来禁用。 7. 关闭不必要的 Valve:Valve 可能会影响性能,可以关闭不必要的 Valve。可以通过修改 server.xml 文件中的 Context 元素的 Valve 元素来实现。 以上是一些常用的 Tomcat 性能参数方法,具体方法需要根据实际情况进行选择。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

柏油

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值