一次JVM OOM问题的分析记录
OOM问题发生在客户的开发环境,系统是一个监控系统,表现为先高CPU,页面极卡,最后发生OOM。问实施人员拿到Heap Dump文件。来看看到底是内存不够用溢出了,还是发生了内存泄漏。
Heap Dump
- jdk自带的jvisualvm可以用,但是表现在我电脑上卡的不行。Dump文件接近7G。
- jprofiler,商用。本次分析借用其试用的10天。
Classes
查看到LinkedBlockingQueue$Node的个数到达了1亿6千万的层次,占用了近4G的内存。这个类是jdk自带并发包下的阻塞队列的内部类,猜测是有人创建了一个无界队列,放在线程池里,或者其他地方使用。
Biggest Objects
- 从这两个CuratorZookeeperClient的大小接近4G,使用了全部的JVM的大小。点来开观察层次,引用。
- EventThread类的实例中创建的waitingEvents是导致本次OOM的元凶。这是zookeeper的客户端curator的代码,竟然会导致OOM?来查看代码一探究竟。
EventThread
class EventThread extends ZooKeeperThread { // 看到没有,这个线程它持有了一个无界队列 private final LinkedBlockingQueue<Object> waitingEvents = new LinkedBlockingQueue<Object>(); EventThread() { super(makeThreadName("-EventThread")); setDaemon(true); } }
- 这个无界队列用来接收等待处理的事件,看看它的调用。
- 可以看到,有多处进行add,也有一处进行take消费。那么大致就可以猜测,消费能力远远跟不上生产的速度时,最终就会导致OOM。刚好这个系统就是对zookeeper添加了监听,监听某个节点下的事件。
- 来看看curator对这个事件的消费具体是如何的
@Override public void run() { try { isRunning = true; while (true) { Object event = waitingEvents.take(); if (event == eventOfDeath) { wasKilled = true; } else { processEvent(event); } if (wasKilled) synchronized (waitingEvents) { if (waitingEvents.isEmpty()) { isRunning = false; break; } } } } catch (InterruptedException e) { LOG.error("Event thread exiting due to interruption", e); } LOG.info("EventThread shut down for session: 0x{}", Long.toHexString(getSessionId())); }
- 在这个线程中的run方法中有个while(true)进行一直消费。本系统中,在curator将事件返回到应用后,会做一定的处理,理论上耗时不会很高,但是也架不住生产的速度过于快。
- 事情已经趋于明朗,zookeeper上更新的数据过快,导致系统监听该节点下的变化并处理的速度都跟不上。
问题定性
本次问题是一个无界队列的消费速度跟不上生产速度导致的内存溢出。
问题解决
- 前提
本系统是一个监控系统,被监听的系统约定将数据按照约定格式写在zookeeper中。 - 分析生产速度
被监控的微服务平台(内部系统)会将一些基本数据,以及交易总笔数、成功笔数等交易汇总信息写在zookeeper上,若无交易时也会定期刷新。生产的速度就和微服务的数量、交易速度呈现一个线性的关系。 - 分析消费速度
监控系统的一个curator客户端的接收线程调用回调处理,处理速度有限。(具体回调代码不能贴~) - 解决方法解决的宗旨就是消费的速度可以跟上生产的速度。但是如何以一个系统去匹敌一个平台呢,想屁吃呢。。。毫无疑问,总是要有舍弃的!
- 对于监控平台,往往只关心最新的,历史总会被覆盖,所以可以丢弃一些队列中老的数据。那么问题的解决方法就有了,在curator的回调方法中,将事件扔入一个新的线程池进行处理,加快处理速度,设置有限容量的队列,并且明确拒绝策略,此处最合适的就是丢弃队列中最长时间的事件任务,因为它已经没有意义。如此一来,就不会导致OOM,即时的将curator的无界队列的任务分发到线程池里去。
- 不再采用监听机制,监控的时效性要求并没有那么高。可以将监听改为轮询,由监控平台主动轮询zookeeper中的节点再进行解析。
吐槽
- 架构设计无权决定,真的不是很明白,将交易笔数汇总这种实时的数据放在zookeeper这种强一致性的分布式中间件,再让时效性以及精确性要求都不是十分苛刻的监控平台进行监控的设计。随着微服务数量、交易速率、连接数的上升,zookeeper必将面临巨大的压力。。
- 就像redis这种高频修改的中间件上其实也有监听机制,但是基本没人去用一样(redis并不强一致性,即使我们也很少遇到那种极端情况)。
分割线
补充更新
- 阅读dubbo官网文档时,看到下面这句话,有异曲同工之妙~他山之石可以攻玉。
http://dubbo.apache.org/zh-cn/docs/user/maturity.html