Web服務器的線程策略

1. 簡介

當前主流的Web服務器如Tomcat、Jetty均已採用nio作爲默認的I/O模型。通常nio的線程模型中會有一組Acceptor線程用於接收客戶端連接,一組Selector線程用於監聽和處理I/O事件。當Selector檢測到I/O事件後,是用同一個線程執行業務邏輯,還是將事件提交到一個業務線程組執行呢?不同的應用服務器,不同的場景下有着不同的策略。

本文將介紹幾種主流的應用服務器的業務線程策略。

2. 原理分析

2.1 Tomcat

image.png

對於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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章