年前接到个任务,说要解决线上一些手机客户端接口报错率很高的问题.拿到了监控邮件,粗略一看,各种50%+的错误率,简直触目惊心.这种疑难杂症解决起来还是挺好玩的,于是撸起袖子action.

最终的结果虽然报错问题得到了解决,但是感觉并不是最根本的解决方案.

下面把解决的过程和目前的问题放出来一起探讨下.

第一步,针对错误进行跟踪,初步定位问题

由于之前客户端同学在请求中添加了唯一标示request_id. 所以选择了一些报错的记录进行跟踪. 打开了jetty的request_log请求日志,经查发现出错请求会出现两种情况:

1,未在request_log中出现,既请求都未能从nginx发送至后端服务
2,在request_log中出现,并返回成功(状态码200,并且响应时间很快)

由此暂时排除后端服务问题,推测问题出现在nginx与服务之间的链接.

第二步,查看nginx日志,初步优化

经由第一步得出结果,进一步观察出错时候的nginx日志.

发现出错时nginx日志中会出现”no live upstreams while connecting to upstream”错误.并伴随大量”upstream prematurely closed connection while reading response header from upstream”错误.

经查阅资料得出nginx负载与健康检查机制的简陋可能造成某些请求无法发送到活动的后端服务上. 遂添加nginx负载机制配置以期解决问题.

参考资料:

http://www.tuicool.com/articles/AfeuUje
https://segmentfault.com/a/1190000002446630

在线上backends中添加了以下配置:

备注: 因涉及服务器机密,IP及端口均经过修改
upstream mobile {
server 192.168.0.10:6001 max_fails=10 fail_timeout=10s;
server 192.168.0.10:6002 max_fails=10 fail_timeout=10s;
server 192.168.0.10:6003 max_fails=10 fail_timeout=10s;
server 192.168.0.10:6004 max_fails=10 fail_timeout=10s;
keepalive 64;
}

经过一段时间观察后,发现"no live upstreams while connecting to upstream"错误大幅度减少,但502错误率依旧,依然存在大量"upstream prematurely closed connection while reading response header from upstream"

第三步,upstream prematurely closed connection while reading response header from upstream

添加第二步的配置后观察与思考,推测问题可能出现在以下两个方面:

1,后端jetty服务压力过大导致无法完成响应.
2,网络原因导致请求出问题.

那么一项一项的排查吧.

排除jetty服务压力

先排查jetty服务压力是否造成报错.遂添加了对jetty线程及请求队列的监控. 也引发了之前一篇关于jetty线程监控的文章.需要的朋友自行取用.

http://www.cnblogs.com/succour/p/6266283.html

观察后发现即使高峰时期jetty中的线程数依然没有过大压力,没有出现队列拥堵现象.所以将重心放于网络原因.

释放招式:tcpdump抓包.网络问题浮出水面.

进行tcp链接抓包并解析,分析出错原因.

使用tcpdump抓包并解析后发现:

出错请求都会在此tcp流中前一个请求未收到响应时就关闭链接
既一个tcp连接中的http请求与响应不能一一对应且请求数永远比响应数多1.
而追踪未出错请求时则发现tcp流中请求与响应都可一一对应.

由于我们线上在nginx中都配置了nginx upstream中的keepalive,既nginx与后端服务链接的复用,推测可能是前一个请求结束后或keepalive时间到后nginx关闭了链接,而新的请求还在发送中就被中断了.

第四步,去除keepalive配置,解决问题

在线上的upstream中去除了keepalive配置,配置变为了:

备注: 因涉及服务器机密,IP及端口均经过修改
upstream mobile {
server 192.168.0.10:6001 max_fails=10 fail_timeout=10s;
server 192.168.0.10:6002 max_fails=10 fail_timeout=10s;
server 192.168.0.10:6003 max_fails=10 fail_timeout=10s;
server 192.168.0.10:6004 max_fails=10 fail_timeout=10s;
}

修改生效当时那茫茫多的"upstream prematurely closed connection while reading response header from upstream"瞬间消失. 观察了一天之后,502错误率明显下降,现已下降到0.00x%的级别.

