Web服务器的线程策略

1. 简介

当前主流的Web服务器如Tomcat、Jetty均已采用nio作为默认的I/O模型。通常nio的线程模型中会有一组Acceptor线程用于接收客户端连接,一组Selector线程用于监听和处理I/O事件。当Selector检测到I/O事件后,是用同一个线程执行业务逻辑,还是将事件提交到一个业务线程组执行呢?不同的应用服务器,不同的场景下有着不同的策略。

本文将介绍几种主流的应用服务器的业务线程策略。

2. 原理分析

2.1 Tomcat

Web服务器的线程策略_第1张图片

对于Tomcat来说情况较为简单:

  • LimitLatch作为连接信号量,负责控制最大连接数,达到阈值后,连接请求将被拒绝
  • Acceptor是一组线程,负责accept客户端请求,当客户端请求到达时,accept方法构建一个Channel对象,并将Channel对象交给Poller处理
  • Poller中维护了一个Selector线程,当检测到OP_READ或者OP_WRITE事件时,生成一个SocketProcessor对象,提交给Executor执行

从下文代码中可以看出,对于OP_READ和OP_WRITE事件,dispatch都是true,SocketProcessor对象都会被提交到线程池Executor执行。
AbstractEndpoint.java

    public boolean processSocket(SocketWrapperBase<S> socketWrapper,
            SocketEvent event, boolean dispatch) {
        try {
            if (socketWrapper == null) {
                return false;
            }
            SocketProcessorBase<S> sc = null;
            if (processorCache != null) {
                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();
            }
        } catch (RejectedExecutionException ree) {
            getLog().warn(sm.getString("endpoint.executor.fail", socketWrapper) , ree);
            return false;
        } catch (Throwable t) {
            ExceptionUtils.handleThrowable(t);
            // This means we got an OOM or similar creating a thread, or that
            // the pool and its queue are full
            getLog().error(sm.getString("endpoint.process.fail"), t);
            return false;
        }
        return true;
    }

AbstractEndpoint.java

                            if (sk.isReadable()) {
                                if (socketWrapper.readOperation != null) {
                                    if (!socketWrapper.readOperation.process()) {
                                        closeSocket = true;
                                    }
                                } else if (!processSocket(socketWrapper, SocketEvent.OPEN_READ, true)) {
                                    closeSocket = true;
                                }
                            }
                            if (!closeSocket && sk.isWritable()) {
                                if (socketWrapper.writeOperation != null) {
                                    if (!socketWrapper.writeOperation.process()) {
                                        closeSocket = true;
                                    }
                                } else if (!processSocket(socketWrapper, SocketEvent.OPEN_WRITE, true)) {
                                    closeSocket = true;
                                }
                            }

2.2 Jetty

Jetty对于业务线程的处理则要复杂一些,它可以采用多种策略:

  • ProduceConsume,任务生产者自己生产和执行任务,当任务耗时较长时,会严重影响Poller的执行效率
  • ProduceExecuteConsume,如果是非阻塞任务,则当前生产者执行;否则,开启提交Executor执行任务。该策略不能重复利用CPU缓存,且线程上下文切换代价较高
  • ExecuteProduceConsume,由生产者执行任务;但是如果任务类型是非阻塞任务,且生产者不处于pending状态,则提交Executor执行
  • EatWhatYouKill,EatWhatYouKill是Jetty对于ExecuteProduceConsume的优化,依赖于QTP在9.4.x新增的ReservedThreadExecutor能力。当任务为非阻塞时,使用ProduceConsume模式;当任务为阻塞时,且生产者处于pending状态,使用EXECUTE_PRODUCE_CONSUME模式;生产者非pending状态,且ReservedThreadExecutor有可用线程时,也是用EXECUTE_PRODUCE_CONSUME模式;其他情况下使用PRODUCE_EXECUTE_CONSUME模式。

2.2.1 ReservedThreadExecutor

ReservedThreadExecutor是Jetty 9.4.x中对于QTP的增强,通过该Executor,Jetty可以在QTP中保留多个线程。

Jetty启动时

QueuedThreadPool.java

    @Override
    protected void doStart() throws Exception
    {
        // 默认情况下,_reservedThreads为-1
        if (_reservedThreads == 0)
        {
            _tryExecutor = NO_TRY;
        }
        else
        {
            ReservedThreadExecutor reserved = new ReservedThreadExecutor(this, _reservedThreads);
            reserved.setIdleTimeout(_idleTimeout, TimeUnit.MILLISECONDS);
            _tryExecutor = reserved;
        }
        addBean(_tryExecutor);

        super.doStart();
        // The threads count set to MIN_VALUE is used to signal to Runners that the pool is stopped.
        _counts.set(0, 0); // threads, idle
        ensureThreads();
    }

ReservedThreadExecutor.java

    public ReservedThreadExecutor(Executor executor, int capacity)
    {
        _executor = executor;
        _capacity = reservedThreads(executor, capacity);
       // 此处使用了无锁队列,以CPU时间为代价,提高并发性能
        _stack = new ConcurrentLinkedDeque<>();

        LOG.debug("{}", this);
    }

    // QTP实现了ThreadPool.SizedThreadPool接口,因此保留线程数在cpu核数和线程池size/10中取小
    private static int reservedThreads(Executor executor, int capacity)
    {
        if (capacity >= 0)
            return capacity;
        int cpus = ProcessorUtils.availableProcessors();
        if (executor instanceof ThreadPool.SizedThreadPool)
        {
            int threads = ((ThreadPool.SizedThreadPool)executor).getMaxThreads();
            return Math.max(1, Math.min(cpus, threads / 10));
        }
        return cpus;
    }

   // tryExecute方法是,TryExecutor接口定义的方法,调用方可以尝试执行一个runnable
   // 如果执行失败,可能是没有空闲保留线程,则返回false,任务不会提交给线程执行,也不会加入等待队列
    @Override
    public boolean tryExecute(Runnable task)
    {
        if (LOG.isDebugEnabled())
            LOG.debug("{} tryExecute {}", this, task);

        if (task == null)
            return false;

        ReservedThread thread = _stack.pollFirst();
        if (thread == null)
        {
            if (task != STOP)
                startReservedThread();
            return false;
        }

        int size = _size.decrementAndGet();
        thread.offer(task);

        if (size == 0 && task != STOP)
            startReservedThread();

        return true;
    }

3. 总结

综上所述,Jetty在业务线程模型上的设计更为精巧,充分利用了CPU缓存,减少了线程上下文切换。

4. 引用

Tomcat 9.0.26
Jetty 9.4.20

你可能感兴趣的:(JAVA并发)