【原创】经验分享:一个 Content-Length 引发的血案 (almost....)
前言
上周在工作中遇到一个问题,挺有意思,这里记录一下。上周在工作中遇到一个问题,挺有意思,这里记录一下。标题起的很唬人,这个问题差点引发血案,花哥还是很严谨的一个人,后面备注了almost....
在测试环境中,前端调用我们服务一个接口时发现巨慢无比,响应时间超过了 30s,简直无法忍受!!
查看日志显示是我们服务在通过Feign
请求调用另一个服务的GET
接口时一直超时,然后重试了一直直到失败。 但是奇怪的是手动通过ip+端口
请求这个超时的GET
接口时却响应速度很快。
这就很奇怪了,之前一直调用好好的接口,怎么现在就一直超时呢?此时的我是满脑子问号。。。
现象
前端调用我们服务(这里叫做服务A
)的一个查询接口,这里前端用的是POST
请求,我们服务又会通过Feign
调用到另一个服务(这里叫做服务B
)的一个接口,这个接口对外提供GET
形式的调用。
从现象上来看就是调用我们服务特别慢,一个请求响应几十秒,具体流程如下:
问题排查
当时脑子中出现的疑惑就是太奇怪了,之前一只调用的接口不应该会出现这种情况,而且手动通过ip+端口
去调用的话响应速度很快的,于是找了服务B
对外开发的同学一起看,因为自己忽略了一些重要的日志信息,所以这里走了不少弯路,在同事的帮助下自己也将这个问题梳理清楚了。
问题的根本原因是我们在GET
请求的Header
中传递了Content-Length
参数,而且服务 B 近期添加了一个jar
包,jar
中有一个拦截器做了一些事情导致了这个问题。我这里从源码层面上梳理下整个问题的根本原因,以及以后如何避免此类问题!
对于这个问题,自己本地分别启动服务A
和服务B
,以DEBUG
模式启动,发现可以稳定重现,而且可以看到在调用服务B
卡住时候的堆栈信息:
服务A
发起的请求卡住的原因是在awaitLatch()
被挂起了,到了这里才算是找到了问题原因的突破口,下面继续往上一步步跟踪就可以找到问题的所在了,下面会一步步认真分析。
问题原因
这里问题的原因其实是通过上面问题排查反推出来的:
前端调用服务端接口时,因为是
post
请求,所以header
中传递的有Content-Length
属性,调用feign
请求时,不论get
还是post
请求,公司底层包中有个Feign
拦截器会将前端请求Header
属性赋值给feign
请求中的Header
,导致我们发送的GET
请求Header
中也含有Content-Length
属性。
ps: 这一点很坑,依赖的底层包加了一个 Feign 拦截器,我们是通过打印 feign 请求日志在控制台才看到 Content-Length 属性的,最后跟踪到这个 FeignInterceptor 中的
服务 B 刚好依赖了另一个
jar
包,该包中包含一个Filter
拦截器,它会读取发送的请求body
数据,然后做一些日志打印。而且这个jar
包依赖也是他们刚加的,他们使用该包中的其他一些工具类
在执行request body
读取的代码时使用到:
bufferedReader.read()
最终会调用到Tomcat
中org.apache.tomcat.util.net.NioBlockingSelector.read()
的方法读取request
中的body
属性:
这里因为GET
请求的body
为空,所以socket.read()
返回为 0,进而走到att.awaitReadLatch(readTimeout, TimeUnit.MILLISECONDS)
;
这里就会调用到LockSuport.parkNanos(time)
接口 直到超时,此时的你们会不会仍然有疑惑,为什么Header
中传递了Content-Length
就会走这个逻辑链路呢?别急,继续往下看,后面还有更精彩的分析......
解决方案
服务B
取消有问题jar
包的依赖修改问题
jar
包中Filter
的配置,判断只有Post
请求才去读取body
属性接口调用方添加配置如果是
GET
请求时过滤掉Content-Length
属性(主要原因)修改底层依赖包
FeignInterceptor
,判断请求的方式然后再针对Header
赋值(公司底层依赖的包我们不太好修改)
其实最应该修改的是方案 4,只是这个是全公司都会依赖的一个底层包,如果改动起来需要通知架构组等等,而且影响面会比较大。
最终我们先采用方案 3,在我们请求链路中去做一些判断,去除GET
请求中Content-Length
的传递。
解决原理
接下来就是真正原理的地方了,当服务端发出feign
请求后,一定会走Tomcat
中的org.apache.coyote.http11.Http11Processor.prepareRequest()
方法,代码如图:
如果contentLength >= 0
,那么会添加一个org.apache.coyote.http11.filters.IdentityInputFilter
类,在服务B
添加的jar
包中的RequestWrapper
中的bufferedReader.read()
会调用到 org.apache.coyote.http11.filters.IdentityInputFilter.doRead()
方法:
这个方法又会直接调用到 org.apache.tomcat.util.net.NioBlockingSelector.read()
中:
因为GET
请求的request body
为空,所以这里通过socket
去读取时返回为 0,直接运行下面的awaitReadLatch()
方法,这里会调用LockSuport.parkNanos(time)
接口 直到超时,这也是为什么我们每次feign
请求都会超时的原因。
但是如果服务请求方配置了传递的Content-Length
为空呢?这里会构造一个org.apache.coyote.http11.filters.VoidInputFilter
,这个拦截器的构造在上面Http11Processor.prepareRequest()
图示中已经标明:
显而易见,这里直接返回-1,不会再去调用NioBlockingSelector.read()
方法了,所以成功解决此问题,这也是问题的关键所在。
总结
这里没有过多的去介绍Content-Length
的概念,默许大家都知道这个,如果不太清楚的还可以参考:
https://blog.piaoruiqing.com/2019/09/08/do-you-know-content-length/
一个简单的Content-Length
确实难住了我,请求的不规范才是这次问题的真正原因。而排查出来这个问题也花费了很多时间,不过这些都是挺值得的,一个人的成长离不开各种问题的洗礼,希望大家阅读完也会有所收获。
欢迎关注:
版权声明: 本文为 InfoQ 作者【一枝花算不算浪漫】的原创文章。
原文链接:【http://xie.infoq.cn/article/99334bd6f36855156c4bab3d9】。文章转载请联系作者。
评论 (2 条评论)