前言

上周在工作中遇到一个问题,挺有意思,这里记录一下。上周在工作中遇到一个问题,挺有意思,这里记录一下。标题起的很唬人,这个问题差点引发血案,花哥还是很严谨的一个人,后面备注了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()被挂起了,到了这里才算是找到了问题原因的突破口,下面继续往上一步步跟踪就可以找到问题的所在了,下面会一步步认真分析。

问题原因

这里问题的原因其实是通过上面问题排查反推出来的:

  1. 前端调用服务端接口时,因为是post请求,所以header中传递的有Content-Length属性,调用feign请求时,不论get还是post请求,公司底层包中有个Feign拦截器会将前端请求Header属性赋值给feign请求中的Header,导致我们发送的GET请求Header中也含有Content-Length属性。

ps: 这一点很坑,依赖的底层包加了一个Feign拦截器,我们是通过打印feign请求日志在控制台才看到Content-Length属性的,最后跟踪到这个FeignInterceptor中的

  1. 服务B刚好依赖了另一个jar包,该包中包含一个Filter拦截器,它会读取发送的请求body数据,然后做一些日志打印。而且这个jar包依赖也是他们刚加的,他们使用该包中的其他一些工具类
public class ChannelFilter implements Filter {
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
if (servletRequest instanceof HttpServletRequest) {
requestWrapper = new RequestWrapper((HttpServletRequest)servletRequest);
log.info("Http RequestURL : {}, Method : {}, RequestParam : {}, RequestBody : {}", new Object[]{((HttpServletRequest)servletRequest).getRequestURL(), ((HttpServletRequest)servletRequest).getMethod(), JSON.toJSON(servletRequest.getParameterMap()), ((RequestWrapper)requestWrapper).getBody()});
} filterChain.doFilter((ServletRequest)requestWrapper, servletResponse);
} public void destroy() {
}
} public class RequestWrapper extends HttpServletRequestWrapper {
private static final Logger log = LoggerFactory.getLogger(RequestWrapper.class);
private final String body; public RequestWrapper(HttpServletRequest request) {
super(request);
StringBuilder stringBuilder = new StringBuilder();
BufferedReader bufferedReader = null;
ServletInputStream inputStream = null; try {
inputStream = request.getInputStream();
if (inputStream != null) {
bufferedReader = new BufferedReader(new InputStreamReader(inputStream));
char[] charBuffer = new char[4096];
boolean var6 = true; int bytesRead;
while((bytesRead = bufferedReader.read(charBuffer)) != -1) {
stringBuilder.append(charBuffer, 0, bytesRead);
}
}
} catch (IOException var19) {
log.error(var19.getMessage(), var19);
}
}
}

在执行request body读取的代码时使用到:

while((bytesRead = bufferedReader.read(charBuffer)) != -1) {
stringBuilder.append(charBuffer, 0, bytesRead);
}

bufferedReader.read()最终会调用到Tomcatorg.apache.tomcat.util.net.NioBlockingSelector.read()的方法读取request中的body属性:

int keycount = 1;
while(!timedout) {
if (keycount > 0) { //only read if we were registered for a read
read = socket.read(buf);
if (read != 0) {
break;
}
}
try {
if ( att.getReadLatch()==null || att.getReadLatch().getCount()==0) att.startReadLatch(1);
poller.add(att,SelectionKey.OP_READ, reference);
if (readTimeout < 0) {
att.awaitReadLatch(Long.MAX_VALUE, TimeUnit.MILLISECONDS);
} else {
att.awaitReadLatch(readTimeout, TimeUnit.MILLISECONDS);
}
} catch (InterruptedException ignore) {
// Ignore
}
}

这里因为GET请求的body为空,所以socket.read() 返回为0,进而走到att.awaitReadLatch(readTimeout, TimeUnit.MILLISECONDS);

protected void awaitLatch(CountDownLatch latch, long timeout, TimeUnit unit) throws InterruptedException {
if ( latch == null ) throw new IllegalStateException("Latch cannot be null");
latch.await(timeout,unit);
}

这里就会调用到LockSuport.parkNanos(time) 接口 直到超时,此时的你们会不会仍然有疑惑,为什么Header中传递了Content-Length就会走这个逻辑链路呢?别急,继续往下看,后面还有更精彩的分析......

解决方案

  1. 服务B取消有问题jar包的依赖
  2. 修改问题jar包中Filter的配置,判断只有Post请求才去读取body属性
  3. 接口调用方添加配置如果是GET请求时过滤掉Content-Length属性(主要原因)
  4. 修改底层依赖包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确实难住了我,请求的不规范才是这次问题的真正原因。而排查出来这个问题也花费了很多时间,不过这些都是挺值得的,一个人的成长离不开各种问题的洗礼,希望大家阅读完也会有所收获。

欢迎关注:

