记一次异步处理导致Jetty Request对象泄漏(二)

简介: 记一次异步处理导致Jetty Request对象泄漏(二)

3.根因与源码分析


我们通过在Request类中设置多个断点,找到了原因。整理过程如下图所示。

1.png


1)同步请求A快速完成返回。


当请求A进来,在一次Http请求结束后(controller方法返回客户端),会进行相应的recycle()操作,这里包括Requst对象执行recycle()方法,清理相关参数,包括_queryParameters

2.png


2)异步任务延迟响应,在recycle()后重新设置了_queryParameter属性。


在请求A执行过程中,使用「自定义线程池」异步执行了一个方法B(方法较慢)。方法B中,从RequestContextHolder中获取了HttpServletRequest,然后通过request.getParameter()获取请求头。


3.png


因为此时_queryParametersnull,因此extractQueryParameters()方法就解析了一个空的对象放进去。

4.png


3)新请求C进入,返回异常。


当新的请求C进入后端服务,拿到了同一个Request对象,由于此时_queryParameters不为null,因此跳过了extractQueryParameters(),导致应该解析的queryString无法被解析,controller抛出异常。


总结:一旦主线程执行完毕,完成recycle过程,而异步线程执行较慢,异步线程中的任何request.getParameter()行为会破坏Request对象的recycle,导致_queryParameters属性为空对象而不是null,从而导致新的请求失败。


3.2异步线程中,RequestContextHolder还能拿到Request对象?(根本原因)


我们知道RequestContextHolder是基于ThreadLocal实现的。因此,在异步线程中,是无法直接通过


RequestContextHolder.getRequestAttributes()获取主线程的HttpServletRequest

问题出在了「自定义线程池」
ThreadPoolExecutorWithMonitor
中。


里面自定义实现了一个内部类DecorateRunnableTask来处理任务。

5.png


内部类DecorateRunnableTask继承了内部类DecorateTask,保存了主线程的RequestAttributes对象。


6.png


然后在异步线程执行前,通过before()方法设置到了当前线程的RequestContextHolder中。


总结:给异步线程传递RequestAttributes对象,是造成Request对象泄漏的根本原因!


3.3两个请求,为什么会共享一个Request对象?


本来上面的分析基本已经找到了Bug的原因,但是我仔细想了下,又觉得有点奇怪。

两个请求,为什么会共享一个Request对象?


如果是使用了相关池化技术,那怎么能在两个请求找到同一个对象,然后稳定复现呢?因此,又继续去研究了下jetty的相关内容。


jetty 9.x整体架构图:

7.png



SelectorManager + ManagedSelector +QueuedThreadPool 组成了「Reactor线程模型」。对于一个http请求,SelectorManager分配给某一个ManagedSelector创建HttpConnection对象,然后在QueuedThreadPool中执行相应的IO操作。


HttpConnection对象持有HttpChannel对象,HttpChannel中持有了Request对象(就是HttpServletRequest)。

网关到后端服务之间使用的是Http请求,默认为长连接,因此,在短时间内的新的请求(长连接结束前),会复用同一个HttpConnection对象。


4.最佳实践


  1. 不要给异步线程传递RequestAttributes对象并进行保存。


  1. 如果需要相关请求参数,可以新建上下文对象存储参数后进行传递。或者使用TransmittableThreadLocal。


5.一些小TIPS


5.1jetty源码不匹配


在对jetty的Request类进行debug时,一开始这里遇到一个小坑,idea一直源码匹配不上。从github上把 jetty源码拉下来,按照引入的jetty版本进行本地mvn install,还是不一致。


根据pom的依赖分析,可以看到引入的jetty版本为9.4.12。

8.png


后来突然想起来,这个项目虽然是springboot项目,但是并不是打成jar包通过内置jetty容器启动的。而是打成了war包,本地通过jetty-maven-pluginjetty:run启动的。这里使用的jetty版本为9.4.9。

9.png


所以,我们需要按照jetty-maven-plugin的版本来选择jetty的源码。


5.2偶发问题难以复现


考虑到篇幅原因与阅读体验,本文在排查过程中,没有展开说明一个非常困难的地方————本地如何稳定复现「偶发问题」异常请求。


真实排查过程中,本地稳定复现耗费了大量时间。如果不是本地可以稳定复现,后面的debug也无从谈起。


后面主要根据代码的近期变更情况,发现了一个异步请求的引入,将异步改为同步后,发现就不会再出现这个问题了。


所以才从异步请求出发,多次尝试后,进行了稳定复现。


所以本次排查的一个重要收获,就是对于一些故障的排查,可以考虑从近期的「各种变更」中去寻找线索。

目录
相关文章
|
前端开发 API 容器
记一次异步处理导致Jetty Request对象泄漏(一)
记一次异步处理导致Jetty Request对象泄漏(一)
138 0
记一次异步处理导致Jetty Request对象泄漏(一)
|
缓存 分布式计算 API
Spark Netty与Jetty (源码阅读十一)
  spark呢,对Netty API又做了一层封装,那么Netty是什么呢~是个鬼。它基于NIO的服务端客户端框架,具体不再说了,下面开始。   创建了一个线程工厂,生成的线程都给定一个前缀名。      像一般的netty框架一样,创建Netty的EventLoopGroup:      在常用...
1068 0