tomcat源码分析05:请求处理流程(一)

899 阅读10分钟

注:本文源码分析基于 tomcat 9.0.43,源码的gitee仓库仓库地址:gitee.com/funcy/tomca….

上一篇文章中,我们分析了Connector的启动流程,本文将来分析tomcat对连接请求的处理。

1. 处理服务端连接:Acceptor#run

通过上一篇文章中,我们了解到,tomcat会启动一个单独的Acceptor线程来处理服务端的连接请求,Acceptor#run如下:

public class Acceptor<U> implements Runnable {
    /** 类型是 NioEndpoint */
    private final AbstractEndpoint<?,U> endpoint;
    ...
    public Acceptor(AbstractEndpoint<?,U> endpoint) {
        this.endpoint = endpoint;
    }

     @Override
    public void run() {
        int errorDelay = 0;
        try {
            while (!stopCalled) {
                ...
                try {
                    ...
                    U socket = null;
                    try {
                        // 接收服务端连接
                        socket = endpoint.serverSocketAccept();
                    } catch (Exception ioe) {
                        endpoint.countDownConnection();
                        if (endpoint.isRunning()) {
                            errorDelay = handleExceptionWithDelay(errorDelay);
                            throw ioe;
                        } else {
                            break;
                        }
                    }
                    errorDelay = 0;
                    if (!stopCalled && !endpoint.isPaused()) {
                        // 手动设置socket属性
                        if (!endpoint.setSocketOptions(socket)) {
                            endpoint.closeSocket(socket);
                        }
                    } else {
                        endpoint.destroySocket(socket);
                    }
                } catch (Throwable t) {
                    ...
                }
            }
        } finally {
            stopLatch.countDown();
        }
        state = AcceptorState.ENDED;
    }
    ...
}

这个方法有点长,不过大多逻辑是在处理一些判断,关键的操作就两个:

  1. endpoint.serverSocketAccept():接收服务端连接
  2. endpoint.setSocketOptions(socket):手动设置socket属性

1.1 接收服务端连接

我们来看看它是怎么获取到一个连接的,进入NioEndpoint#serverSocketAccept方法:

protected SocketChannel serverSocketAccept() throws Exception {
    return serverSock.accept();
}

这里调用的是ServerSocketChannel#accept方法来获取服务端连接,这个方法是jdk提供的,需要注意的是,这个方法是阻塞的,也就是说,tomcat会一直在这等着,直到有新的服务端连接进来才会被唤醒,因此Acceptor是阻塞的。

1.2 设置socket属性

我们再来看看手动设置socket属性的操作,也就是NioEndpoint#setSocketOptions方法:

protected boolean setSocketOptions(SocketChannel socket) {
    // 将 socketChannel 包装为 NioSocketWrapper
    NioSocketWrapper socketWrapper = null;
    try {
        // 包装socket为NioSocketWrapper的过程,具体过程我们并不关心
        // 热爱 nio 网络编程的小伙伴可对照源码自行分析
        ...

        // 手动注册事件
        poller.register(socketWrapper);
        return true;
    } catch (Throwable t) {
        ...
    }
    return false;
}

这个方法所做的工作就是将SocketChannel包装为NioSocketWrapper,然后将其注册到poller上,我们继续进入其注册方法NioEndpoint.Poller#register,一探究竟。

1.3 处理socketWrapper注册:NioEndpoint.Poller#register

public void register(final NioSocketWrapper socketWrapper) {
    // 表示 NioSocketWrapper 是读取操作
    socketWrapper.interestOps(SelectionKey.OP_READ);

    // 将 NioSocketWrapper 包装为 PollerEvent
    PollerEvent event = null;
    if (eventCache != null) {
        event = eventCache.pop();
    }
    // 表示事件需要注册
    if (event == null) {
        event = new PollerEvent(socketWrapper, OP_REGISTER);
    } else {
        event.reset(socketWrapper, OP_REGISTER);
    }
    // 添加事件
    addEvent(event);
}

这个方法会将传入的NioSocketWrapper包装为PollerEvent,然后进行添加事件操作,我们继续进入NioEndpoint.Poller#addEvent方法:

