有这个文章的主要原因是在某一个投产前的压力&稳定性测试中,收到前方反馈测试过程中有504报错,且比例接近万分之一,质量部门认为504问题不可接受,要求解决问题并且给出完整的链路分析。
收集了一下前方信息:
- 测试压力高于生产实际压力;
- 测试集群未调优;
- 整体链路是:网关->Spring boot应用->DB操作
一般来说,压力测试下504报错是比较正常的行为,我们一般会更关注集群预期压力下504的比例,确保集群架构的设计性能符合生产要求;或者关注单个节点的性能上限和横向扩展的性能上升比例,对产品本身的性能情况摸底。
但是沟通下来,前方的测试场景是:要求集群在高于生产的压力情况下没有504报错,且不能提供更多节点。
无言以对,但还是要认真沟通,最后的沟通结果就促成了今天这个笔记,前方要求研发侧给出完整的504问题分析和建议。
ok,前面说啥都不重要,现在的目标是搞清楚504出现在哪一环。
问题定位前分析
504对应的message就是Gateway timeout,也就是超时问题,可能的原因是:
- 预期的时间内upstream没能完成服务的处理,导致超时;
- upstream未响应gateway的请求,导致超时;
- 网络异常、服务异常导致的请求丢失或者处理失败;
如果是一般业务场景下的504,我们倾向于认为upstream的服务出现异常,以至于不能及时响应请求,看下应用对应的日志,一般都能找到Root cause解决。
但如果是压测场景下想要找到问题具体哪一环,事情就变得复杂了起来,因为链路上的每一环,都有可能导致异常。
所以针对这个问题,我的思路是分步走,先找到请求丢失在链路的哪个位置,然后分析请求在对应位置丢失的原因,最后尝试针对原因调优或者直接解决问题(解决估计很困难,调优还是有希望的)。
链路分析
应用整体是云上部署的,使用k8s。请求从压测客户端到gateway再到spring boot应用,链路大致如下:
jmeter -> vip服务 -> gateway对应网卡 -> gateway服务 -> gateway对应网卡 -> spring boot应用对应网卡 -> spring boot 应用的tomcat -> 对应controller -> db 操作
返回链路不写了,也是这么回事。
考虑到jmeter拿到了504返回,gateway的日志打印了对应请求,那么gateway服务之前的链路应该是没有问题的,就不用查了。
spring boot应用没有打印对应的日志,所以请求肯定没有到对应的controller,controller往后,也就不用查了。
范围被圈定在(开区间):
gateway服务 -> gateway对应网卡 -> spring boot应用对应网卡 -> spring boot 应用的tomcat -> 对应controller
然后继续缩小范围,网卡上的请求可以抓包,tomcat的日志并不好处理(accessLog在这个场景并不能解决问题)。
那就先抓包。
如果是本地的话可以直接开wireShark,本地开会方便很多,https的问题也很好处理,但是由于测试只能在服务器上,所以只能使用tcpdump抓包,然后拉回本地wireshark看。
网上有很多配置wireshark解密https流量的,但那都要求在请求落文件时就导入密钥,服务器上没有这个条件,最简单的方式就是直接把ssl关掉,504情况不会因为关掉ssl就消失,然后再抓包。
给点命令参考:
1 |
|
src表示来自某个地址的请求,dst表示目标是某个地址的请求。
第一行命令挂在springboot对应的网卡上,第二行命令挂在gateway对应的网卡上,就可以抓到gateway网springboot应用的报文。
考虑到504也可可能是tomcat内超时后返回的,抓包的时候也挂了反向的流量,命令参考:
1 |
|
抓到的包直接拖到wireshark打开,根据gateway日志中504请求对应的请求uuid,直接在wireshark里面过滤请求头。
最后得到结果是,gateway对应网卡正确的发送了请求,spring boot应用对应网卡正确的接收了请求,但是没有请求返回。
那么链路排查到这里就基本结束了,问题就出在Tomcat上,严格的说是网卡到Tomcat之间。
排查Tomcat的一些手段
Tomcat如果是一个独立的应用,那么可以开启完整的catalina.out日志,这个日志也许能提供给我们一些有用的信息,但是Spring boot内嵌的Tomcat是没有这个日志的。
前面提到access log,如果网上搜索开启spring boot内置的tomcat日志,那么大部分的文章都会指导你开启access log,现在说下为什么access.log可能是没用的。
看下access log的常用开启配置
1 |
|
server.tomcat.accesslog.pattern=%t “%r” %s %b “%{Request-uuid}i” 这一行的意思是:
记录请求的时间、请求行(方法资源协议)、响应状态码、响应字节数、请求头中 Request-uuid的值。
既然能够记录响应状态码,那在这个场景大概率是无效了,毕竟从上面的情况来看,504的请求并没有能够进入处理,那也就不会再处理完成之后打印access log。
当然排查问题不嫌多,我们还是打开这个日志,如果没有打印,起码可以帮我们确认请求没有被处理。
然后分析一下Tomcat请求处理的过程。
connector组件从指定端口接收请求 -> connector组件解析请求并且封装成request对象 -> mapping到对应的servlet -> 请求到线程池等待分配worker -> 在filter中执行 -> 真正到达servlet开始处理 -> 生成相应数据 -> 再过一次filter -> connector接收到响应,包装成response -> 返回请求结果
从这个链路看,关键点是: 线程池分配worker ,那么关联的需要关注的内容是:等待队列。
监控线程可以使用Jconsole,但是如果使用Jconsole直接监控Spring boot进程,那么拿到的结果肯定不是我们期望的。
Tomcat有manger工具可以提供监控数据,但在springboot项目中更推荐actuator模块或者写代码。
这里选写代码:
1 |
|
打印出来的信息大概会是:
1 |
|
然后这里的问题是,我们不能控制这个打印触发的时间,只能选择定时打印,然后根据趋势判断是否有问题。
把这些修改都集成到应用中,重新测试然后对比日志。
access log如预测一样没有打印任何有用的信息,Tomcat性能监控日志打印出的信息显示确实负载很高。
Tomcat性能调优
分析到了这个阶段,其实已经很难继续往下挖掘了,链路基本上也摸清楚了。
仍然无法定位的问题;
- 如果队列满,那么一般结果是connection refused,对应502,这个跟504预期对不上
- 请求到底丢失在connector连接上还是connector转发上
如果继续排查,最具备可行性的办法变成了自定义connector,这排查成本可就太大了些,再说也没给我这么多时间。
技术解决不了的业务解决,于是跟前方沟通一波,一番努力。前方要求:那你调优下吧,把504的比例降到十万分之一。
调优就直观多了,关键参数:
1 |
|
以上为默认值。
accept-count调大一点,如果cpu核数够多,thread也调大一点,connection一般够用,且有长连接,问题不大。
因为上面排查过程中打印了参数,队列中的等待一直比较多,线程数基本跑满,看了下服务器cpu分的确实也还可以,就把线程多给了一倍,accept-count给到500,再压504数据达标。
到这里,写个报告给前方,结束。
一点吐槽
个人觉得这个504排查过程中我对tomcat细节的了解提升大于业务稳定性的提升,干了个不太开心的活,那就记一下吧。