业务背景
- 生产消费模型
- 生产者通过
异步http
的方式定时拉取外围系统数据,在回调中将数据推送到queue
- 消费者从
queue
中take数据,然后解析,每个元素可能会被解析成一个或多个metric meta
对象 - 通过监控系统提供的SDK,将
metric meta
对象推送到Metric Queue
中 - 监控系统SDK将数据上报到监控平台
- 因为目前只是在拿几个外围系统测试,数据量不大,每分钟大概2000数据
问题现象
- 收到应用
内存
、CPU
使用率过高告警,看了一下,两者都接近100%
- 先重启快速恢复服务,然后密切观察服务资源状况
- 随着服务运行时长的不断增加,内存使用率也在不停的增加
- 随着内存使用率的不断增加,CPU的使用率也在不断增加。在刚开始启动服务的时候,CPU使用率很低,10%不到。但就拉取得数据量来说,不存在说数据越来越大得情况,基本是均与的
问题排查
根据目前已知的问题现象,可以发现有几个点很值得关注:
- 内存在不停的缓慢增长。一般什么问题会导致这种现象呢?
- 内存泄漏:该回收的对象没有被回收,随着时间的推移,脏对象越来越多,直到OOM。这是BUG,得修复
- 请求量太大:假如堆容量为1000单位,系统每秒可以处理(回收:执行完业务逻辑之后才会回收)90单位,每秒请求量为100单位。如果应用一直这样运行,总有一个是时间点会把堆撑爆,然后OOM。这种情况得话,我们就需要提高系统得吞吐量,得优化
- CPU使用率随着内存使用率得增加而不断增加。这个我第一想到的原因就是
GC
,实际上就是GC
,这个我们只要验证一下就可以了
上面两个分析,其实已经提供了很多有用的信息了,接下来我们只需要结合业务背景,去验证我们的猜想,并最终修复问题就可以了
问题猜想
我们先不管CPU
使用率过高的问题,这就是因为GC
导致的,这不是问题的关键,关键是为什么会频繁GC
?结合内存使用率不停增长
和业务背景
,做出以下猜想
生产速度大于消费速度
也就是说生产速度太快,消费速度太慢。假如是这种情况,会有哪些现象?
- 随着时间的推移,在
Queue
中堆积的对象越来越多 - 随着不可回收的对象越来越多,堆可用空间越来越少
- 生产速度和消费速度保持不变,意味着在单位时间内,需要的堆空间保持不变
- JVM为了保证有足够的可用可空间分配对象,需要根据情况进行GC
- 结合上面4点,随着堆可用空间的越来越小,单位时间内需要的可用空间保持不变,意味着GC的间隔时间会越来越小,即GC会越来越频繁
- 随着GC越来越频繁,CPU使用率会越来越高
- 最终的表现:系统瘫痪,CPU一直在进行无效GC,因为每次GC都无法回收有效对象
从问题表现来看,和我们出现的问题很像。但结合业务背景就发现不太可能:
- 数据量很小,系统完全可以处理的过来,也不存在什么耗时操作
- 要验证也可以简单,可以直接观察消费情况。如果是这种情况,消费一定会有很大的延迟的。或者可以把生产消费的速率打印出来,对比一下就知道了
内存泄漏
如果是内存泄漏的话,那么需要借助一些工具来排查,大致思路如下:
- 通过
jmap
查看堆中大对象,大对象主要包括两方面:占空间大的
、数量多的
- 结合步骤1,思考可能是哪部分代码有问题
- 如果步骤2还是没有思绪,可以借助
MAT
、jvisualvm
等工具分析一下
验证猜想
CPU使用率过高猜想验证
上面我们已经猜想了是频繁GC
导致的CPU使用率过高,下面验证一下:
jstat -gcutil pid
top -Hp pid
jstack pid |grep 0xxx
- 发现就是几个GC线程CPU使用率比较高,验证通过
内存泄漏猜想验证
jmap -histo pid |more
- 定位到关键对象
xxEvent
,发现它的数量超过了100W
- 检查相关的业务代码,主要有会从这几个方面检查:
创建的源头
、持有者
、销毁点
。有时候可能不太好找,需要一点耐心
根据上面3点,最终找到的xxEvent
相关源头就是:生产者
、消费者
、Queue
。
上面有一点很值得注意,xxEvent对象超过了100W。根据我们每分钟处理2000条数据的速度来算,Queue
中还存在超过100W对象明显不合理:一个小时才产生12W条数据,100W就意味着至少8个小时,并且这8个小时内,消费者完全不消费,根据实际情况来看,这不可能
。
所以,这铁定是一个内存泄漏问题
问题原因
- 项目中用到了
Disruptor
框架 - 创建
Disruptor
对象的时候,需要我们传入一个ringBufferSize
,表示该队列的最大长度 Disruptor
对象在初始的时候,会创建一个ringBufferSize
大小的数组,同时根据我们传入的eventFactory
创建ringBufferSize
个对象,这里对应的就是xxEvent
- 代码中
ringBufferSize
大小为1024 * 1024
,也就是大概100W
。也就是说,在Disruptor
对象初始的时候,就完成了100W
个xxEvent
对象的创建 Disruptor
底层是基于数组,内部持有消费指针和生产指针,消费者消费之后,并不会直接将该元素从数组中移除,而是改变消费指针的位置。如果生产指针到了数组尾部,结论下来会覆盖前面的元素- 结合上面5点再来分析:
Disruptor
对象创建的时候,就完成了100W
个xxEvent
对象的创建,但因为此时xxEvent
对象是空对象,里面没数据,所以此时也不会占用很多内存。随着生产者在持续生产元素,队列中的xxEvent
不停的被填充,被填充的xxEvent
对象越来越多。因为这些对象被队列引用,即使消费完了之后也不会被回收。所以,可用内存越来越少,最终导致不可回收的对象装满了新生代和老年代,然后JVM一直在无效GC
问题解决
ringBufferSize
设置为512
,部署并观察现象- 用Java原生
BlockingQueue
其实完全可以满足需求
补充
1、有关于上面说的异步pull数据
- 通过观察代码,发现有一个每10s一次的定时任务,通过
异步http
的方式拉取外围系统数据。可以先认为一个异步线程对应一个外围系统,当然线程数肯定有限制 - 在该任务中,会通过异步方式依次从所有外围系统拉取数据。这里想到个问题:假如现在有100个外围系统,因为是异步请求,我们可能在0.1s之内就把100个请求发出去了,然后这个任务就结束了,10s之后再次执行。多个异步的请求结果可能会同时到达,然后将数据丢到
queue
中。假如100个请求的结果消费者3s可以消费完,剩下的7s钟将无事可做,在这3s内,消费逻辑可能会占用大量的CPU,从而导致系统中的其他业务耗时增加。如果匀速消费,将消费请求分摊到剩下的7s内,是不是会好一点?不过这样就会导致消费延时更大了,还是要结合具体业务场景来做吧
© 版权声明
文章版权归作者所有,未经允许请勿转载。
THE END