// 存放 PollerEvent 的结构
private final SynchronizedQueue<PollerEvent> events =
    new SynchronizedQueue<>();

private void addEvent(PollerEvent event) {
    events.offer(event);
    if (wakeupCounter.incrementAndGet() == 0) {
        // 唤醒 selector
        selector.wakeup();
    }
}

最终,只是把PollerEvent添加到了一个SynchronizedQueue结构中,然后就唤醒了selector

至此,Acceptor#run所做的工作就结束了。

2. NioEndpoint.Poller#run

Acceptor#run方法的最后,PollerEvent被添加到了一个SynchronizedQueue结构中,并且还唤醒了selector,这个selector是个啥呢?这就得说起Poller线程了,它的NioEndpoint.Poller#run方法如下:

public class Poller implements Runnable {

    private Selector selector;

    public Poller() throws IOException {
        // 得到一个 selector 对象
        this.selector = Selector.open();
    }

    @Override
    public void run() {
        while (true) {
            boolean hasEvents = false;
            try {
                if (!close) {
                    // 1. 调用 events() 方法
                    hasEvents = events();
                    // 2. selector 操作
                    if (wakeupCounter.getAndSet(-1) > 0) {
                        keyCount = selector.selectNow();
                    } else {
                        // 这里会阻塞,当 selector 在这里阻塞时,selector.wakeup() 会唤醒它
                        keyCount = selector.select(selectorTimeout);
                    }
                    wakeupCounter.set(0);
                }
                ...
                if (keyCount == 0) {
                    // 这里也会调用 events() 方法
                    hasEvents = (hasEvents | events());
                }
            } catch (Throwable x) {
                ...
            }

            Iterator<SelectionKey> iterator =
                keyCount > 0 ? selector.selectedKeys().iterator() : null;
            // selector 上存在key,进入处理流程
            while (iterator != null && iterator.hasNext()) {
                SelectionKey sk = iterator.next();
                iterator.remove();
                NioSocketWrapper socketWrapper = (NioSocketWrapper) sk.attachment();
                if (socketWrapper != null) {
                    // 3. 处理请求
                    processKey(sk, socketWrapper);
                }
            }
        }
        ...
    }

    ...
}

Poller会定期阻塞selector的执行,并且会调用events()方法,当Acceptor线程完成"PollerEvent被添加到了一个SynchronizedQueue结构中,并且还唤醒了selector"操作后,在NioEndpoint.Poller#run调用events()方法会发生什么情况呢?

2.1 NioEndpoint.Poller#events

NioEndpoint.Poller#events方法如下:

public boolean events() {
    boolean result = false;
    PollerEvent pe = null;
    // 遍历所有的事件,events.poll()操作的是SynchronizedQueue<PollerEvent>,元素在addEvent()添加的
    for (int i = 0, size = events.size(); i < size && (pe = events.poll()) != null; i++ ) {
        result = true;
        NioSocketWrapper socketWrapper = pe.getSocketWrapper();
        SocketChannel sc = socketWrapper.getSocket().getIOChannel();
        int interestOps = pe.getInterestOps();
        if (sc == null) {
            log.warn(sm.getString("endpoint.nio.nullSocketChannel"));
            socketWrapper.close();
        } else if (interestOps == OP_REGISTER) {
            try {
                // 手动注册读事件到 selector 上,getSelector():返回与Poller绑定的selector
                sc.register(getSelector(), SelectionKey.OP_READ, socketWrapper);
            } catch (Exception x) {
                log.error(sm.getString("endpoint.nio.registerFail"), x);
            }
        } else {
            ...
        }
        ...
    }
    return result;
}

events()方法中,会调用events.poll()方法获取events中的所有元素,events类型是SynchronizedQueue<PollerEvent>,在NioEndpoint.Poller#addEvent方法中添加的PollerEvent会在这里被取出来处理,处理时有一个关键操作:

sc.register(getSelector(), SelectionKey.OP_READ, socketWrapper);

