服务响应超时问题排查

现象:
部署在新搭建的服务器上的服务一直处于未响应状态,每次发送请求等待若干秒之后只能接收到底层返回的超时报错而不是正常返回报文。

解决过程:
我们系统包括多个业务微服务一个网关服务,一般请求都是先发送到网关服务里面处理,然后网关服务根据自定义配置与请求参数去路由到相应业务服务,然后最终的返回信息也是交由给网关统一返回。所以正常情况下,一次服务调用引发的程序执行,网关服务与相应的业务服务配置好的日志文件都会有日志刷出来。
为了看看是什么情况,让测试重现下场景。等测试操作发送请求时,监控日志是可以看到有刷新并最终把请求给路由到了业务服务的,但是业务服务的日志确实始终没动静的。

现在可以总结下问题:
1:网关服务应该是没问题的,从日志看是可以接收到请求并且已经将请求路由到相应业务服务了。
2:业务服务的最近一条日志记录还是十几分钟前的,而且从日志可以看出那次请求也没正常处理完,而是停在了将请求参数入库的位置。(这里怀疑与数据库交互应该也有问题,先记下)

虽然看到个入库不正常的点,但是好像跟现在超时没多大关系,因为现在是程序完全没执行呀(没刷日志)。

接着后面一顿操作~

在管理中心(公司内部的一个环境管理前端工具,在dubbox的基础上再封装了一层,拥有jvm参数配置、war包部署、服务状态管理、数据源配置等众多功能)查看服务部署状态,是正常启动的,并没有异常或被停掉;

用top查看服务器资源使用情况,CPU和内存使用率也不高啊;
把USER列给去掉了,大家不要觉得奇怪

用jstat看了下jvm gc的频率,并没有发生疯狂full gc的情况;
用jmap查看JVM内存使用情况,新生代、老年大、持久带使用率都还算不高;
在这里插入图片描述

然后又换到其他目录去看服务器相关的日志,还是没发现什么异常情况。

来来回回折腾了一个小时左右,这时候也不早了,本来都收拾东西准备跑路打算明天再看的。突然想起是不是线程的问题呀,然后用jstack打印了一下线程栈,拖到本地来瞄一眼,结果一看还真看出了点东西。

在这个线程堆栈文件中,能够看到已经有5个业务服务请求的工作线程了。因为我们每个业务服务配置的最大线程数刚好是5,即同一时刻最多只能处理5个请求,而现在已经有5个请求在处理了,就算总入口再将请求向业务服务转发也处理不了,这是程序会自动先将请求缓存到队列中,除非有5个请求线程被业务服务执行完了才会去队列中取下一个继续执行,难怪服务会一直超时未响应了。

另外,这5个请求线程都处于Object.wait()(这个就是在等待synchronized同步锁唤醒的情况了)的状态,而且不止这5个服务A的工作线程,还有很多其他定时服务也同样处于Object.wait()状态,并且线程堆栈显示都是这样的:
在这里插入图片描述

从这个线程堆栈可以看出是在操作数据库拿连接的时候一直阻塞在那里了(这就对应的上面的问题总结的第二点,即业务服务最近一次日志记录是停在请求入库的位置)。所以从最开始的第一个请求卡顿到这里不能再往下执行,然后接下来四个也同样卡在这个位置执行不了,再到后面的请求干脆就全缓存在队列中连程序都不会执行日志也不会刷新了。

但是为什么会阻塞在操作数据库拿连接的地方呢,继续往下挖,最终定位到GenericObjectPool。
GenericObjectPool是什么呢,它是一个对象池,而java很多池管理的概念,如线程池、数据库连接池其实都是继承自它的。
因为这个是操作数据库时的线程栈,而且堆栈的顶层是阻塞到GenericObjectPool的逻辑内。所以这时候怀疑是服务配置的数据源可能有问题。

之前说到我们用的管理中心是可以对数据源进行配置的,此时再登上管理中心,找到数据源配置管理页面。看到数据源的配置中maxActive(最大连接数)=0、maxIdle(最大空闲连接数)=0,maxWait(最大等待时间)=-1。顿时心中一万只神兽崩腾而过。。。。

稍微解释一下,maxActive为0会让你永远从数据库拿不到连接。maxIdle为0,会导致连接池中永远没有空闲连接数,对象都是在需要的时候创建、用完的时候又立马destroy了。而maxWait为-1,即如果拿不到连接会无限等待。尤其是在当被依赖的服务由于某些故障而响应极慢时,会有大量线程阻塞,最终导致线程耗尽,服务卡死,用户请求被拒绝。(怀疑是新搭建的环境默认就是这个值后面没有自定义修改的)

后记:
后面把maxActive、maxIdle都设为80,maxWait设为3000,单位为毫秒(要根据具体情况设值),然后重启数据源使其配置生效,再发请求调用服务已经能正常处理了。

总结:
原因总结就是:数据源的可用连接数配置不当(为0)导致程序需要对数据库进行操作的时候获取不到连接。然后数据源超时时间又设为-1(永不超时)导致程序一直卡死在那里。因为程序给每个服务定义的并发最大线程数是5,即同时最多只能处理5个线程,所以除了前5个请求能刷日志到操作数据库的位置,然后由底层返回超时给调用方(而不是服务返回超时),后续的服务调用请求都会缓存到队列里面不会到达应用服务所以程序也不会执行,日志就更不会刷新了。

其实最终解决问题的时候,回头反思下是觉得自己走了很大的弯路的。当一开始看到日志停顿在请求入库的时候就应该静下心来想想这个是为什么?嗯,不要放过任何一个疑点。经验丰富的人可能一看到这个就会想到要去看线程停顿在哪里?是什么原因导致的?看到GenericObjectPool的时候就会猜到是数据源配置的问题,再一检查配置参数就知道了。

另外刚开始写博客,本身也不是技术大牛,所以很多地方可能写的不清楚或者词不达意的,请轻喷。

猜你喜欢

转载自blog.csdn.net/insomsia/article/details/84585441
今日推荐