Tomcat线程模型

注:不能免俗,本文大量借鉴综合其他文章及图片,结尾有备注,在此先行感谢,仅作为自己学习总结用。 那为何还要搞一篇这样的文章呢,虽然是大篇幅的借鉴,但是也还是有自己的独立思考,涉及的源码也是看过的,自己理解了就是好的,当然也希望能帮助那些需要的同学,毕竟把那几篇大佬的文章都拉在一起了,也可以自己去参照。

开篇就是直接复制的  : )

我们先来简单回顾下目前一般的NIO服务器端的大致实现,借鉴infoq上的一篇文章Netty系列之Netty线程模型中的一张图


thread pool

一个或多个Acceptor线程,每个线程都有自己的Selector,Acceptor只负责accept新的连接,一旦连接建立之后就将连接注册到其他Worker线程中

多个Worker线程,有时候也叫IO线程,就是专门负责IO读写的。一种实现方式就是像Netty一样,每个Worker线程都有自己的Selector,可以负责多个连接的IO读写事件,每个连接归属于某个线程。另一种方式实现方式就是有专门的线程负责IO事件监听,这些线程有自己的Selector,一旦监听到有IO读写事件,并不是像第一种实现方式那样(自己去执行IO操作),而是将IO操作封装成一个Runnable交给Worker线程池来执行,这种情况每个连接可能会被多个线程同时操作,相比第一种并发性提高了,但是也可能引来多线程问题,在处理上要更加谨慎些。tomcat的NIO模型就是第二种。

Tomcat处理请求过程

先来借鉴看下tomcat高并发场景下的BUG排查中的一张图 。主要有几个组件:

Acceptor线程:全局唯一,负责接受请求,并将请求放入Poller线程的事件队列。Accetpr线程在分发事件的时候,采用的Round Robin的方式来分发的

Poller线程:官方的建议是每个处理器配一个,但不要超过两个,由于现在几乎都是多核处理器,所以一般来说都是两个。每个Poller线程各自维护一个事件队列(无上限),它的职责是从事件队列里面拿出socket,往自己的selector上注册,然后等待selector选择读写事件,并交给SocketProcessor线程去实际处理请求。

SocketProcessor线程:它是实际的工作线程,用于处理请求。

一个典型的请求处理过程

Acceptor线程接受请求,从socketCache里面拿出socket对象(没有的话会创建,缓存的目的是避免对象创建的开销),

Acceptor线程标记好Poller对象,组装成PollerEvent,放入该Poller对象的PollerEvent队列

Poller线程从事件队列里面拿出PollerEvent,将其中的socket注册到自身的selector上,

Poller线程等到有读写事件发生时,分发给SocketProcessor线程去实际处理请求

SocketProcessor线程处理完请求,socket对象被回收,放入socketCache

一个典型的请求过程

以上是总体介绍,我们再来详细分析:

Accept Queue

对于client端的一个请求进来,流程是这样的:tcp的三次握手建立连接,建立连接的过程中,OS维护了半连接队列(syn队列)以及完全连接队列(accept队列),在第三次握手之后,server收到了client的ack,则进入establish的状态,然后该连接由syn队列移动到accept队列。ServerSocketChannel accept就是从这个队列中不断取出已经建立连接的的请求。所以当ServerSocketChannel accept取出不及时就有可能造成该队列积压,一旦满了连接就被拒绝了。

(上面图并没有介绍这个queue,也不是直接从queue中拿socket,是由于tomcat作了缓存。从socketCache里面拿出socket对象。没有的话才创建,缓存的目的是避免对象创建的开销)

tomat的acceptCount参数指的就是这个队列的大小。

acceptCount:The maximum queue length for incoming connection requests when all possible request processing threads are in use. Any requests received when the queue is full will be refused. The default value is 100.


accept queue

Acceptor

tomcat的acceptor线程则负责从accept队列中取出该connection,接受该connection,并转交出去,然后自己接着去accept队列取connection(当当前socket连接超过maxConnections的时候,acceptor线程自己会阻塞等待,等连接降下去之后,才去处理accept队列的下一个连接)。

accept核心有3步:

1.countUpOrAwaitConnection,进行连接数的自增,是在accept新的连接之前判断,目的就是控制连接数:当前socket连接超过maxConnections的时候,acceptor线程自己会阻塞等待,等连接降下去之后,才去处理accept队列的下一个连接。

当连接数我们设置maxConnections=-1的时候就表示不用限制最大连接数。默认是限制10000,如果不限制则一旦出现大的冲击,则tomcat很有可能直接挂掉,导致服务停止。

2.socket = serverSock.accept(); accept

3.setSocketOptions(socket))    //will add channel to the poller 

3.1设置一些参数 ..

3.2选中一个pooller注册:getPoller0().register(channel);

我们来看下Acceptor源码:

