情景 项目上线了一个接口,先灰度一台机器观察调用情况; 接口不断的调用,过了一段时间,发现机器上的接口调用开始报OOM异常 ! 当天就是上线deadline了,刺激。。 发现问题 第一步,使用j…
情景 1 项目上线了一个接口,先灰度一台机器观察调用情况; 接口不断的调用,过了一段时间,发现机器上的接口调用开始报 `OOM异常` ! 当天就是上线deadline了,刺激。。 ')
发现问题 第一步,使用 jps 命令获取出问题jvm进程的进程ID 1 使用 `jps -l -m` 获取到当前jvm进程的pid,通过上述命令获取到了服务的进程号:427726 (此处假设为这个) ') jps命令
第二步,使用 jstat 观察jvm状态,发现问题 我们使用 jstat -gc pid time 命令观察GC,发现GC在YGC后,GC掉的内存并不多,每次YGC后都有一部分内存未回收,导致在多次YGC后回收不掉的内存被挪到堆的old区,old满了之后FGC发现也是回收不掉; 这里基本可以确定是内存泄漏的问题了,下面我们有简单看了下机器的cpu、内存、磁盘状态 jstat命令:
在这里先简单说一下,堆的GC:
第三步,观察机器状态,确认问题 1 使用 `top -p pid` 获取进程的cpu和内存使用率;查看RES 和 %CPU %MEM三个指标: ')
在这里先简单说一下,top命令展示的内容:
发现机器的自身状态不存在问题, so毋庸置疑,发现问题了,典型的内存泄漏。。
第四步,使用jmap获取jvm进程dump文件 1 我们使用 `jmap -dump:format=b,file=dump_file_name pid` 命令,将当前机器的jvm的状态dump下来或缺的一份dump文件,用做下面的分析
jmap命令:
解决问题 第一步,dump文件分析 1 2 在这里,我们分析dump文件,使用的 `Jprofiler` 软件,就是下面这个东东: ') 具体的使用方法,在这就不再赘述了,下面将dump文件导入到 `Jprofiler` 中: 选择 `Heap Walker` 中的 `Current Object Set` ,这里面显示的是当前的类的占用资源,从占用空间从大到小排序; ') 从上图中,没有观察出什么问题,我们点击 `Biggest Objects` ,查看哪个对象的占用的内存高: ') 从上图中,我们发现 ```java
org.janusgraph.graphdb.database.StandardJanusGraph
看来内存泄漏八九不离十就是这个对象的问题了! 再点开看看 ,如下图,可以发现是一个 `openTransactions` 的类型为 `ConcurrentHashMap` 的数据结构: ') 发现是我们项目使用的图数据库 `janusgraph` 的一个类,找到对应的数据结构: 类型定义: ```java ```java private Set<StandardJanusGraphTx> openTransactions;
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 初始化为一个ConcurrentHashMap: openTransactions = Collections.newSetFromMap(new ConcurrentHashMap<StandardJanusGraphTx, Boolean>(100, 0.75f, 1)); `观察上述代码,我们可以看到,里面的存储的```text StandardJanusGraphTx ``` 从字面意义上理解是janusgraph框架中的事务对象,下面往上追一下代码,看看什么时候会往这个Map中赋值: ```java // 找到执行openTransactions.add()的方法 public StandardJanusGraphTx newTransaction(final TransactionConfiguration configuration) { if (!isOpen) ExceptionFactory.graphShutdown(); try { StandardJanusGraphTx tx = new StandardJanusGraphTx(this, configuration); tx.setBackendTransaction(openBackendTransaction(tx)); openTransactions.add(tx); // 注意! 此处对上述的map对象进行了add return tx; } catch (BackendException e) { throw new JanusGraphException("Could not start new transaction", e);
}
1 2 3 public JanusGraphTransaction start () { TransactionConfiguration immutable = new ImmutableTxCfg (isReadOnly, hasEnabledBatchLoading,
assignIDsImmediately, preloadedData, forceIndexUsage, verifyExternalVertexExistence,
verifyInternalVertexExistence, acquireLocks, verifyUniqueness,
1 2 propertyPrefetching, singleThreaded, threadBound, getTimestampProvider(), userCommitTime, indexCacheWeight, getVertexCacheSize(), getDirtyVertexSize(),
logIdentifier, restrictedPartitions, groupName,
1 2 defaultSchemaMaker, customOptions); return graph.newTransaction(immutable); // 注意!此处调用了上述的newTransaction方法
}
1 2 3 public JanusGraphTransaction newTransaction () { return buildTransaction().start();
}
1 2 3 4 5 6 7 8 9 在我们对图数据库中图数据操作的过程中,采用的是手动创建事务的方式,在每次查询图数据库之前,我们都会调用类似于 ```java dataDao.begin() `代码, 其中就是调用的```java public JanusGraphTransaction newTransaction() ``` 这个方法; 最后,我们简单的看下源码可以发现,从上述内存泄漏的map中去除数据的逻辑就是 `commit` 事务的接口,调用链如下: ```java public void closeTransaction(StandardJanusGraphTx tx) { openTransactions.remove(tx); // 从map中删除StandardJanusGraphTx对象
}
1 2 3 4 private void releaseTransaction () { isOpen = false ; graph.closeTransaction(this ); vertexCache.close();
}
1 2 3 4 5 public synchronized void commit () { Preconditions.checkArgument(isOpen(), "The transaction has already been closed" ); boolean success = false ; if (null != config.getGroupName()) { MetricManager.INSTANCE.getCounter(config.getGroupName(), "tx" , "commit" ).inc();
}
1 2 3 4 5 try { if (hasModifications()) { graph.commit(addedRelations.getAll(), deletedRelations.values(), this); } else { txHandle.commit(); // 这个commit方法中释放事务也是调用releaseTransaction
}
1 2 3 4 5 6 success = true; } catch (Exception e) { try { txHandle.rollback(); } catch (BackendException e1) { throw new JanusGraphException("Could not rollback after a failed commit", e);
}
1 2 3 throw new JanusGraphException("Could not commit transaction due to exception during persistence", e); } finally { releaseTransaction(); // // 调用releaseTransaction
1 2 if (null != config.getGroupName() && !success) { MetricManager.INSTANCE.getCounter(config.getGroupName(), "tx", "commit.exceptions").inc();
}
终于,我们找到了内存泄漏的根源所在:项目代码中存在调用了事务```text begin但是没有```text commit
1 2 3 4 5 6 7 8 ###### 第三步,修复问题验证 解决问题: 找到内存泄漏接口的代码,并发现了没有commit()的位置,try-catch-finally中添加上了commit()代码; 提交-部署-发布-灰度一台机器后观察内存泄漏的现象消失,GC回收正常; 内存泄漏问题解决,项目如期上线~ ##### 最后 大家,有没有遇到过内存泄漏的情况,欢迎在评论区说出你的故事=.= 写这篇文章耗费的时间超出了我的预料,预计2个小时写完,结果花了一下午的时间...
本文标题: 事务处理不当,线上接口又双叒内存泄漏了!(附图解问全过程)
发布时间: 2019年07月08日 00:00
最后更新: 2025年12月30日 08:54
原始链接: https://haoxiang.eu.org/d3c7bc7c/
版权声明: 本文著作权归作者所有,均采用CC BY-NC-SA 4.0 许可协议,转载请注明出处!