曹工说Tomcat:200个http-nio-8080-exec线程全都被第三方服务拖住了,这可如何是好(上:线程模型解析)
前言
这两年,tomcat慢慢在新项目里不怎么接触了,因为都被spring boot之类的框架封装进了内部,成了内置server,不用像过去那样打个war包,再放到tomcat里部署了。
但是,内部的机制我们还是有必要了解的,尤其是线程模型和classloader,这篇我们会聚焦线程模型。
其实我本打算将一个问题,即大家知道,我们平时最终写的controller、service那些业务代码,最终是由什么线程来执行的呢?
大家都是debug过的人,肯定知道,线程名称大概如下:
http-nio-8080-exec-2@5076
这个线程是tomcat的线程,假设,我们在这个线程里,sleep个1分钟,模拟调用第三方服务时,第三方服务异常卡住不返回的情况,此时客户端每秒100个请求过来,此时整个程序会出现什么情况?
但是我发现,这个问题,一篇还是讲不太清楚,因此,本篇只讲一下线程模型。
主要线程模型简介
大家可以思考下,一个服务端程序,有哪些是肯定需要的?
我们肯定需要开启监听对吧,大家看看下面的bio程序:
这个就是个线程,在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线程。
1处,acceptor线程内部维护了一个endpoint对象,这个对象呢,就代表了1个服务端端点;该对象有几个实现类,如下:
我们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;
}
给个图的话,大概就是如下的红线流程部分了:
小结
好了,到了课后思考时间了,我们也说了,最终会交给http-nio-8080-exec-2@5076这类线程所在的线程池,那假设这些线程全都在sleep,会发生什么呢?
下一篇,我们继续。
曹工说Tomcat:200个http-nio-8080-exec线程全都被第三方服务拖住了,这可如何是好(上:线程模型解析)的更多相关文章
- 曹工说Spring Boot源码(18)-- Spring AOP源码分析三部曲,终于快讲完了 (aop:config完整解析【下】)
写在前面的话 相关背景及资源: 曹工说Spring Boot源码(1)-- Bean Definition到底是什么,附spring思维导图分享 曹工说Spring Boot源码(2)-- Bean ...
- 曹工说Tomcat1:从XML解析说起
一.前言 第一次被人喊曹工,我相当诧异,那是有点久的事情了,楼主13年校招进华为,14年在东莞出差,给东莞移动的通信设备进行版本更新.他们那边的一个小伙子来接我的时候,这么叫我的,刚听到的时候,心里一 ...
- 曹工说Spring Boot源码(19)-- Spring 带给我们的工具利器,创建代理不用愁(ProxyFactory)
写在前面的话 相关背景及资源: 曹工说Spring Boot源码(1)-- Bean Definition到底是什么,附spring思维导图分享 曹工说Spring Boot源码(2)-- Bean ...
- 曹工说Spring Boot源码(21)-- 为了让大家理解Spring Aop利器ProxyFactory,我已经拼了
写在前面的话 相关背景及资源: 曹工说Spring Boot源码(1)-- Bean Definition到底是什么,附spring思维导图分享 曹工说Spring Boot源码(2)-- Bean ...
- 曹工说Spring Boot源码(25)-- Spring注解扫描的瑞士军刀,ASM + Java Instrumentation,顺便提提Jar包破解
写在前面的话 相关背景及资源: 曹工说Spring Boot源码(1)-- Bean Definition到底是什么,附spring思维导图分享 曹工说Spring Boot源码(2)-- Bean ...
- 【曹工杂谈】Maven源码调试工程搭建
Maven源码调试工程搭建 思路 我们前面的文章<[曹工杂谈]Maven和Tomcat能有啥联系呢,都穿打补丁的衣服吗>分析了Maven大体的执行阶段,主要包括三个阶段: 启动类阶段,负责 ...
- 曹工说Spring Boot源码系列开讲了(1)-- Bean Definition到底是什么,附spring思维导图分享
写在前面的话&&About me 网上写spring的文章多如牛毛,为什么还要写呢,因为,很简单,那是人家写的:网上都鼓励你不要造轮子,为什么你还要造呢,因为,那不是你造的. 我不是要 ...
- 曹工说Spring Boot源码(20)-- 码网灰灰,疏而不漏,如何记录Spring RedisTemplate每次操作日志
写在前面的话 相关背景及资源: 曹工说Spring Boot源码(1)-- Bean Definition到底是什么,附spring思维导图分享 曹工说Spring Boot源码(2)-- Bean ...
- 曹工说Spring Boot源码(22)-- 你说我Spring Aop依赖AspectJ,我依赖它什么了
写在前面的话 相关背景及资源: 曹工说Spring Boot源码(1)-- Bean Definition到底是什么,附spring思维导图分享 曹工说Spring Boot源码(2)-- Bean ...
随机推荐
- 新网站seo如何优化
http://www.wocaoseo.com/thread-189-1-1.html 做seo优化也有一段时间了,有做过自己的博客,自己的论坛,也有做过公司企业的网站,有同时一个人完成8个 ...
- vue路由守卫+cookie实现页面跳转时验证用户是否登录----(二)设置路由守卫
上一篇我们已经封装好了cookie方法,登录成功之后也可以吧用户信息存到cookie中,接下来需要在router/index.js中引入一下cookie.js文件 然后继续添加以下代码 /* * be ...
- vs2015创建项目没法选择.net2.0的问题
今天要修护一个net2.0开发的程序bug,只修改了一行代码,发布到测试系统,结果程序奔溃了,吓出一身汗. 因为是直接崩溃,所以第一个想到的就是.net版本错了.一查看,果实是,项目属性里目标框架是4 ...
- 从后端到前端之Vue(六)表单组件
表单组件 做项目的时候会遇到一个比较头疼的问题,一个大表单里面有好多控件,一个一个做设置太麻烦,更头疼的是,需求还总在变化,一会多选.一会单选.一会下拉的,变来变去的烦死宝宝了. 那么怎么解决这个问题 ...
- android开发中防止刚进入activity时edittext获取焦点,防止自动自动弹出软键盘
刚进入activity的时候,如果布局组件有edittext的话,往往edittext会获取焦点,自动弹出软键盘,影响整个界面的视觉效果.解决方法如下: 可以在edittext的父布局结构中(例如Li ...
- 【Gin-API系列】Gin中间件之异常处理(六)
本文我们介绍生产环境上如何通过捕捉异常recovery来完善程序设计和提高用户体验. Golang异常处理 golang 的异常处理比较简单,通常都是在程序遇到异常崩溃panic之后通过defer调用 ...
- kotlin 作用域函数 : let、run、with、apply、 also、takeIf、takeUnless
1.官方文档 英文: https://kotlinlang.org/docs/reference/scope-functions.html 中文: https://www.kotlincn.net/d ...
- HDU多校1003-Divide the Stones(构造)
Problem Description There are n stones numbered from 1 to n.The weight of the i-th stone is i kilogr ...
- Black & White(尺取)
链接:https://ac.nowcoder.com/acm/contest/893/F来源:牛客网 * 第一行一个整数 T ,表示接下来有 T 个样例.* 首先输入n,m,表示S串的长度n和操作次数 ...
- 同样是logback1.11,更换了log配置后,无论是否有线程持续不断写入log文件,log文件会按设定以日期序号轮换
上次发现了logback1.11的一个bug,即有线程持续写入log,则log文件不会按设定模式进行轮换. 但发现同样采用logback1.11的另外一个工程,它的日志文件就没有错误,于是参照其配置文 ...