曹工说Tomcat:200个http-nio-8080-exec线程全都被第三方服务拖住了,这可如何是好(上:线程模型解析)

摘要:
你们都调试过了。您必须知道线程名称如下:http-nio-8080-exec-2@5076这条线是假猫的线。假设我们在这个线程中休眠一分钟,以模拟第三方服务被卡住并且在调用第三方服务器时不返回的情况。此时,客户端每秒接收100个请求。整个程序会发生什么?好的,必须需要这个线程。轮询器线程负责将套接字注册到选择器,还负责处理套接字的IO读/写事件。
前言

这两年,tomcat慢慢在新项目里不怎么接触了,因为都被spring boot之类的框架封装进了内部,成了内置server,不用像过去那样打个war包,再放到tomcat里部署了。

但是,内部的机制我们还是有必要了解的,尤其是线程模型和classloader,这篇我们会聚焦线程模型。

其实我本打算将一个问题,即大家知道,我们平时最终写的controller、service那些业务代码,最终是由什么线程来执行的呢?

大家都是debug过的人,肯定知道,线程名称大概如下:

http-nio-8080-exec-2@5076

这个线程是tomcat的线程,假设,我们在这个线程里,sleep个1分钟,模拟调用第三方服务时,第三方服务异常卡住不返回的情况,此时客户端每秒100个请求过来,此时整个程序会出现什么情况?

但是我发现,这个问题,一篇还是讲不太清楚,因此,本篇只讲一下线程模型。

主要线程模型简介

大家可以思考下,一个服务端程序,有哪些是肯定需要的?

我们肯定需要开启监听对吧,大家看看下面的bio程序:

曹工说Tomcat:200个http-nio-8080-exec线程全都被第三方服务拖住了,这可如何是好(上:线程模型解析)第1张

这个就是个线程,在while(true)死循环里,一直accept客户端连接。

ok,这个线程肯定是需要的。接下来,再看看还是否需要其他的线程。

如果一切从简,我们只用这1个线程也足够了,就像redis一样,redis都是内存操作,做啥都很快,还避免了线程切换的开销;

但是我们的java后端,一般都要操作数据库的,这个是比较慢,自然是希望把这部分工作能够交给单独的线程去做,在tomcat里,确实是这样的,交给了一个线程池,线程池里的线程,就是我们平时看到的,名称类似http-nio-8080-exec-2@5076这样的,一般默认配置,最大200个线程。

但如果这样的话,1个acceptor + 一个业务线程池,会导致一个问题,就是,该acceptor既要负责新连接的接入,还要负责已接入连接的socket的io读写。假设我们维护了10万个连接,这10万个连接都在不断地给我们的服务端发数据,我们服务端也在不停地给客户端返回数据,那这个工作还是很繁重的,可能会压垮这个唯一的acceptor线程。

因此,理想情况下,我们会在单独弄几个线程出来,负责已经接入的连接的io读写。

大体流程:

acceptor--->poller线程(负责已接入连接的io读写)-->业务线程池(http-nio-8080-exec-2@5076)

这个大概就是tomcat中的流程了。

在netty中,其实是类似的:

boss eventloop--->worker eventloop-->一般在解码完成后的最后一个handler,交给自定义业务线程池
tomcat如何接入新连接

大家可以看看下图,这里面有几个橙色的方块,这几个代表了线程,从左到右,分别就是acceptor、nio线程池、poller线程。

曹工说Tomcat:200个http-nio-8080-exec线程全都被第三方服务拖住了,这可如何是好(上:线程模型解析)第2张

  • 1处,acceptor线程内部维护了一个endpoint对象,这个对象呢,就代表了1个服务端端点;该对象有几个实现类,如下:

    曹工说Tomcat:200个http-nio-8080-exec线程全都被第三方服务拖住了,这可如何是好(上:线程模型解析)第3张

    我们spring boot程序里,默认是用的NioEndpoint。

  • 2处,将新连接交给NioEndpoint处理

    @Override
        protected boolean setSocketOptions(SocketChannel socket) {
            // Process the connection
            try {
                // Disable blocking, polling will be used
                socket.configureBlocking(false);
                Socket sock = socket.socket();
                socketProperties.setProperties(sock);
                // 进行一些socket的参数设置
                NioSocketWrapper socketWrapper = new NioSocketWrapper(channel, this);
                channel.setSocketWrapper(socketWrapper);
                socketWrapper.setReadTimeout(getConnectionTimeout());
                socketWrapper.setWriteTimeout(getConnectionTimeout());
                //3 交给poller处理
                poller.register(channel, socketWrapper);
                return true;
            }
     		...
            // Tell to close the socket
            return false;
        }
    
  • 3处,就是交给NioEndpoint内部的poller对象去进行处理。

            public void register(final NioChannel socket, final NioSocketWrapper socketWrapper) {
                socketWrapper.interestOps(SelectionKey.OP_READ);//this is what OP_REGISTER turns into.
                PollerEvent r = null;
                // 丢到poller的队列里,poller线程会轮旋该队列
                r = new PollerEvent(socket, OP_REGISTER);
                // 丢到队列里
                addEvent(r);
            }
    

    上面的addEvent值得一看。

    private final SynchronizedQueue<PollerEvent> events =
                    new SynchronizedQueue<>();
            
    private void addEvent(PollerEvent event) {
        // 丢到队列里
        events.offer(event);
        // 唤醒poller里的selector,及时将该socket注册到selector中
        if (wakeupCounter.incrementAndGet() == 0) {
            selector.wakeup();
        }
     }
    

    到这里,acceptor线程的逻辑就结束了,一个异步放队列,完美收工。接下来,就是poller线程的工作了。

    poller线程,要负责将该socket注册到selector里面去,然后还要负责该socket的io读写事件处理。

  • poller线程逻辑

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

    可以看到,poller内部维护了一个selector,和一个队列,队列里也说了,主要是要新注册到selector的新socket。

    既然丢到队列了,那我们看看什么时候去队列取的呢?

            @Override
            public void run() {
                // Loop until destroy() is called
                while (true) {
                    boolean hasEvents = false;
                    // 检查events
                    hasEvents = events();
               }
           }
    

    这里我们跟一下events()。

    public boolean events() {
        boolean result = false;
    
        PollerEvent pe = null;
        for (int i = 0, size = events.size(); i < size && (pe = events.poll()) != null; i++ ) {
            result = true;
            pe.run();
            ...
        }
    
        return result;
    }
    

    这里的

    pe = events.poll()
    

    就是去队列拉取事件,拉取到了之后,就会赋值给pe,然后下面就调用了pe.run方法。

    pe的类型是PollerEvent,我们看看其run方法会干啥?

            @Override
            public void run() {
                if (interestOps == OP_REGISTER) {
                    try { socket.getIOChannel().register(socket.getSocketWrapper().getPoller().getSelector(), SelectionKey.OP_READ, socket.getSocketWrapper());
                    } catch (Exception x) {
                        log.error(sm.getString("endpoint.nio.registerFail"), x);
                    }
                }
            }
    

    这个方法难理解吗,看着有点吓人,其实就是把这个新的连接,向selector注册,感兴趣的io事件为OP_READ。后续呢,这个连接的io读写,就全由本poller的selector包了。