这里由jdk nio提供的方法AbstractSelectableChannel#register,该方法的说明如下:

Registers this channel with the given selector, returning a selection key.

注册当前 channel 到指定的 select,并返回 selection

也就是说,selector上的事件是在AbstractSelectableChannel#register方法中手动注册的!

2.2 NioEndpoint.Poller#processKey

让我们再回到 NioEndpoint.Poller#run 方法:

   @Override
    public void run() {
        while (true) {
            boolean hasEvents = false;
            try {
                if (!close) {
                    hasEvents = events();
                    // selector 操作
                    if (wakeupCounter.getAndSet(-1) > 0) {
                        keyCount = selector.selectNow();
                    } else {
                        keyCount = selector.select(selectorTimeout);
                    }
                    wakeupCounter.set(0);
                }
                ...
               ...
            } catch (Throwable x) {
                ...
            }
            Iterator<SelectionKey> iterator =
                keyCount > 0 ? selector.selectedKeys().iterator() : null;
            // selector 上存在key,进入处理流程
            while (iterator != null && iterator.hasNext()) {
                SelectionKey sk = iterator.next();
                iterator.remove();
                NioSocketWrapper socketWrapper = (NioSocketWrapper) sk.attachment();
                if (socketWrapper != null) {
                    // 处理请求
                    processKey(sk, socketWrapper);
                }
            }
        }
        ...
    }

执行完events()方法后,selector上就有数据了,调用selector.select(...)selector.selectNow()方法就能获取到SelectionKey了。经观察发现,最终调用的是processKey(sk, socketWrapper)方法来处理请求:

protected void processKey(SelectionKey sk, NioSocketWrapper socketWrapper) {
    try {
        if (close) {
            cancelledKey(sk, socketWrapper);
        } else if (sk.isValid() && socketWrapper != null) {
            // 在这里处理请求
            if (sk.isReadable() || sk.isWritable()) {
                if (socketWrapper.getSendfileData() != null) {
                    // 处理文件数据
                    processSendfile(sk, socketWrapper, false);
                } else {
                    unreg(sk, socketWrapper, sk.readyOps());
                    boolean closeSocket = false;
                    // 读事件
                    if (sk.isReadable()) {
                        if (socketWrapper.readOperation != null) {
                            if (!socketWrapper.readOperation.process()) {
                                closeSocket = true;
                            }
                        // processSocket(...):处理是 socketChannel 的读事件
                        } else if (!processSocket(socketWrapper, SocketEvent.OPEN_READ, true)) {
                            closeSocket = true;
                        }
                    }
                    // 写事件
                    if (!closeSocket && sk.isWritable()) {
                        ...
                    }
                    if (closeSocket) {
                        cancelledKey(sk, socketWrapper);
                    }
                }
            }
        } else {
            // Invalid key
            cancelledKey(sk, socketWrapper);
        }
    } catch (...) {
        ...
    }
}

这个方法还是比较长的,这个方法会处理读、写事件,以及文件发送处理,我们主要关注读事件,方法为AbstractEndpoint#processSocket,代码如下:

