2024-05-27-不是丈二和尚也没摸着头脑的504排查记录.md

有这个文章的主要原因是在某一个投产前的压力&稳定性测试中,收到前方反馈测试过程中有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
2
3
4
yum install -y tcpdump
tcpmdump -i any src host gateway-ip -w springboot.pcap
tcpmdump -i any dst host springboot-ip -w gateway.pcap

src表示来自某个地址的请求,dst表示目标是某个地址的请求。

第一行命令挂在springboot对应的网卡上,第二行命令挂在gateway对应的网卡上,就可以抓到gateway网springboot应用的报文。

考虑到504也可可能是tomcat内超时后返回的,抓包的时候也挂了反向的流量,命令参考:

1
tcpdump -i any src host gateway-ip or dst host gateway-ip -w springboot.pcap

抓到的包直接拖到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
2
3
4
5
6
7
8
9
10
11
server.tomcat.accesslog.buffered=true
server.tomcat.accesslog.enabled=true
server.tomcat.accesslog.file-date-format=.yyyy-MM-dd
server.tomcat.accesslog.pattern=%t "%r" %s %b "%{Request-Id}i"
server.tomcat.accesslog.prefix=access_log
server.tomcat.accesslog.rename-on-rotate=false
server.tomcat.accesslog.request-attributes-enabled=false
server.tomcat.accesslog.rotate=true
server.tomcat.accesslog.suffix=.log
server.tomcat.accesslog.directory=/var/log/
server.tomcat.basedir=.

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
2
3
4
@Resource
ServletWebServerApplicationContext webServerApplicationContext;
TomcatWebServer webServer = (TomcatWebServer) webServerApplicationContext.getWebServer();
String info = webServer.getTomcat().getConnector().getProtocolHandler().getExecutor().toString();

打印出来的信息大概会是:

1
org.apache.tomcat.util.threads.ThreadPoolExecutor@xxxxxxxx[Running, pool size = x, active threads = x, queued tasks = x, completed tasks = x]

然后这里的问题是,我们不能控制这个打印触发的时间,只能选择定时打印,然后根据趋势判断是否有问题。

把这些修改都集成到应用中,重新测试然后对比日志。

access log如预测一样没有打印任何有用的信息,Tomcat性能监控日志打印出的信息显示确实负载很高。

Tomcat性能调优

分析到了这个阶段,其实已经很难继续往下挖掘了,链路基本上也摸清楚了。

仍然无法定位的问题;

  • 如果队列满,那么一般结果是connection refused,对应502,这个跟504预期对不上
  • 请求到底丢失在connector连接上还是connector转发上

如果继续排查,最具备可行性的办法变成了自定义connector,这排查成本可就太大了些,再说也没给我这么多时间。

技术解决不了的业务解决,于是跟前方沟通一波,一番努力。前方要求:那你调优下吧,把504的比例降到十万分之一。

调优就直观多了,关键参数:

1
2
3
server.tomcat.accept-count=100
server.tomcat.threads.max=200
server.tomcat.max-connections=10000

以上为默认值。

accept-count调大一点,如果cpu核数够多,thread也调大一点,connection一般够用,且有长连接,问题不大。

因为上面排查过程中打印了参数,队列中的等待一直比较多,线程数基本跑满,看了下服务器cpu分的确实也还可以,就把线程多给了一倍,accept-count给到500,再压504数据达标。

到这里,写个报告给前方,结束。

一点吐槽

个人觉得这个504排查过程中我对tomcat细节的了解提升大于业务稳定性的提升,干了个不太开心的活,那就记一下吧。