说明推测是正确的,nginx upstream的keeplive导致了此次问题的出现.

第五步,后续

虽然去除keepalive解决了问题,但是keepalive对于链接的复用确实是可以提高通信效率的.粗暴的删除也只能是暂时的解决方案.而且也并没有查阅到相关keepalive会引起此问题的文章.

所以问题的根源依旧没有水落石出.

继续推测可能是由于线上tcp链接的配置问题导致的.

于是将线上的tcp配置拷贝到测试环境,添加上keepalive对测试环境进行压测,奇异的一幕出现了...问题并没有被复现...

tcp配置参考资料:

http://www.cnblogs.com/zengkefu/p/5749009.html

一脸懵逼的我继续观察tcp抓到的包以及nginx中的错误日志...

终于是有所发现...

原来在nginx错误日志中以HTTP/1.1协议发送的请求,到了tcp抓包中竟然被悄悄改为了HTTP/1.0协议...并且Connection请求头为close! nginx中所有报错为"upstream prematurely closed connection while reading response header from upstream"的请求所抓到的包全部都是这种情况...

注意ip地址以及时间,确定与下图为同一请求.

继续观察发现在这个被改变了http协议的请求前,都会有一个HTTP/1.0的请求.

然后对这个TCP流抓包,发现了下面的情况:

如图,80为nginx服务器,72为后端jetty服务.

  在80向72以tcp发送第一个get请求后,72以tcp回发了一个响应.这个响应中FIN标记是为0的,也就是不关闭连接.
80在接收到72的响应后,继续以http发送了第二个get请求,也就是我们出错的请求.而且此请求被改为了HTTP/1.0!
然后80解析了72回发的第一个get请求的响应,而这个响应的FIN标记被http协议标记为了1,也就是需要关闭连接了.
然后80就没有等待第二个get请求的响应,发送了关闭连接的tcp报文.
此时第二个get请求也就没有办法发送响应了.因为tcp连接已经不存在了.

那么可以理解为HTTP/1.0协议发送的请求在请求结束后链接就被关闭,而在关闭前nginx依然复用了这个链接发送了请求...然后nginx关闭了连接,导致了后面这个请求报错!

还有第一个get请求的响应中tcp到http这个"解析"过程是怎么回事,还有待查询资料.问题就是在这个"解析"的时间内发送了另一个请求导致的...

至于第一个HTTP/1.0的请求是不是客户端发送过来的1.0还是被nginx修改的1.0,今天我去查看日志的时候,发现日志被删了...运维大哥今天又没在...只能等他回来再验证了...

未完待续...

有了结论:

据运维说不知道谁把nginx转换http1.1的配置删掉了...就是下面两行:

proxy_http_version 1.1;

proxy_set_header Connection "";

虽然结论显得有点中二...但是感觉排查问题的过程还是值得记录的.