public boolean processSocket(SocketWrapperBase<S> socketWrapper, 
        SocketEvent event, boolean dispatch) {
    try {
        if (socketWrapper == null) {
            return false;
        }
        // 获取一个 SocketProcessorBase
        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 (...) {
        ...
    }
    return true;
}

最终socketWrapperevent会包装成SocketProcessorBase,然后丢到线程池中去处理了。SocketProcessorBase相关代码如下:

/**
 * 内部持有两个成员变量:socketWrapper,event
 * 实现了Runnable接口
 */
public abstract class SocketProcessorBase<S> implements Runnable {

    protected SocketWrapperBase<S> socketWrapper;
    protected SocketEvent event;

    ...

    @Override
    public final void run() {
        synchronized (socketWrapper) {
            ...
            if (socketWrapper.isClosed()) {
                return;
            }
            // 实际干活的方法
            doRun();
        }
    }

    /**
     * 真正干活的方法留给子类实现
     */
    protected abstract void doRun();
}

这个类的内部持有两个成员变量:socketWrapperevent,它也实现了Runnable接口,线程的执行逻辑在run()方法中,不过从代码上来看,SocketProcessorBase#run方法基本没干啥事,真正干活的是doRun()方法,这个方法由子类实现。

当前SocketProcessorBase实际类型为SocketProcessor,因此真正的处理逻辑就是在SocketProcessor#doRun(),关于这个方法的分析,我们留到下篇文章进行。

2.3 tomcat 线程池

请求过来后,最终是丢到tomcat线程池中运行的,这里我们看看tomcat 线程池有啥特点。

1. 创建

线程池的创建是在NioEndpoint#startInternal方法:

public void startInternal() throws Exception {
    // 创建处理业务逻辑的线程池
    if (getExecutor() == null) {
        createExecutor();
    }
}

继续进入createExecutor()方法,来到AbstractEndpoint#createExecutor

public void createExecutor() {
    internalExecutor = true;
    // 任务队列,没有设置大小,默认大小为Integer.MAX_VALUE
    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);
}
  • getMinSpareThreads():核心线程数,默认为10
  • getMaxThreads():最大线程数,默认为200
  • 60, TimeUnit.SECONDS:非核心线程最大存活时间,即超时核心线程数后,若有线程空闲时间超过60秒,那么该非核心线程就会关闭
  • taskqueue:任务队列,处理不过来的任务放队列里,注意:这里的任务队列没有设置大小,默认大小为Integer.MAX_VALUE
  • th:线程工厂,用来生成线程

那么队列满了,它会采取什么拒绝策略呢?我们继续进入ThreadPoolExecutor的构造方法:

public class ThreadPoolExecutor extends java.util.concurrent.ThreadPoolExecutor {

    /**
     * 构造方法
     */
    public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, 
            TimeUnit unit, BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory) {
        // 调用父类的构造方法
        super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, threadFactory, 
                new RejectHandler());
        prestartAllCoreThreads();
    }

    ...

    /**
     * 这里拒绝策略
     */
    private static class RejectHandler implements RejectedExecutionHandler {
        @Override
        public void rejectedExecution(Runnable r,
                java.util.concurrent.ThreadPoolExecutor executor) {
            throw new RejectedExecutionException();
        }
    }
}

需要注意的是,这里的ThreadPoolExecutortomcat提供的org.apache.tomcat.util.threads.ThreadPoolExecutor,它继承了jdk的ThreadPoolExecutor,从代码来看,它的拒绝策略为抛异常。

2. 任务的执行

还是org.apache.tomcat.util.threads.ThreadPoolExecutor类,我们来看看它是怎么执行任务的:

public class ThreadPoolExecutor extends java.util.concurrent.ThreadPoolExecutor {

    /** 正在执行的任务数 */
    private final AtomicInteger submittedCount = new AtomicInteger(0);

    /**
     * 执行
     */
    @Override
    public void execute(Runnable command) {
        execute(command,0,TimeUnit.MILLISECONDS);
    }

    public void execute(Runnable command, long timeout, TimeUnit unit) {
        // 执行前,将submittedCount值加1
        submittedCount.incrementAndGet();
        try {
            super.execute(command);
        } catch (RejectedExecutionException rx) {
            ...
        }
    }

    /**
     * 执行完成
     */
    @Override
    protected void afterExecute(Runnable r, Throwable t) {
        // 执行结束后,将submittedCount值减1
        if (!(t instanceof StopPooledThreadException)) {
            submittedCount.decrementAndGet();
        }

        if (t == null) {
            stopCurrentThreadIfNeeded();
        }
    }

    ...

从源码上来看,ThreadPoolExecutor的执行还是调用java.util.concurrent.ThreadPoolExecutor#execute来做的,只不过ThreadPoolExecutor中维护了一个成员变量submittedCount,执行前将这个值加1,执行完成后再将这个值减1.从这里我们可以大致推断出,submittedCount表示的是当前正在执行的任务数。

那么submittedCount在哪里用到呢?这里我们需要来到TaskQueue#offer方法:

public class TaskQueue extends LinkedBlockingQueue<Runnable> {