protected classAcceptorextendsAbstractEndpoint.Acceptor{

//if we have reached max connections, wait 

 countUpOrAwaitConnection();        1.

  SocketChannel socket = null;

 try {

    // Accept the next incoming connection from the server                                     

    socket = serverSock.accept();        2.

// setSocketOptions() will add channel to the poller 

 if (!setSocketOptions(socket)) {        3.

getPoller0().register(channel);            3.2

Pooller

1、注册其实就是将socket和Poller的关系绑定,再次从缓存中取出或者重新构建一个PollerEvent,然后将该event放到Poller的事件队列中等待被异步处理。

2、在Poller的run方法中不断处理上述事件队列中的事件,直接执行PollerEvent的run方法,将SocketChannel注册到自己的Selector上。

3、并将Selector监听到的IO读写事件封装成SocketProcessor,交给线程池执行

public void register(finalNioChannel socket){        1.

    socket.setPoller(this);

    NioSocketWrapper ka = new NioSocketWrapper(socket, NioEndpoint.this);

    socket.setSocketWrapper(ka);

    ka.setPoller(this);

    ka.setReadTimeout(getSocketProperties().getSoTimeout());

    ka.setWriteTimeout(getSocketProperties().getSoTimeout());

    ka.setKeepAliveLeft(NioEndpoint.this.getMaxKeepAliveRequests());

    ka.setSecure(isSSLEnabled());

    ka.setReadTimeout(getSoTimeout());

    ka.setWriteTimeout(getSoTimeout());

    PollerEvent r = eventCache.pop();

    ka.interestOps(SelectionKey.OP_READ);//this is what OP_REGISTER turns into.    

   if ( r==null) r = new PollerEvent(socket,ka,OP_REGISTER);

    else r.reset(socket,ka,OP_REGISTER);

    addEvent(r);

}

private final SynchronizedQueue<PollerEvent> events = new SynchronizedQueue<>();

private void addEvent(PollerEvent event){

    events.offer(event);

    if ( wakeupCounter.incrementAndGet() == 0 ) selector.wakeup();

}


SocketProcessor sc = processorCache.pop();

if ( sc == null ) sc = new SocketProcessor(attachment, status);

else sc.reset(attachment, status);

Executor executor = getExecutor();if (dispatch && executor != null) {

    executor.execute(sc);                     //3.

} else {

    sc.run();

}

SocketProcessor ThreadPool/Executor

这里才到了真正执行任务的线程池:SocketProcessor线程池,其配置就对应于我们Tomcat的配置了:

minSpareThreads,maxThreads

最主要的是这个线程池跟Jdk里的线程池还是有些区别的,据之前ThreadPoolExecutor的源码分析,核心线程数满了之后,会先将任务放到队列中,队列满了才会创建出新的非核心线程,如果队列是一个大容量的话,也就是不会到创建新的非核心线程那一步了。但是这里的TaskQueue修改了底层offer的实现。当线程数小于最大线程数的时候就直接返回false即入队列失败,则迫使ThreadPoolExecutor创建出新的非核心线程。

public void createExecutor(){

    TaskQueue taskqueue = new TaskQueue();

    executor = new ThreadPoolExecutor(getMinSpareThreads(), getMaxThreads(), 60, TimeUnit.SECONDS,taskqueue, tf);

}

coreThread:

private int minSpareThreads = 10;

publicintgetMinSpareThreads(){

    return Math.min(minSpareThreads,getMaxThreads());

}

maxThread: private int maxThreads = 200;

public boolean offer(Runnable o) {

  //we can't do any checks    

    if (parent==null) return super.offer(o);

    //we are maxed out on threads, simply queue the object    if (parent.getPoolSize() ==     parent.getMaximumPoolSize()) return super.offer(o);

    //we have idle threads, just add it to the queue    

    if (parent.getSubmittedCount()<(parent.getPoolSize())) 

    return super.offer(o);

    //if we have less threads than maximum force creation of a new thread    

    if (parent.getPoolSize()<parent.getMaximumPoolSize()) 

    return false;

    //if we reached here, we need to add it to the queue    

    return super.offer(o);

}

了解这些之后,来发表下自己的拙见:回过头来看看这些参数:acceptCount, maxConnections, inSpareThreads, maxThreads,并试图理一下:

建立连接,放入accept queue,当queue已满直接丢弃!什么时候accept queue会堆积?那就是acceptor没有及时来取连接。那什么时候acceptor不能及时来取连接?那就是acceptor线程被阻塞了,一般acceptor线程不断轮训来拿线程是完全不会造成堆积的。那acceptor线程为何会阻塞呢?前面代码看了,也就是当前连接数已达到maxConnections。那什么情况下,连接会堆积呢?那就是说明工作线程池(SocketProcessor)处理不过来了(否则其处理完socket是会被回收的),pooller队列中还堆积了大量的连接,tomat连接数=pooller队列中的连接 + SocketProcessor线程池中正在处理的连接。

至于inSpareThreads, maxThreads就比较好理解了,就是线程池中线程数。

所以,又反过来梳理一遍正常流程,SocketProcessor 线程池及时处理任务,不断从pooller中取event,则pooller queue就不会堆积,那么Acceptor就总能及时将连接放入到Pooller queue。。周而复始。

感觉自己总结的有点Low啊,睡觉去。。


参考:

https://tomcat.apache.org/tomcat-8.5-doc/config/http.html

https://my.oschina.net/pingpangkuangmo/blog/668925

https://yq.aliyun.com/articles/2889?spm=5176.team22.teamshow1.30.XRi499

https://segmentfault.com/a/1190000008064162

https://www.cnblogs.com/zhanjindong/p/concurrent-and-tomcat-threads-updated.html

connection timeout & socket timeout

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,033评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,725评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,473评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,846评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,848评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,691评论 1 282
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,053评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,700评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,856评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,676评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,787评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,430评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,034评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,990评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,218评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,174评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,526评论 2 343

推荐阅读更多精彩内容