记一次线上由nginx upstream keepalive与http协议"协作"引起的接口报错率飙高事件的更多相关文章

  1. 记一次线上bug排查-quartz线程调度相关

    记一次线上bug排查,与各位共同探讨. 概述:使用quartz做的定时任务,正式生产环境有个任务延迟了1小时之久才触发.在这一小时里各种排查找不出问题,直到延迟时间结束了,该任务才珊珊触发.原因主要就 ...

  2. 解Bug之路-记一次线上请求偶尔变慢的排查

    解Bug之路-记一次线上请求偶尔变慢的排查 前言 最近解决了个比较棘手的问题,由于排查过程挺有意思,于是就以此为素材写出了本篇文章. Bug现场 这是一个偶发的性能问题.在每天几百万比交易请求中,平均 ...

  3. 记一次线上SpringCloud-Feign请求服务超时异常排查

    由于近期线上单量暴涨,第三方反馈部分工单业务存在查询处理失败现象,经排查是当前系统通过FeignClient调用下游系统出现部分超时失败(异常代码贴在下方). Caused by: feign.Ret ...

  4. 我整理的一份来自于线上的Nginx配置(Nginx.conf),希望对学习Nginx的有帮助

    我整理了一份Nginx的配置文件说明,是真正经历过正式线上考验过.如果有优化的地方,也请朋友们指点一二,整理出一份比较全而实用的配置. 主要包含配置:负载均衡配置,页面重定向,转发,HTTPS和HTT ...

  5. 记一次线上Curator使用过程JVM栈溢出解决

       为了同学们看起来一目了,特按如下思路进行讲解. 1.出现的场景    2.分析及解决的过程    3.总结 最近公司要使用zookeeper做配置管理(后面简称ZK),然后自己就提前用虚拟机进行 ...

  6. 记一次线上coredump事故

    1.事故背景 上周三凌晨,我负责的某个模块在多台机器上连续发生coredump,幸好发生在业务低峰期,而且该模块提供的功能也不是核心流程功能,所以对线上业务影响比较小.发生coredump后,运维收到 ...

  7. 记一次线上事故的JVM内存学习

    今天线上的hadoop集群崩溃了,现象是namenode一直在GC,长时间无法正常服务.最后运维大神各种倒腾内存,GC稳定后,服务正常.虽说全程在打酱油,但是也跟着学习不少的东西. 第一个问题:为什么 ...

  8. 记一次线上gc调优的过程

           近期公司运营同学经常表示线上我们一个后台管理系统运行特别慢,而且经常出现504超时的情况.对于这种情况我们本能的认为可能是代码有性能问题,可能有死循环或者是数据库调用次数过多导致接口运行 ...

  9. 记一次线上MySQL数据库死锁问题

            最近线上项目报了一个MySQL死锁(DealLock)错误,虽说对业务上是没有什么影响的,由于自己对数据库锁这块了解不是很多,之前也没怎么的在线上碰到过.这次刚好遇到了,便在此记录一下 ...

随机推荐

  1. QCheckBox 使用本地图片的QSS样式

    setStyleSheet 为以下字符串,选中,未选中,有焦点,无焦点时显示不一样的图片 QString("QCheckBox::indicator:unchecked{image:url( ...

  2. 使用jQuery快速高效制作网页交互特效--JavaScript操作BOM对象

    JavaScript操作BOM 一.window对象: 二.window对象的属性和方法 1.windows对象的常用属性: 语法:window.属性名="属性值" 2.windo ...

  3. AutoFileName 使用src ,href 引入文件时,会自动补全路径的插件

    AutoFileName功能:快捷输入文件名 简介:自动完成文件名的输入,如图片选取 使用:输入”/”即可看到相对于本项目文件夹的其他文件 只要输入 src="" 输入双引号就立马 ...

  4. jquery动画函数里面可以跟一个回调函数,表示动画结束后执行的代码

    jquery动画函数里面可以跟一个回调函数,表示动画结束后执行的代码 使用js监听动画结束后进行的操作: $ele.fadeIn(300,function(){...}) $ele.fadeOut(3 ...

  5. CSP考前总结

    10.2 考试: 1.数位DP 或者找规律 2.SB题,扫一遍找最大最小即可 3.莫比乌斯反演 出题人相出个数论和数据结构的综合题,但是找不到NOIP级别的,没办法只能忍痛割爱出个莫比乌斯,话说回来, ...

  6. Centos7 安装php7.3 并扩展 MySQL、postgresql

    首先是安装需要的扩展文件 yum -y install freetype-devel yum -y install libpng-devel yum -y install libjpeg-devel ...

  7. P/NP问题

    目录 P NP NPC NPH 写在开头 1.多项式 如公式:y = axn-bxn-1+c.Ο(log2n).Ο(n). Ο(nlog2n).Ο(n2)和Ο(n3)称为多项式时间.Ο(2n)和Ο(n ...

  8. jquery实现图片切换

    <div> <img class="imgclick" src="img/pic1.png" /> </div> <s ...

  9. 简述*args and **kwargs

    为了能让一个函数接受任意数量的位置参数:* 为了接受任意数量的关键字参数:** *参数只能出现在函数定义中最后一个位置参数后面,而**参数只能出现在最后一个参数 解决的问题:构造一个可接受任意数量参数 ...

  10. 20191214数组之四:数字不相同的完全平方数(关于数位上数字判断与sprintf)

    sprintf用法参见之前的随笔;(以解决):