Tomcat的主要工作是监听用户通过浏览器发送的网络请求,然后将请求连接到你的应用程序进行信息交换。在这个过程中,Tomcat中的acceptor、poller、exec等线程都在做这项工作。然而网上很多朋友都认为poller就是Tomcat中读写socket请求数据的线程,但事实真的如此吗?过去,Tomcat的工作就像一个黑盒子一样被封装在底层架构中,我们看不到。这次我们使用kindlingcamera工具进行了一个实验,让大家可以看到每次请求到来后所有工作线程的执行情况,从而确认poller是否在进行socket读写?首先我们简单回顾一下Tomcat的工作流程:它有两个核心组件,connector和container,其中container包含了你的应用代码。比如这个是脚本杀,用户就是玩家,你的程序代码就是通关宝,那么连接器就是NPC。但实际上,真正的网络请求远比脚本杀复杂,所以Tomcat还设计了线程池来应对大量并发的情况。现在我们来澄清一下文章开头提出的问题:poller线程是Tomcat中执行socket读写的线程吗?下图是我用kindling的摄像头工具抓拍记录的一个请求的执行情况。大家可以通过上面两张图快速了解这个工具的使用方法。说明下实验场景设置:一共发起了5个并发请求,每次发送间隔100ms,请求的实现代码为sleep(1000)ms,Tomcat配置的最大线程数为3.所以这也是一个很耗资源的场景:有5个并发线程,但是只有3个线程。后面我们也可以看到Tomcat是如何响应的。我这里用工具抓取的是第一个请求的执行记录。我们从上面两张图可以看出,acceptor首先对这个请求进行cpu-on,建立socket连接,然后将连接事件传递给poller进行管理。请注意,轮询器执行的时间戳是.319,接受器是318,所以轮询器必须在接受器之后执行。之后request事件交给Tomcat的线程池分配线程exec-1执行,也就是图中黑线部分。Futex的意思是线程卡住了,或者说是在等待,确实是因为我的实现代码写的是“sleep(1000)ms”。如上图所示,我们可以看到请求流的读写是由执行线程exec-1完成的,netRead是网络流的读写。同样,请求报文的写入也是由exec-1完成的。如下所示。(这里有两个netwrites因为消息可能太大,写了两次。第三个netread是exec-1执行下一个请求的事件,因为时间差太小,几乎重合。)至此,我们可以清楚的开始提出的问题:Tomcat中请求流的读写不是由poller线程完成的,而是由exec执行线程完成的。我们继续看,5个并发请求,Tomcat只有3个线程,它会怎么处理呢?如上图,等了大概100ms后,我的第二个请求进来了,Tomcat线程池分配给了exec-3执行。同样,第三个请求被分配给exec-2执行。但是第4次和第5次请求的情况就不同了。如下图,第4次和第5次请求从client发出,acceptor建立连接,poller管理请求事件,等待一段时间后才被exec线程执行。因为此时Tomcat没有多余的线程,所以它们需要等待exec线程空闲,才能执行。这反映了一种什么样的现象?对于client,我的前3次请求都正常,但是第4次和第5次请求有点慢,等了一会就开始执行了。但是对于服务器来说,它的响应时间是没有问题的,因为服务器的响应时间不是从连接的建立开始计算的,而是从exec线程实际处理请求开始计算的。这也是我们生产环境中普遍存在但排查难度非常大的资源匮乏问题,因为很难从表面上找到问题的症结所在。但是如果了解了Tomcat的一些原理,再加上kindling摄像头工具的辅助,定位是不是会容易很多?本次分享暂时就到这里,希望能给大家带来一些收获,可能有同学还有更多的问题需要解答,那么请继续关注我们,或者扫描二维码联系我们,谢谢~
