Tomcat acceptCount maxConnections maxThread 参数意义

缘起

问题的开始,是一次线上问题,某台服务器连接数告警,登上去一看,果然连接都卡在上边一个关键服务上 。OK,jstack 看一下请求都卡在哪里了。查看了线程栈信息之后,发现大多线程都阻塞在对某一个上游服务的请求上。原来,我们的一些接口是依赖某一个上游服务的,然而这个上游服务因为某些历史原因,性能不是很好,请求量不是很大的情况下还好,请求量一大的情况下响应会比较慢。

那目前这个上游服务我们一时半会优化不了,线上服务用不了,是火烧眉毛的事,咋整呢?考虑到我们不是所有接口都依赖这个上游服务的,最差情况,至少得让我们其他的接口可用吧。

基于此,大笔一挥,在配置文件中写下如下几行:

server.tomcat.maxConnections=XXX
server.tomcat.acceptCount=XXX
server.tomcat.maxThreads=XXX
# XXX 代表数字,具体是多少?这事儿不能说太细,个人可以根据自己的服务器配置来决定。

重启服务,OK了,其他接口可用了,至于上游服务,我们慢慢优化。

那么,这几个参数是什么意思呢?为什么改了就有用呢?在具体讲之前,得说一个坑。。

我一般查询这些配置参数,都是去 spring-autoconfigure 里面直接查看源代码,但是!我查看的是 1.5.8 的源码,里头这三个参数是都有的

		/**
		 * Maximum amount of worker threads.
		 */
		 
		private int maxThreads = 0; // Number of threads in protocol handler
		
		/**
		 * Maximum number of connections that the server will accept and process at any
		 * given time. Once the limit has been reached, the operating system may still
		 * accept connections based on the "acceptCount" property.
		 */
		private int maxConnections = 0;

		/**
		 * Maximum queue length for incoming connection requests when all possible request
		 * processing threads are in use.
		 */
		private int acceptCount = 0;

然鹅,我们线上这个服务是基于 1.3.1 的,org.springframework.boot.autoconfigure.web.ServerProperties$Tomcat 里头是只有一个 maxThreads 的。。。那为毛只改了一个 maxThreads 也能解决问题呢?了解完这仨参数都是干啥用的再去讨论。

Tomcat 架构

这段的标题起的非常大,但是具体问题具体分析嘛,所以我们只关注 Connector 这个组件的 NIO 实现,具体代码在 org.apache.tomcat.util.net.NioEndpoint 里面,有兴趣的朋友可以去看看,先从网上盗个图:

输入图片说明

这里头主要有三个组件 Acceptor Poller Worker,它们的主要职责如下(我们关注的):

Acceptor

不断从 accept 队列取出连接,丢到相应的 Poller 里,这个过程与我之前处理 websocket 消息的方法基本一样,Poller 本身维护了一个 Event 队列,Acceptor 将 SocketChannel 间接封装在一个 PollerEvent 里,丢到相应的 Poller 里,具体丢到哪个 Poller ,是用一个计数器对 Poller 数组长度取模得出的:

    /**
     * The socket poller.
     */
    private Poller[] pollers = null;
    private AtomicInteger pollerRotater = new AtomicInteger(0);
    /**
     * Return an available poller in true round robin fashion
     */
    public Poller getPoller0() {
        int idx = Math.abs(pollerRotater.incrementAndGet()) % pollers.length;
        return pollers[idx];
    }
    

Poller

Poller 主要做两件事:

  1. 从 events 中取出 SocketChannel, 以OP_READ事件注册到 Poller 内部的 selector 上。
  2. 调用 selector 的 select 方法,遍历出可读的 socket,最终封装为一个 SocketProcessor 对象,丢到 worker 线程池里交给 worker 处理。

Worker

这个里面其实也挺复杂的,但是我们只要知道 soket 的具体读写,我们在 Controller 中写的业务逻辑都是在 worker 线程中处理的就好了,上游服务调用阻塞也发生在这个线程内就OK了。

acceptCount

acceptCount 最终用到的地方在 org.apache.tomcat.util.net.NioEndpoint 的 bind 方法中:

    serverSock.socket().bind(addr,getAcceptCount());

bind 方法的 doc 里是这么说的:

The {@code backlog} argument is the requested maximum number of
     * pending connections on the socket. Its exact semantics are implementation
     * specific. In particular, an implementation may impose a maximum length
     * or may choose to ignore the parameter altogther. The value provided
     * should be greater than {@code 0}. If it is less than or equal to
     * {@code 0}, then an implementation specific default will be used.

恩,就是 backlog 嘛,这个 exact semantics 已经在前文我们分析过了,不再赘述。

maxConnections

先看看 ServerProperties 中这个字段的注释:

Maximum number of connections that the server will accept and process at any given time. 
Once the limit has been reached, the operating system may still accept connections based on the "acceptCount" property.

