服务提供者假死,记一次full gc问题排查

原创 吴就业 164 0 2020-01-18

本文为博主原创文章,未经博主允许不得转载。

本文链接:https://wujiuye.com/article/1bd2fed544c5411395c4b6d246fc9819

作者:吴就业
链接:https://wujiuye.com/article/1bd2fed544c5411395c4b6d246fc9819
来源:吴就业的网络日记
本文为博主原创文章,未经博主允许不得转载。

本篇文章写于2020年01月18日,从公众号同步过来(博客搬家),本篇为原创文章。

线上某服务一直运行很稳定,最近突然就cpu百分百,rpc远程调用全部失败,并走了mock逻辑。重启后,一个小时后问题又重现。于是dump线程栈信息,但不仔细看也看不出什么问题。于是就有了一番排查历程。

前一天,我dump线程栈信息后发现占用cpu高的线程是阿里的限流组件sentinel,并根据线程栈信息知道,在sentinel控制台会拉取该服务的qps统计信息。这个接口会读取本地文件内容,因为qps信息是输出到/root/logs/csp/目录下的某个文件的,并且是用NIO方式读取文件。因此我猜测是sentinel在某种条件下会触发死循环bug,也就能解释得清楚为什么cpu会出现百分百的使用率。

带着这个猜测,我调试了一遍源码,也并未复现。于是我将限流功能暂时去掉,以为问题就能解决,但是没想到第二天早上又看到这个问题。于是我先将该服务的其它节点重启,留下一个节点,并将剩下的节点从注册中心中剔除。可没想到的是,注册中心已经没有这个节点了,应该是所有该服务的节点都与注册中心失联了,其它是因为重启之后重新注册到注册中心的。

既然没有注册到注册中心,其它服务调用直接走mock逻辑也解释得通。继续前一天的进度,我首先是查看文件句柄数是否打开很多,于是使用lsof查看,果然不出所料。

查看文件句柄数是否打开很多

进程15567打开的文件句柄数20多万,因此我更愿意相信是sentinel读取文件出现bug的猜测,因为我想起前一天限流功能还没有完全去除,只是去除了限流配置。接着查看这个进程是否打开了/root/logs/csp目录下的文件(这个目录是从限流组件的源码中找出来的)。

查看这个进程是否打开了/root/logs/csp目录下的文件

遗憾的是看不到每个文件的打开数量。接着我用lsof -p 进程id命令查看下,该进程打开的文件句柄信息。

查看这个进程打开的文件句柄信息

于是我发现打开最多的不是/root/logs/csp目录下的文件,而是redis连接。这下只能将限流组件sentinelbug的猜测去掉了,因为昨天去掉配置后,也并没有看到sentinel线程占用cpu过高的现象了,只能排除。继续统计redissocket打开数量。

统计redis的socket打开数量

确实对上了,文件句柄数达上限的问题确实是与redis-cluster有关。于是我使用redis-cli命令连接上redis服务,看下是否连接数增多了,是否与该进程持有的文件句柄数对得上。因为前一天我也发现某消息消费服务偶尔出现连接超时的情况。

查看redis连接是否增多

然而并未发现什么异常,客户端连接数与平常一样。只能悬于此了。于是我又去分析线程栈信息,发现大量的dubbo线程处于wait状态,且都是阻塞在归还redis连接到连接池以及从连接池中申请连接这一步。

线程栈

还有少数几个dubbo线程当前是runing状态,原因是这些线程走了内存缓存。代码中我是用aop实现内存缓存的,命中内存缓存的不会往下走业务逻辑,也就不会用到redis。因此这些线程处理的请求是正常的。

请求命中内存缓存的线程栈

那么为什么cpu会百分百呢?大片的业务线程都已经被阻塞住了,都在wait状态,到底是什么线程占用如此高的cpu。于是我才想到用top命令去查看。

top命令查看cpu占用高的进程

看到两个非业务相关的线程,占用非常高的cpu,于是又回去琢磨线程栈信息,与top命令显示的command列对得上的也就只有两个垃圾回收线程。

垃圾回收线程占用非常高的cpu

于是就是jstat命令查看gc垃圾回收情况。

jstat命令查看gc情况

连续使用jstat命令查看gc情况,发现每次执行命令,大概是1秒多,fgc的次数就加1了。问题的真相已经非常接近。找到这里,接着就是排查业务代码。

由于使用了内存缓存,会导致full gc的情况很正常。我首先想到两点,一是缓存数据太多,二是缓存数据清理的定时任务执行失败了。

由于之前该服务一直很稳定,猜测可能是数据格式不正确触发的bug,导致数据清理失败,越积越多。但是看了代码后,我是加了try-catch的,出异常也会移除缓存,只是清理时间间隔比较长,一个小时才清理一次。那么就是数据量暴增的原因。

字符串转数字异常

到此,我就不再往下分析。去掉内存缓存?那就需要加好几台机器来扛下目前的并发数,而且还需要添加Redis节点,想想也是不可能的。所以只能计算业务所需要的内存,提升实例的内存。

通过数据库查询,估算需要缓存的记录数是四百多万,本地写了个测试用例,计算四百多万记录大概消耗700m的内存,因此,只需要将机器的内存稍微提高一点就没有问题了。除非数据量还会暴涨,这也是某个业务功能的实现导致的。

预估内存消耗

#后端

声明:公众号、CSDN、掘金的曾用名:“Java艺术”,因此您可能看到一些早期的文章的图片有“Java艺术”的水印。

文章推荐

Java锁事之Unsafe、CAS、AQS知识点总结

Unsafe、CAS、AQS是我们了解Java中除synchronized之外的锁必须要掌握的重要知识点。CAS是一个比较和替换的原子操作,AQS的实现强依赖CAS,而在Java中,CAS操作需通过使用Unsafe提供的方法实现。

Dubbo路由功能实现灰度发布及源码分析

灰度发布是实现新旧版本平滑过渡的一种发布方式,即让一部分服务更新到新版本,如果这部分服务没有什么问题,再将其它旧版本的服务更新。而实现简单的灰度发布我们可以使用版本号控制,每次发布都更新版本号,新更新的服务就不会调用旧的服务提供者。

JVM垃圾回收GC Root与安全点Safepoint

我看很多资料在介绍`GC Root`时,并没有说栈帧的操作数栈上引用的对象也是`GC Root`,包括我去翻阅《深入理解Java虚拟机》这本书也是一样。所以我才好奇。

JITWatch查看字节码被JIT编译后的汇编代码

最近看书看到关于`volitale`关键字与`jmm`内存模型的介绍,这个知识点似乎看了好多次,背都能背下来了。但理论性的东西真的很容易忘记,看不到摸不着。于是乎,我上网搜索看底层机器指令的实现,发现不少文章说可以看到`java`编译后的汇编代码,于是了解到`jitwatch`这个工具,从名字上也能看出`jit`编译器监视的意思。

Spring Cloud Kubernetes入门必知运维知识之Docker

容器化部署就是一次配置到处使用,例如将安装nginx配置nginx这一系列工作制作成一个镜像,在服务器上通过docker拉取镜像并启动容器即可完成nginx的部署。

Dubbo源码,详解dubbo协议数据包及解包过程

Dubbo框架的传输层默认使用dubbo协议,这也是一种RPC远程通信协议。学习Dubbo,我们有必要了解dubbo协议长什么样,最好的办法就是从源码中寻找答案。