tomcat如何处理客户端读事件

我们说了,poller是个线程,在其runnable实现里,除了要处理上面的新连接注册到selector这个事,还要负责io读写,这部分逻辑就是在:

        Iterator<SelectionKey> iterator=selector.selectedKeys().iterator();
        while (iterator != null && iterator.hasNext()) {
            SelectionKey sk = iterator.next();
            NioSocketWrapper socketWrapper = sk.attachment();
            processKey(sk, socketWrapper);
        }

最后一行的processKey,会调用如下逻辑,将工作甩锅给http-nio-8080-exec-2@5076这类打杂的线程。

public boolean processSocket(SocketWrapperBase<S> socketWrapper,SocketEvent event, boolean dispatch) {
		Executor executor = getExecutor();
		executor.execute(sc);
        return true;
}

给个图的话,大概就是如下的红线流程部分了:

曹工说Tomcat:200个http-nio-8080-exec线程全都被第三方服务拖住了,这可如何是好(上:线程模型解析)第4张

小结

好了,到了课后思考时间了,我们也说了,最终会交给http-nio-8080-exec-2@5076这类线程所在的线程池,那假设这些线程全都在sleep,会发生什么呢?

下一篇,我们继续。

免责声明:文章转载自《曹工说Tomcat:200个http-nio-8080-exec线程全都被第三方服务拖住了,这可如何是好(上:线程模型解析)》仅用于学习参考。如对内容有疑问,请及时联系本站处理。

上篇3D打印机开源、免费分层软件介绍HTML5播放器 MediaElement.js 使用方法下篇

宿迁高防,2C2G15M,22元/月;香港BGP,2C5G5M,25元/月 雨云优惠码:MjYwNzM=

相关文章

OpenMP 线程同步之临界区

多核/多线程编程中肯定会用到同步互斥操作。除了互斥变量以为,就是临界区。 临界区是指在用一时刻只允许一个线程执行的一段用{...},包围的代码段。 在OpenMP中临界区声明方法如下: #pragma omp critical [(name)] //[]表示名字可选 { //需要同一时刻只能有一个线程访问的代码 } 如下面的代码: 1 #include &...

RxJava入门

项目小版本上线,抽空简单学习了下久仰大名的RxJava 一、引入 个人觉得rxjava的特点: 强大灵活的事件流处理(多线程/多事件/复合对象) 强大灵活优雅简洁的异步 链式调用 可自动Lambda化   实现:RxJava 是通过一种扩展的观察者模式来实现的 类比 类比 实际 实际 职责 演讲者 Button (可)被订阅者 (同右)...

[ PyQt入门教程 ] PyQt5中多线程模块QThread使用方法

本文主要讲解使用多线程模块QThread解决PyQt界面程序唉执行耗时操作时,程序卡顿出现的无响应以及界面输出无法实时显示的问题。用户使用工具过程中出现这些问题时会误以为程序出错,从而把程序关闭。这样,导致工具的用户使用体验不好。下面我们通过模拟上述出现的问题并讲述使用多线程QThread模块解决此类问题的方法。 PyQt程序卡顿和无法实时显示问题现象 使...

IOS网络编程之Socket详解

Socket描述了一个IP、端口对。它简化了程序员的操作,知道对方的IP以及PORT就可以给对方发送消息,再由服务器端来处理发送的这些消息。所以,Socket一定包含了通信的双发,即客户端(Client)与服务端(server)。1)服务端利用Socket监听端口;2)客户端发起连接;3)服务端返回信息,建立连接,开始通信;4)客户端,服务端断开连接。 1...

web 服务的基础介绍

1>web 服务的访问流程          1.电脑浏览器网页上输入请求的地址          2.服务器接收到请求          3.服务器响应请求          4.将响应的数据返回给客户端 2>  apache 的三种工作模型(面试)               select ;work;event           2...

AQS与重入锁ReetrantLock原理

一、AQS原理 AQS(AbstractQueuedSynchronizer)队列同步器是用来构建锁、同步组件的基础框架。 AQS内部通过一个volatile int类型的成员变量state控制同步状态【0代表锁未被占用,1表示已占用】,通过内部类Node构成FIFO的同步队列实现等待获取锁的线程排队工作,通过内部类ConditionObject构建条件等...