那它是咋用的呢?在 NioEndpoint 中,使用这个值初始化了一个 LimitLatch, init方法如下:

    protected LimitLatch initializeConnectionLatch() {
        if (maxConnections==-1) return null;
        if (connectionLimitLatch==null) {
            connectionLimitLatch = new LimitLatch(getMaxConnections());
        }
        return connectionLimitLatch;
    }

我们已经知道,Acceptor 在一个 while 循环里不断地从 accept 队列中获取可用连接,获取前调用一个 countUpOrAwaitConnection 方法,连接释放后调用一个 countDownConnection 方法,代码如下:


    protected void countUpOrAwaitConnection() throws InterruptedException {
        if (maxConnections==-1) return;
        LimitLatch latch = connectionLimitLatch;
        if (latch!=null) latch.countUpOrAwait();
    }

    protected long countDownConnection() {
        if (maxConnections==-1) return -1;
        LimitLatch latch = connectionLimitLatch;
        if (latch!=null) {
            long result = latch.countDown();
            if (result<0) {
                getLog().warn(sm.getString("endpoint.warn.incorrectConnectionCount"));
            }
            return result;
        } else return -1;
    }
    

在 latch 释放至 maxConnections 之前,Acceptor 会一直阻塞,直到有连接释放,调用了 countDownConnection 方法为止。这期间 Acceptor 是不会去 accept 新的连接的。那现在 NioPoint 中已经存在的连接是 maxConnections 个,accept 队列(或 pending 队列,包含了 SYN 队列与 accept 队列,取决于实现)的最大大小是 acceptCount 个,所以文档中才会说“Once the limit has been reached, the operating system may still accept connections based on the "acceptCount" property”。

但是我感觉这个过程应该是相反的,从 accept 队列获取连接应该是很快的,accept 队列应该一般不会满,除非此时 Acceptor 线程阻塞了,才会造成 accept 队列的积压。

maxThreads

好了,现在要到重点了,maxThreads 是什么的 maxThreads 呢?先看文档:

    Maximum amount of worker threads.

哦,Workers 的最大线程池大小。结合代码确认下:

    /**
     * Maximum amount of worker threads.
     */
    private int maxThreads = 200;

    /**
     * Priority of the worker threads.
     */
    protected int threadPriority = Thread.NORM_PRIORITY;

    public void createExecutor() {
        internalExecutor = true;
        TaskQueue taskqueue = new TaskQueue();
        TaskThreadFactory tf = new TaskThreadFactory(getName() + "-exec-", daemon, getThreadPriority());
        executor = new ThreadPoolExecutor(getMinSpareThreads(), getMaxThreads(), 60, TimeUnit.SECONDS,taskqueue, tf);
        taskqueue.setParent( (ThreadPoolExecutor) executor);
    }
    

然后 Endpoint 里也的确最后是把 socket 多次封装丢到 executor(worker 线程池) 的:

    SocketProcessorBase<S> sc = processorCache.pop();
    if (sc == null) {
        sc = createSocketProcessor(socketWrapper, event);
    } else {
        sc.reset(socketWrapper, event);
    }
    Executor executor = getExecutor();
    if (dispatch && executor != null) {
        executor.execute(sc);
    } else {
        sc.run();
    }

emmm.... 漏说了一个 minSpareThreads , 从上面也可以看出来,这个就是 worker 线程池的 corePoolSize.

好了,RejectedExecutionException 抛出的地方找到了,executor 满了,Poller 再怎么往里头丢任务都会直接抛异常。因此,我们直接调大 maxThreads 之后,跟上游服务有关的虽然该阻塞还是阻塞,但是至少其他请求能进来了。

Note: 这个问题,还有一些可以调整的细节:

  1. 请求上游服务的超时时间,但是调整这个参数还是要有一些考虑,它性能本来就差,如果超时时间太短,搞不好大部分请求都拿不到响应就超时了。。 要解决还是得从根上来解决,优化上游服务的性能才是根本。
  2. 本服务的 connectionTimeout, 因为本服务一些接口依赖于上游服务,顾虑同上。不过这个还是可以调整的,它的默认值是 20s,从今天的网络环境来看是太大了。但是需要说明的是,在 spring-autoconfigure 1.3.1 里同样是没这个参数的。。。

最后,稍作调整之后至少服务。。死的慢一点了,目前来说除非过来的请求全是牵涉上游服务的,不会一下把 worker 线程池占满的。临危解决问题,有时候没法儿做到尽善尽美,根儿上的问题,只能 todo 了。

最后,想了解 tomcat 架构的可结合代码看这篇文章: http://gearever.iteye.com/blog/1844203

想了解线程数设置考量的可以看看这篇文章: https://www.cnblogs.com/bobsha/p/6178995.html

转载于:https://my.oschina.net/HY1024/blog/1634007

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值