    @Override
    public boolean offer(Runnable o) {
        // parent 不存在,直接添加到队列
        if (parent==null) return super.offer(o);

        // 线程池的核心线程数等于最大线程数,放到队列里,不能再创建线程了
        if (parent.getPoolSize() == parent.getMaximumPoolSize()) return super.offer(o);

        // 执行到这里,表明当前线程数小于最大线程数。
        // 表明是可以创建新线程的,那到底要不要创建呢?分两种情况:

        // 已提交的任务数小于等于核心线程数,说明当前线程还有空闲,不必创建线程,添加到队列中
        if (parent.getSubmittedCount()<=(parent.getPoolSize())) return super.offer(o);

        // 线程池的核心线程数小于最大线程数,表明需要创建新线程处理,不添加到队列中
        if (parent.getPoolSize()<parent.getMaximumPoolSize()) return false;

        return super.offer(o);
    }

    ...
}

在设置线程池时,我们设置了核心线程数与最大线程数,当核心线程数来不急处理时,就再创建线程,直到达到最大线程数,此时再有任务进来,就放进队列里。

3. 关于 tomcat nio 模型的几个问题

3.1 tomcat 的 nio 模型

tomcat 中,线程模型如下:

  • 处理连接的组件为Acceptor,一个线程处理,用来接收服务端请求,然后丢给 Poller 处理;
  • 处理读事件的线程为Poller,一个线程处理,处理Acceptor传递过来的请求,然后丢给线程池处理;

整个处理流程如下图所示:

3.2 Poller线程的作用?

看完nio的线程模型后,不知道大家有没有一个疑问:

Acceptor线程获取到一个连接后,要经过这么些步骤的处理:

  1. Acceptor线程将连接丢到队列里
  2. Poller线程扫描队列,得到连接,将其注册到selector
  3. Poller线程调用selector#select(...)/selector#selectNow(...)方法拿到连接
  4. Poller线程将连接丢到线程池中

看完后,有没有发现Poller似乎做了一些额外的工作,比如,

  1. Poller 线程在队列中获取到连接后,直接丢到线程池中就好了,为何还要注册到selector中?
  2. Acceptor线程获取到连接后,直接丢到线程池中不更好,为何要经过Poller线程?

个人猜测,注册到selector上,主要是为了复用连接,在http 1.1的时候,http请求是可以复用的,当注册到selector后,下一次请求过来时,就不会再走Acceptor线程了,而是直接处理读写请求了。

3.3 selector空轮询问题

关于selector空轮询问题,不了解的小伙伴可以看看JDK Epoll空轮询bug,个人感觉解释得还不错,这里引用文章中的一张图:

对于空轮询问题,图上已经标记得很清楚了,简单来说,就是关注的select轮询事件返回数量为0时,NIO照样不断的从select本应该阻塞的Selector.select()/Selector.select(timeout)wake up出来,导致CPU 100%问题,

tomcatPoller线程中,一样有Selector.select(timeout)操作,那它会有空轮询问题吗?注意到tomcat9支持的最低版本是jdk8,而代码中又没任何关于空轮询的处理,这是说jdk8已经没有了空轮询问题吗?大家对此有何见解呢,欢迎留言讨论。

4. 总结

本文主要分析了tomcat处理服务端连接的过程,总结如下:

  1. Acceptor线程将连接丢到队列里
  2. Poller线程扫描队列,得到连接,将其注册到selector
  3. Poller线程调用selector#select(...)/selector#selectNow(...)方法拿到连接
  4. Poller线程将连接丢到线程池中

连接丢到线程池后,tomcat又是如何处理的呢?我们下一篇揭晓。


限于作者个人水平,文中难免有错误之处,欢迎指正!原创不易,商业转载请联系作者获得授权,非商业转载请注明出处。

本文首发于微信公众号 Java技术探秘,链接:mp.weixin.qq.com/s/2SRV_bppK…,如果您喜欢本文,欢迎关注该公众号,让我们一起在技术的世界里探秘吧!