查找这个接口的调用_事务处理不当,线上接口又双叒内存泄漏了!(附图解问题全过程)...
情景
項目上線了一個接口,先灰度一臺機(jī)器觀察調(diào)用情況;
接口不斷的調(diào)用,過了一段時間,發(fā)現(xiàn)機(jī)器上的接口調(diào)用開始報 OOM異常 !
當(dāng)天就是上線deadline了,刺激。。
發(fā)現(xiàn)問題
第一步,使用 jps 命令獲取出問題jvm進(jìn)程的進(jìn)程ID
使用 jps -l -m 獲取到當(dāng)前jvm進(jìn)程的pid,通過上述命令獲取到了服務(wù)的進(jìn)程號:427726 (此處假設(shè)為這個)
jps命令
jps (JVM Process Status Tool):顯示指定系統(tǒng)內(nèi)所有的HotSpot虛擬機(jī)進(jìn)程
jps -l -m : 參數(shù)-l列出機(jī)器上所有jvm進(jìn)程,-m顯示出JVM啟動時傳遞給main()的參數(shù)
第二步,使用 jstat 觀察jvm狀態(tài),發(fā)現(xiàn)問題
因為是OOM異常,所以我們首先重啟機(jī)器觀察了JVM的運行情況;
我們使用 jstat -gc pid time 命令觀察GC,發(fā)現(xiàn)GC在YGC后,GC掉的內(nèi)存并不多,每次YGC后都有一部分內(nèi)存未回收,導(dǎo)致在多次YGC后回收不掉的內(nèi)存被挪到堆的old區(qū),old滿了之后FGC發(fā)現(xiàn)也是回收不掉;
這里基本可以確定是內(nèi)存泄漏的問題了,下面我們有簡單看了下機(jī)器的cpu、內(nèi)存、磁盤狀態(tài)
jstat命令:
jstat (JVM statistics Monitoring)是用于監(jiān)視虛擬機(jī)運行時狀態(tài)信息的命令,它可以顯示出虛擬機(jī)進(jìn)程中的類裝載、內(nèi)存、垃圾收集、JIT編譯等運行數(shù)據(jù)。
jstat -gc pid time : -gc 監(jiān)控jvm的gc信息,pid 監(jiān)控的jvm進(jìn)程id,time每隔多少毫秒刷新一次
jstat -gccause pid time : -gccause 監(jiān)控gc信息并顯示上次gc原因,pid 監(jiān)控的jvm進(jìn)程id,time每隔多少毫秒刷新一次
jstat -class pid time : -class 監(jiān)控jvm的類加載信息,pid 監(jiān)控的jvm進(jìn)程id,time每隔多少毫秒刷新一次
在這里先簡單說一下,堆的GC:
年齡達(dá)到一定值(年齡閾值,可以通過-XX:MaxTenuringThreshold來設(shè)置)的對象會被移動到年老代中,沒有達(dá)到閾值的對象會被復(fù)制到“To”區(qū)域。經(jīng)過這次GC后,Eden區(qū)和From區(qū)已經(jīng)被清空。這個時候,“From”和“To”會交換他們的角色,也就是新的“To”就是上次GC前的“From”,新的“From”就是上次GC前的“To”。不管怎樣,都會保證名為To的Survivor區(qū)域是空的,minor GC會一直重復(fù)這樣的過程。
第三步,觀察機(jī)器狀態(tài),確認(rèn)問題
使用 top -p pid 獲取進(jìn)程的cpu和內(nèi)存使用率;查看RES 和 %CPU %MEM三個指標(biāo):
在這里先簡單說一下,top命令展示的內(nèi)容:
VIRT:virtual memory usage 虛擬內(nèi)存
1、進(jìn)程“需要的”虛擬內(nèi)存大小,包括進(jìn)程使用的庫、代碼、數(shù)據(jù)等
2、假如進(jìn)程申請100m的內(nèi)存,但實際只使用了10m,那么它會增長100m,而不是實際的使用量
RES:resident memory usage 常駐內(nèi)存
1、進(jìn)程當(dāng)前使用的內(nèi)存大小,但不包括swap out
2、包含其他進(jìn)程的共享
3、如果申請100m的內(nèi)存,實際使用10m,它只增長10m,與VIRT相反
4、關(guān)于庫占用內(nèi)存的情況,它只統(tǒng)計加載的庫文件所占內(nèi)存大小
SHR:shared memory 共享內(nèi)存
1、除了自身進(jìn)程的共享內(nèi)存,也包括其他進(jìn)程的共享內(nèi)存
2、雖然進(jìn)程只使用了幾個共享庫的函數(shù),但它包含了整個共享庫的大小
3、計算某個進(jìn)程所占的物理內(nèi)存大小公式:RES – SHR
4、swap out后,它將會降下來
DATA
1、數(shù)據(jù)占用的內(nèi)存。如果top沒有顯示,按f鍵可以顯示出來。
2、真正的該程序要求的數(shù)據(jù)空間,是真正在運行中要使用的。
ps : 如果程序占用內(nèi)存比較多,說明程序申請內(nèi)存多,實際使用的空間也多。
如果程序占用虛存比較多,說明程序申請來很多空間,但是沒有使用。
發(fā)現(xiàn)機(jī)器的自身狀態(tài)不存在問題, so毋庸置疑,發(fā)現(xiàn)問題了,典型的內(nèi)存泄漏。。
第四步,使用jmap獲取jvm進(jìn)程dump文件
我們使用 jmap -dump:format=b,file=dump_file_name pid 命令,將當(dāng)前機(jī)器的jvm的狀態(tài)dump下來或缺的一份dump文件,用做下面的分析
jmap命令:
jmap (JVM Memory Map)命令用于生成heap dump文件,還可以查詢finalize執(zhí)行隊列、Java堆和永久代的詳細(xì)信息,如當(dāng)前使用率、當(dāng)前使用的是哪種收集器等。
jmap -dump:format=b,file=dump_file_name pid : file=指定輸出數(shù)據(jù)文件名, pid jvm進(jìn)程號
接下來,回滾灰度的機(jī)器,開始解決問題=.=
解決問題
第一步,dump文件分析
在這里,我們分析dump文件,使用的 Jprofiler 軟件,就是下面這個東東:
具體的使用方法,在這就不再贅述了,下面將dump文件導(dǎo)入到 Jprofiler 中:
選擇 Heap Walker 中的 Current Object Set ,這里面顯示的是當(dāng)前的類的占用資源,從占用空間從大到小排序;
從上圖中,沒有觀察出什么問題,我們點擊 Biggest Objects ,查看哪個對象的占用的內(nèi)存高:
從上圖中,我們發(fā)現(xiàn) org.janusgraph.graphdb.database.StandardJanusGraph 這個對象居然占用了高達(dá) 724M 的內(nèi)存! 看來內(nèi)存泄漏八九不離十就是這個對象的問題了!
再點開看看 ,如下圖,可以發(fā)現(xiàn)是一個 openTransactions 的類型為 ConcurrentHashMap 的數(shù)據(jù)結(jié)構(gòu):
第二步,源碼查找定位代碼
這到底是什么對象呢,去項目中查找一下,打開idea-打開項目-雙擊shift鍵-打開全局類查找-輸入 StandardJanusGraph ,如下圖:
發(fā)現(xiàn)是我們項目使用的圖數(shù)據(jù)庫 janusgraph 的一個類,找到對應(yīng)的數(shù)據(jù)結(jié)構(gòu):
類型定義:
private Set openTransactions;初始化為一個ConcurrentHashMap:
openTransactions = Collections.newSetFromMap(new ConcurrentHashMap(100, 0.75f, 1));觀察上述代碼,我們可以看到,里面的存儲的 StandardJanusGraphTx 從字面意義上理解是janusgraph框架中的事務(wù)對象,下面往上追一下代碼,看看什么時候會往這個Map中賦值:
// 找到執(zhí)行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對象進(jìn)行了add return tx; } catch (BackendException e) { throw new JanusGraphException("Could not start new transaction", e); } } // 上述發(fā)現(xiàn),是一個newTransaction,創(chuàng)建事務(wù)的一個方法,為確保起見,再往上跟找到調(diào)用上述方法的類: public JanusGraphTransaction start() { TransactionConfiguration immutable = new ImmutableTxCfg(isReadOnly, hasEnabledBatchLoading, assignIDsImmediately, preloadedData, forceIndexUsage, verifyExternalVertexExistence, verifyInternalVertexExistence, acquireLocks, verifyUniqueness, propertyPrefetching, singleThreaded, threadBound, getTimestampProvider(), userCommitTime, indexCacheWeight, getVertexCacheSize(), getDirtyVertexSize(), logIdentifier, restrictedPartitions, groupName, defaultSchemaMaker, customOptions); return graph.newTransaction(immutable); // 注意!此處調(diào)用了上述的newTransaction方法 } // 接著找上層調(diào)用,發(fā)現(xiàn)了最上層的方法 public JanusGraphTransaction newTransaction() { return buildTransaction().start(); // 此處調(diào)用了上述的start方法 }在我們對圖數(shù)據(jù)庫中圖數(shù)據(jù)操作的過程中,采用的是手動創(chuàng)建事務(wù)的方式,在每次查詢圖數(shù)據(jù)庫之前,我們都會調(diào)用類似于 dataDao.begin() 代碼,
其中就是調(diào)用的 public JanusGraphTransaction newTransaction() 這個方法;
最后,我們簡單的看下源碼可以發(fā)現(xiàn),從上述內(nèi)存泄漏的map中去除數(shù)據(jù)的邏輯就是 commit事務(wù)的接口,調(diào)用鏈如下:
public void closeTransaction(StandardJanusGraphTx tx) { openTransactions.remove(tx); // 從map中刪除StandardJanusGraphTx對象 } private void releaseTransaction() { isOpen = false; graph.closeTransaction(this); // 調(diào)用上述closeTransaction方法 vertexCache.close(); } 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(); } try { if (hasModifications()) { graph.commit(addedRelations.getAll(), deletedRelations.values(), this); } else { txHandle.commit(); // 這個commit方法中釋放事務(wù)也是調(diào)用releaseTransaction } success = true; } catch (Exception e) { try { txHandle.rollback(); } catch (BackendException e1) { throw new JanusGraphException("Could not rollback after a failed commit", e); } throw new JanusGraphException("Could not commit transaction due to exception during persistence", e); } finally { releaseTransaction(); // // 調(diào)用releaseTransaction if (null != config.getGroupName() && !success) { MetricManager.INSTANCE.getCounter(config.getGroupName(), "tx", "commit.exceptions").inc(); } } }終于,我們找到了內(nèi)存泄漏的根源所在:項目代碼中存在調(diào)用了事務(wù) begin 但是沒有 commit的代碼!
第三步,修復(fù)問題驗證
解決問題: 找到內(nèi)存泄漏接口的代碼,并發(fā)現(xiàn)了沒有commit()的位置,try-catch-finally中添加上了commit()代碼;
提交-部署-發(fā)布-灰度一臺機(jī)器后觀察內(nèi)存泄漏的現(xiàn)象消失,GC回收正常;
內(nèi)存泄漏問題解決,項目如期上線~
最后
大家,有沒有遇到過內(nèi)存泄漏的情況,歡迎在評論區(qū)說出你的故事=.=
總結(jié)
以上是生活随笔為你收集整理的查找这个接口的调用_事务处理不当,线上接口又双叒内存泄漏了!(附图解问题全过程)...的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 2vec需要归一化吗_LTSM模型预测数
- 下一篇: java sorted排序_【算法】排序