共计 8765 个字符,预计需要花费 22 分钟才能阅读完成。
你好呀,我是歪歪。
之前不是公布了这篇文章嘛:《千万不要把 Request 传递到异步线程外面!有坑!》
说的是因为 Request 在 tomcat 外面是复用的,所以如果在一个 Request 的生命周期实现之后,在异步线程外面调用了相干的办法,会导致这个 Request 被净化,而后在下一个申请中察看到一些匪夷所思的场景。
然而文章的评论区外面呈现了个问题,还一下把我问住了:
因为我那篇文章关注的重点是把 Request 传递到异步线程这个骚操作,并没有特地的关注 Request 到底是怎么复用的。
我只是通过打印日志的形式去察看到了复用的这个景象:
把我的项目启动起来之后,别离拜访 testRequest 和 testRequest1,从控制台的输入来看,Request 对象的确是一个对象。
然而从后面的线程名称来看,这是线程池外面两个齐全不同的线程。
所以,尽管我还啥都没剖析呢,基于日志就至多能看出这个问题的答案:
复用的 request 是和线程绑定的吗?
不是,没有绑定关系。
如果不是和线程绑定,那么问题就随之而来了:
如何决定哪个线程每次复用哪个 request 呢?
这是个好问题,我也不晓得答案,所以我决定来盘一盘它。
然而在盘它之前,咱们先想个问题:假如 Request 和申请线程绑定在一起了,这是一个正当的设计吗?
必定不是的。
线程就应该是单纯的线程,不应该给它“绑定”一个 Request。这种绑定让线程不单纯了,线程和申请耦合在一起了。
好一点的设计应该是 Request 放在一个“池子”外面,来一个线程就从池子外面去取能够用的 Request。
这样能够实现线程和申请之间解耦的成果。
当然,这也只是我在进行摸索之前的一个假如而已,先放在这里,最初看看这个猜测是否正确。
看这篇文章不须要你对 Tomcat 有多少理解,会用它就行,很多货色都是能够基于源码推理进去的。
对了,说一下 Tomcat 源码版本:9.0.58。
第一个断点
要找到问题的答案必定得去翻源码,然而从哪里开始翻呢?
或者换个问题:第一个断点打在哪呢?
遇到这个问题我的第一反馈还是从日志外面看看能不能找到相干的线索,从而找到打第一个断点的地位。
然而我别离把日志调整到 DEBUG 级别和 TRACE 级别,均没有发现有价值的信息,所以日志这条路感觉走不通了,怎么办?
不慌,这个时候就要沉着剖析一下了。
悄悄的问本人一句:我能够把断点打在办法入口处吗?
当然能够了,这也是能想到的一个十分惯例的伎俩:
然而如果把断点打在这里,相当于从业务代码的第一行反向去推源码,把路绕的略微远了一点。
那么还能够把断点打在哪里呢?
我这里不是输入了 Request 这个对象的全类名吗:
http-nio-8080-exec-2:testRequest1 = org.apache.catalina.connector.RequestFacade@5db48dd3
RequestFacade,这个类能用,必然有一个 new 它的中央,而要 new 它,必然要调用它的构造方法。
那我是不是只有在其对应的构造方法上打个断点,当程序创立这个类的时候,不就是我要找的源头吗?
所以,我把第一个断点打在了 RequestFacade 的构造方法上。
从构造方法动手,这也是我的一个调试小技巧,送给你,不客气。
有的小伙伴就要问了:如果一个类有多个构造方法怎么办呢?
很简略,鼎力出奇观,每个构造方法都打上断点,肯定会有一个中央触发的。
调试源码
找到第一个断点的地位了,接下来就是把我的项目重启,发动调用了。
我间断发动了两次调用,从程序的体现上我就晓得这个断点打对了。
我先给你上个动图,你就晓得我为什么这么说了:
我的项目启动之后,第一次调用在断点的中央停下来了,接着第二次调用并没有在断点的中央停下来。
阐明第二次的确没有新建 RequestFacade 对象,而是复用了第一次调用时产生的 RequestFacade 对象。
验证了断点打的地位没故障之后,就能够开始缓缓的调试了。
首先,咱们关注一下这个 RequestFacade 对象创立的中央:
有两个 if 判断。
第一个是判断 facade 是否为 null,不为 null 就 new。
第二个是把 facade 赋值给 applicationRequest 对象,接着返回 applicationRequest 对象。
第二个 if 其实很有意思,你想啊,这里间接返回 facade 也能够呀,为什么要用 applicationRequest 来承接一下呢?
这是一个好问题。
这两个 if 的关键在于 facade 和 applicationRequest 是否为空。
第一次拜访的时候必定是空。那么后续什么时候又会变为空呢?
就是在一次申请完结,执行 recycle 办法的时候:
org.apache.catalina.connector.Request#recycle
从源码中能够看到 applicationRequest 是间接设置为 null 的。
然而这个 facade 设置为 null 有个前提,getDiscardFacades 办法返回为 true。
这是个什么玩意?
看一眼就晓得了:
意思是 RECYCLE_FACADES 这个参数管制着是否循环应用 facade 这个对象,如果设置为 true 会进步安全性,而这个参数默认是 false。
也就是说我这个中央如果把这个参数批改为 true,facade 对象就会在每次调用实现之后进行回收。
能够通过启动参数 JAVA_OPTS 来配置:
-Dorg.apache.catalina.connector.RECYCLE_FACADES=true
从后面的源码中能够晓得,在默认的状况下,applicationRequest 会在每次申请实现之后设置为 null,而 facade 会保留下来。
因而下一次申请过去的时候,facede 并不为空,间接复用 facade。把 facade 赋值给 applicationRequest。
所以咱们在日志外面察看到的景象是两次申请输入的 facade 对象是一样的。
接着,咱们持续看调用堆栈。
看创立 facade 的这个 getRequest 申请到底是谁在调用:
发现是一个 Request 对象在调用 getRequest 办法。
所以接下来要找的就是 Request 对象最开始是从哪个办法开始作为入参传递的。
顺着调用堆栈,能够找到上面这个中央:
org.apache.coyote.http11.Http11Processor#service
这就是 Request 对象最开始作为入参传递的中央。
那么这个 Request 对象是怎么产生的呢?
我也不晓得。
所以,要晓得这个问题的答案,第二个断点打的地位也就跃然纸上了:
重启我的项目,发动申请,发现 Debug 停在了 AbstractProcessor 类的构造方法,这就是 request 最开始产生的中央,同时咱们又播种了一个调用堆栈:
org.apache.coyote.AbstractProcessor#AbstractProcessor(org.apache.coyote.Adapter, org.apache.coyote.Request, org.apache.coyote.Response)
这个 Request 是怎么来的呢?
new 进去的:
为什么要执行这个 new 办法呢?
因为这个中央在 createProcessor:
而咱们要寻找的问题的答案,就藏在下面这个截图中。
精确的说,就藏在下面截图中,标记了五角星的中央:
processor = recycledProcessors.pop();
从代码的片段看,如果从 recycledProcessors 外面 pop 出的 processor 对象不为空,则不会调用 createProcessor 办法。
而从调试的角度看,不调用 createProcessor 办法,也就不会创立 RequestFacade 对象。
所以,recycledProcessors,这个玩意是华点、是真正的突破口。
这一大节,次要是分享一下我找到这个突破口的一个过程,两个要害的断点是基于下面思考设置的。
其实你回忆一下,这是一个十分顺其自然的事件,带着问题去调试源码是一件比较简单的事件。
不要怂,就是翻。
recycledProcessors
你看这个对象的名称,recycled + Processors,一看就晓得外面有故事,有对于对象复用的故事。
org.apache.coyote.AbstractProtocol.RecycledProcessors
这个类的办法也特地简略,就三个办法:push、pop、clear。
继承至 SynchronizedStack 对象,就是一个标规范准的栈构造,只不过是用 Synchronized 批改了对应的办法:
在 SynchronizedStack 类的正文上提到了这是一个对象池、这个对象池不须要缩容、目标是为了缩小垃圾对象,开释 GC 压力。
当初咱们找到了这个对象池,也找到了调用这个对象池 pop 的中央。
那么什么时候往这个对象池 push 呢?
我也不晓得。
所以第三个断点就来了,能够打在 push 办法上:
而后发动调用,发现是在申请解决实现,release 以后 processor 的时候,就把这个 processor 放到 recycledProcessors 外面去,等着下一次申请应用:
此时咱们曾经把握了这样的一个闭环:
当申请来了之后,先看 recycledProcessors 这个栈构造外面有没有可用的 processor,没有则调用 createProcessor 办法创立一个新的,接着在申请完结之后,将其放入到栈构造外面。
而在调用 createProcessor 办法的时候,会构建一个新的 Request 对象,最终这个 Request 对象会封装为 RequestFacade 对象。
所以我当初想要验证 Processor、Request 和 RequestFacade 三者之间有这样的一个对应关系。
怎么验证呢?
打印日志。
留神,接下来又是一个调试小技巧了。
我想要在选定 processor 之后,退出一行输入语句:
怎么加呢?
在本人的我的项目外面创立一个和源码一样的包门路,而后把对应的类间接粘贴过去:
因为是在本人的我的项目外面,你想怎么改都行:
比方我退出这个输入语句,打印出 processor 和外面的 request。
发动申请之后你会发现的确失效了,然而 reuqest 的输入是这样的:
为什么呢?
因为在源码外面,这个类的 toString 办法被重写了:
怎么办?
改源码啊,刚刚才教你了的:
批改之后发动调用,就能够在控制台看到对应的预期的输入了:
你看,processor 外面有个 request。当初我要找的是 request 和 RequestFacade 之间的关系。
很简略,在 getRequest 办法这里也输入一行:
发动调用之后,发现,完犊子了:
这两个 Request 基本就不是同一个玩意啊:
org.apache.coyote.Request@667cbb30
org.apache.catalina.connector.Request@9ffc697
不要慌,冷静下来细嗦一下,尽管这是两个不同的 Request,然而它们之间肯定有着千头万绪的分割。
先看一下 org.apache.catalina.connector.Request 是怎么来的,老规矩,构造方法上打断点:
基于这个调用堆栈,往前找一点点,就能看到一个值得注意的中央:
org.apache.catalina.connector.CoyoteAdapter#service
在下面截图的这个办法中,有一行这样的代码:
request.setCoyoteRequest(req);
其中 request 是 org.apache.catalina.connector.Request 对象。
而 req 是 org.apache.coyote.Request 对象。
也就是说,我这里的这个输入语句应该是这样的才对:
批改之后,再次发动调用,输入日志是这样的:
如果你还没看出点什么的话,我给你加工一下:
意思就是 Processor 和 RequestFacade 的确是一一对应的。
回到文章最开始的这个截图,为什么我发动两次申请,RequestFacade 对象是同一个呢?
因为两次申请用的是同一个 Processor 呀。
你看我再发动两次申请,都是 Http11Processor@26807016 在解决:
所以,外表上看是同一个 RequestFacade,本质上是用的同一个 Processor。
换句话说: 要是两个申请用的是不同的 Processor,就不会存在复用的状况。
怎么验证一下呢?
我想到了上面的这个验证形式:
我能够先申请 sleepTenSeconds,而后在 10s 内申请 testRequest。这样,我就能察看到两个不同的 Processor:
为了更加直观的看到这个景象。
我决定在操作 recycledProcessors 的 pop 办法之前和 push 办法之后,输入一下 recycledProcessors 外面的内容:
org.apache.coyote.AbstractProtocol.RecycledProcessors
然而你依照我这样写的时候会发现:RecycledProcessors 的父类,也就是 SynchronizedStack 类并没有提供 print 办法,怎么办呢?
很简略嘛,源码我都能够拿到,加一个办法,还不是手到擒来的事件?
接着,我还是依照先拜访 sleepTenSeconds 再拜访 testRequest 办法的程序发动申请,日志是这样的:
独自拿进去,testRequest 整个申请实现之后,对应的日志是这样的,
========pop 之前【开始】打印以后所有 Processor========
========pop 之前【完结】打印以后所有 Processor========
1.processor=org.apache.coyote.http11.Http11Processor@6720055f,request=org.apache.coyote.Request@69e7f7cb
2.coyoteRequest=org.apache.coyote.Request@69e7f7cb,facade=org.apache.catalina.connector.RequestFacade@6dd86e2f
3.http-nio-8080-exec-1:testRequest = org.apache.catalina.connector.RequestFacade@6dd86e2f
========push 之后【开始】打印以后所有 Processor========
org.apache.coyote.http11.Http11Processor@6720055f
========push 之后【完结】打印以后所有 Processor========
而 sleepTenSeconds 整个申请实现之后,对应的日志是这样的:
========pop 之前【开始】打印以后所有 Processor========
========pop 之前【完结】打印以后所有 Processor========
1.processor=org.apache.coyote.http11.Http11Processor@7ba33829,request=org.apache.coyote.Request@1334fe58
2.coyoteRequest=org.apache.coyote.Request@1334fe58,facade=org.apache.catalina.connector.RequestFacade@2a0231eb
3.http-nio-8080-exec-2:sleepTenSeconds = org.apache.catalina.connector.RequestFacade@2a0231eb
========push 之后【开始】打印以后所有 Processor========
org.apache.coyote.http11.Http11Processor@6720055f
org.apache.coyote.http11.Http11Processor@7ba33829
========push 之后【完结】打印以后所有 Processor========
也就是说,此时 recycledProcessors 外面有两个 Processor:
========push 之后【开始】打印以后所有 Processor========
org.apache.coyote.http11.Http11Processor@6720055f
org.apache.coyote.http11.Http11Processor@7ba33829
========push 之后【完结】打印以后所有 Processor========
那么问题就来了:你说我接下来再次发动一个申请,哪个 Processor 会来承接这个申请呢?
尽管我还没有发动申请,然而我晓得,肯定是 Http11Processor@7ba33829 来进行解决。
因为我晓得它将是下一个被 pop 进去的 Processor 对象。
不信,你就看这个动图:
在下面的动图中,我先是 testRequest 这个申请。
如果我先拜访 sleepTenSeconds,再拜访 testRequest 呢?
尽管我还没有发动申请,然而我晓得,肯定是这样的对应关系来解决这两次申请:
sleepTenSeconds->Http11Processor@7ba33829
testRequest->Http11Processor@6720055f
因为 sleepTenSeconds 申请来的时候,recycledProcessors 外面会 pop 出 Processor@7ba33829 这个对象,来解决这个申请。
所以在 10 秒内,也就是 sleepTenSeconds 申请未实现的时候,拜访 testRequest 申请,recycledProcessors 外面接着 pop 进去的 就是 Http11Processor@6720055f 这个对象。
不信的话,你再看这个动图:
所以,当初咱们是不是找到这个问题的答案了:
如何决定哪个线程每次复用那个 request 呢?
申请线程和 request 之间没有关联关系。每次申请应用哪个 request 取决于应用哪个 Processor。而每次申请应用哪个 Processor,取决于 recycledProcessors 类外面缓存了哪些 Processor。申请过去的时候,pop 进去哪个,就是哪个。
recycledProcessors 既然是一个缓存,它的大小,肯定水平上决定了我的项目的性能。
而它的默认值是 200:
为什么是 200 呢?
因为 tomcat 线程池的最大线程数默认就是 200:
这个能想明确吧?
尽管线程和 Processor 之间没有绑定关系,然而从逻辑上讲一个线程对应一个 Processor。因而,好一点的做法是让线程数和 Processor 的数量保持一致。
如果我把 processorCache 这个参数批改为 1:
server.tomcat.processor-cache=1
你说高并发的时候会产生什么事件呢?
很多申请 push 的时候会 push 不进去,从而走到 handler.unregister(processor) 的逻辑外面去:
而这个 unregister 办法,对应的还有一个 register 办法,我一起给你看看:
它们持有的是同一笔 synchronized 锁,阐明它们之间有竞争。
咱们晓得,一个申请完结之后会调用 RecycledProcessors 的 push 办法,而 push 的时候会调用 unregister 办法。
那么问题就来了:register 什么时候调用呢?
其实后面曾经呈现过了:
一个申请来了,创立完 processor 之后。
所以,当我把 processorCache 设置为 1,高并发的状况下,在不停的调用 register 和 unregister,锁竞争频繁,性能降落。
这个论断,就是我通过翻阅源码得进去的论断,而不是在其余的某个书上或者视频外面失去的一个现成的论断。
这就是翻阅源码的高兴和意义。
回手掏
写到这里的时候,我不禁的想起了我在《千万不要把 Request 传递到异步线程外面!有坑!》这篇文章中踩到的坑。
再看一下这个动图,次要关注两次调用的时候控制台对应的输入:
就是因为在 Request 的生命周期之外应用了它,导致复用的时候呈现了问题。
过后我给出的正确计划是应用 Request 的异步编程,也就是 startAsync 和 AsyncContext.complete 办法那一套。
然而这篇文章写完之后,我又想到了两个骚操作。
第一个办法,就藏在我后面说的 RECYCLE_FACADES 这个配置中。
从官网文档上的形容来看这个参数如果设置为 true 会进步安全性,然而它默认是 false。
它怎么进步安全性呢?
就是每次把 RequestFacade 也给回收了。
那我把它改成 true 试一试,看看啥成果:
-Dorg.apache.catalina.connector.RECYCLE_FACADES=true
启动我的项目,发动调用:
抛出了一个异样。
看到这个异样的时候,我一下就明确了官网文档外面说的“安全性”是什么意思了:你的用法谬误了,我给你抛个异样,给你揭示一下,这里须要进行批改,晋升安全性。
而第二个是这样的:
server.tomcat.processor-cache=0
你明确我意思吧?
我间接不让你复用了,每次都用新的,绕过复用这个“坑”:
先别管它好不好用,有没有性能问题,你就说在彻底了解了底层逻辑之后,这个操作骚不骚吧。