【原创】经验分享:一个Content-Length引发的血案(almost....)的更多相关文章

  1. 【原创经验分享】JQuery(Ajax)调用WCF服务

    最近在学习这个WCF,由于刚开始学 不久,发现网上的一些WCF教程都比较简单,感觉功能跟WebService没什么特别大的区别,但是看网上的介绍,就说WCF比WebService牛逼多少多少,反正我刚 ...

  2. 【原创经验分享】WCF之消息队列

    最近都在鼓捣这个WCF,因为看到说WCF比WebService功能要强大许多,另外也看了一些公司的招聘信息,貌似一些中.高级的程序员招聘,都有提及到WCF这一块,所以,自己也关心关心一下,虽然目前工作 ...

  3. 一个由正则表达式引发的血案 vs2017使用rdlc实现批量打印 vs2017使用rdlc [asp.net core 源码分析] 01 - Session SignalR sql for xml path用法 MemCahe C# 操作Excel图形——绘制、读取、隐藏、删除图形 IOC,DIP,DI,IoC容器

    1. 血案由来 近期我在为Lazada卖家中心做一个自助注册的项目,其中的shop name校验规则较为复杂,要求:1. 英文字母大小写2. 数字3. 越南文4. 一些特殊字符,如“&”,“- ...

  4. 转:一个Sqrt函数引发的血案

    转自:http://www.cnblogs.com/pkuoliver/archive/2010/10/06/1844725.html 源码下载地址:http://diducoder.com/sotr ...

  5. 一个Sqrt函数引发的血案(转)

    作者: 码农1946  来源: 博客园  发布时间: 2013-10-09 11:37  阅读: 4556 次  推荐: 41   原文链接   [收藏]   好吧,我承认我标题党了,不过既然你来了, ...

  6. 【转载】一个Sqrt函数引发的血案

    转自:http://www.cnblogs.com/pkuoliver/archive/2010/10/06/sotry-about-sqrt.html 源码下载地址:http://diducoder ...

  7. 一个Sqrt函数引发的血案

    源码下载地址:http://diducoder.com/sotry-about-sqrt.html 好吧,我承认我标题党了,不过既然你来了,就认真看下去吧,保证你有收获. 我们平时经常会有一些数据运算 ...

  8. 一个数字键盘引发的血案——移动端H5输入框、光标、数字键盘全假套件实现

    https://juejin.im/post/5a44c5eef265da432d2868f6 为啥要写假键盘? 还是输入框.光标全假的假键盘? 手机自带的不用非得写个假的,吃饱没事干吧? 装逼?炫技 ...

  9. 【原创】经验分享:一个小小emoji尽然牵扯出来这么多东西?

    前言 之前也分享过很多工作中踩坑的经验: 一个线上问题的思考:Eureka注册中心集群如何实现客户端请求负载及故障转移? [原创]经验分享:一个Content-Length引发的血案(almost.. ...

  10. Expression Blend4经验分享:制作一个简单的图片按钮样式

    这次分享如何做一个简单的图片按钮经验 在我的个人Silverlight网页上,有个Iphone手机的效果,其中用到大量的图片按钮 http://raimon.6.gwidc.com/Iphone/de ...

随机推荐

  1. Prometheus监控神器-Alertmanager篇(1)

    本章节主要涵盖了Alertmanager的工作机制与配置文件的比较详细的知识内容,由浅入深的给大家讲解. 警报一直是整个监控系统中的重要组成部分,Prometheus监控系统中,采集与警报是分离的.警 ...

  2. 2、Java 基础语法标识符、修饰符、变量、 数组、枚举、关键字

    Java 基础语法 一个 Java 程序可以认为是一系列对象的集合,而这些对象通过调用彼此的方法来协同工作.下面简要介绍下类.对象.方法和实例变量的概念. 对象:对象是类的一个实例,有状态和行为.例如 ...

  3. 最适合做C/C++开发的IDE

    一.Windows Windows没什么好说的了,铁定vs(下载链接:https://visualstudio.microsoft.com/zh-hans/), 因为如果不用vs,做Qt时会有奇妙的错 ...

  4. SSH安全端口

    端口安全指的是尽量避免服务器的远程连接端口被不法分子知道,为此而改变默认服务端口号的操作 如何改变SSH服务端口 修改 /etc/ssh/sshd_config 配置 配置修改后执行命令 servic ...

  5. 【工具】OSS阿里云存储服务--超级简单--个人还是觉得Fastdfs好玩

    介绍 阿里云对象存储服务(Object Storage Service,简称 OSS)为您提供基于网络的数据存取服务. 使用 OSS,您可以通过网络随时存储和调用包括文本.图片.音频和视频等在内的各种 ...

  6. 关于dubbo扩展点的一点分析

    扩展点能力 能load class,这个class除了顶层接口class(在ExtensionLoader中对应type字段),还能load各实现类的class. 能创建instance. 能指定这个 ...

  7. 细讲前端设置cookie, 储存用户登录信息

    细讲前端设置cookie 引言 正文 一.设置cookie 二.查看cookie 三.删除cookie 四.封装cookie操作 结束语 引言 我们都知道如果想做一个用户登录并使浏览器保存其登录信息, ...

  8. 【接口自动化】Python+Requests接口自动化测试框架搭建【一】

    公司项目启用新框架,前后端分离,所以接口测试成为测试工作中不可缺失的一个环节,现在将从0开始搭建接口自动化测试框架的路程,一步步记录下来. 开发语言我们采用Python+第三方库Requests,测试 ...

  9. python 用 prettytable 输出漂亮的表格

    原文链接:https://linuxops.org/blog/python/prettytable.html #!/usr/bin/python #**coding:utf-** import sys ...

  10. The Successor Representation: Its Computational Logic and Neural Substrates

    郑重声明:原文参见标题,如有侵权,请联系作者,将会撤销发布! Received May 14, 2018; revised June 28, 2018; accepted July 5, 2018.T ...