這一天風和日麗,我很榮幸的參加進入組織的活動,這個組織依然是一群悶騷的少年,熱火朝天的甩着膀子,寫着神聖的 Java 代碼,偌大的辦公室,隻能聽見噼裡啪啦的敲擊鍵盤聲!
好騷氣的組織!!!
------------------------------------------------------------------------------------------------------------------------
進入組織後,給我随手就撩過來一個 git 位址,習慣性的通過 git clone <git-url> ,将代碼 dang 下來!
我的 IDEA 已經饑渴難耐,說時遲那時快。滑鼠飛快的飛到桌面的
, 以盲狙的速度進行了輕按兩下!經過 10s 的長達等待。主界面終于展示在我的眼前。仿佛看到了我夢中的女神——怦然心動,無法形容我的處境。
作為一個不會技術的二溜子,豈能就此作罷,掃視一眼後發現,原來是仰慕已久的 Web 項目基佬(這麼說主要是因為教育訓練機構 3 個月能生産一批,而且都是各行各業的男性同胞),瞬間心情大落!吾等倒要看看你是什麼妖孽,沒有妖孽也要給你制造一批出來。
拉出我的湯姆貓( tomcat ), 将它迅速加載進來。緊接着一個飄逸的 “Shift + F10” (IDEA 的 快捷鍵)閃過。
一個 http://localhost:8080 的界面自動打開在我的眼前。
哎呀,好帥氣的界面……
輸入測試賬号、測試密碼、登入驗證碼……一波騷操作之後,功能都可以正常使用!
待我休息三秒後,撓了撓頭,對 組織成員 A 說:嗨,帥哥,發 50 個請求過來玩玩!
哈……果然,出現了騷氣的問題,頁面請求處于 pending 狀态,過 10s 報 timeout , 背景日志報錯:
org.springframework.data.redis.RedisConnectionFailureException: Cannot get Jedis connection; nested exception is redis.clients.jedis.exceptions.JedisException: Could not get a resource from the pool
at org.springframework.data.redis.connection.jedis.JedisConnectionFactory.fetchJedisConnector(JedisConnectionFactory.java:204)
at org.springframework.data.redis.connection.jedis.JedisConnectionFactory.getConnection(JedisConnectionFactory.java:348)
at org.springframework.data.redis.core.RedisConnectionUtils.doGetConnection(RedisConnectionUtils.java:129)
at org.springframework.data.redis.core.RedisConnectionUtils.getConnection(RedisConnectionUtils.java:92)
at org.springframework.data.redis.core.RedisConnectionUtils.getConnection(RedisConnectionUtils.java:79)
at org.springframework.data.redis.core.RedisTemplate.execute(RedisTemplate.java:194)
at org.springframework.data.redis.core.RedisTemplate.execute(RedisTemplate.java:169)
at org.springframework.data.redis.core.AbstractOperations.execute(AbstractOperations.java:91)
at org.springframework.data.redis.core.DefaultValueOperations.increment(DefaultValueOperations.java:63)
注:說明一下,我們得 redis 環境配置主要為:
<property name="maxIdle" value="50"/>
<property name="minIdle" value="20"/>
<property name="usePool" value="true" />
----------------------------------------------------------------------------------------------------------------------------------
看到如此之問題,豈不是很蛋疼,立即跳起來,左手叉腰,右手指向天花闆。大喊一聲:拿我的 5 米長刀來。
我跟着異常中提示的異常堆棧資訊,我打開代碼,并定位到異常的行數位置,檢視代碼。大多都是通過 redisTemplate 來與 Redis 互動。redis 的連接配接池是通過 common-pools 來管理的,redisTemplate 之前我在其他項目也使用過,不應該會出現洩漏的問題。
懷着激動不安的心情,我進去到如下代碼中進行了代碼跟蹤:
ValueOperations<String, Object> valueOperations = redisTemplate.opsForValue();
這裡我以 valueOperations.set()進行了代碼跟蹤, set 方法的實作如下:
public void set(K key, V value) {
final byte[] rawValue = rawValue(value);
execute(new ValueDeserializingRedisCallback(key) {
protected byte[] inRedis(byte[] rawKey, RedisConnection connection) {
connection.set(rawKey, rawValue);
return null;
}
}, true);
}
追蹤代碼,set 方法調用内部,我很能确定的是,調用後,連結進行了關閉操作(代碼如下)!其實,嚴格來說,使用連接配接池,通過 borrowObject()方法擷取的,最終當然是通過 returnObject()! 讀者有興趣可以直接了解 : common-pools2.jar 源代碼了解。
/**
* Executes the given action object within a connection that can be exposed or not. Additionally, the connection can
* be pipelined. Note the results of the pipeline are discarded (making it suitable for write-only scenarios).
*
* @param <T> return type
* @param action callback object to execute
* @param exposeConnection whether to enforce exposure of the native Redis Connection to callback code
* @param pipeline whether to pipeline or not the connection for the execution
* @return object returned by the action
*/
public <T> T execute(RedisCallback<T> action, boolean exposeConnection, boolean pipeline) {
Assert.isTrue(initialized, "template not initialized; call afterPropertiesSet() before using it");
Assert.notNull(action, "Callback object must not be null");
RedisConnectionFactory factory = getConnectionFactory();
RedisConnection conn = null;
try {
if (enableTransactionSupport) {
// only bind resources in case of potential transaction synchronization
conn = RedisConnectionUtils.bindConnection(factory, enableTransactionSupport);
} else {
conn = RedisConnectionUtils.getConnection(factory);
}
boolean existingConnection = TransactionSynchronizationManager.hasResource(factory);
RedisConnection connToUse = preProcessConnection(conn, existingConnection);
boolean pipelineStatus = connToUse.isPipelined();
if (pipeline && !pipelineStatus) {
connToUse.openPipeline();
}
RedisConnection connToExpose = (exposeConnection ? connToUse : createRedisConnectionProxy(connToUse));
T result = action.doInRedis(connToExpose);
// close pipeline
if (pipeline && !pipelineStatus) {
connToUse.closePipeline();
}
// TODO: any other connection processing?
return postProcessResult(result, connToUse, existingConnection);
} finally {
RedisConnectionUtils.releaseConnection(conn, factory);
}
}
看了看我的 5 米長刀,再看看這個異常,虎軀一震:難不成真要讓我的大刀上場?!
幹!!!
天下大事,要幹成功,一般需要三步:
1. 千軍易找,一将難求!(打開終端,找到程式的 pid )
2. 招兵買馬、屯田生産(擷取應用堆棧資訊,指令為: jmap -dump,format=b,file=/home/hadoop/my-dump.hprof <pid> )
3. 拿下城池,弑帝稱王!(使用 Mat 分析定位、解決問題)
注: MAT 全稱為:Eclipse Memory Analyzer, 下載下傳位址為:http://www.eclipse.org/mat/ , 讀者下載下傳完後,可以考慮修改一下 MemoryAnalyzer.ini 檔案中 -Xmx 的大小( 如果你的 hprof 很大的話,會造成 OOM,導緻無法繼續分析 )
--------------------------------------------------------------------------------------------------------------------------
我的檔案打開後,如下圖,占用記憶體并不大。我們主要是分析連結洩漏。既然是洩漏,肯定就存在有記憶體不能釋放,并且可 DUMP 。
點選記憶體占用最高的餅圖位置,會出現如下圖示的菜單可選擇。選擇 "show objects by class " -> “ by incoming references ” !
随後會打開 "class references" 視窗, 果斷的在 ClassName 頂部的搜尋框中輸入 “com” ( 我們隻關注我們關注的内容),由于是連接配接池洩漏,是以其他的内容我們可以不用理會了哈,直接看 org.apache.commons.pool2.impl.GenericObjectPool 即可。
在該類上面右擊,選擇 “Java Basics” ——> "Open In Dominator Tree" ,打開 !
一步步的展開 org.apache.commons.pool2.impl.GenericObjectPool 我們可以看到如下圖。哈,,,大大的 hscan !
到這一步,我們已經很清晰了。 在 IDEA 中搜尋 hscan 相關的代碼。果然,找到了一些使用 scan 指令的地方,再細細端詳才發現, 罪魁禍首為:
Cursor<Map.Entry<String, Bean>> cursor = hashOperations.scan(scanOption);
cursor 使用完畢後,沒有看到調用 .close() 的地方。
果斷加上,再測一把!!!順利通過。
結論:
在平時的代碼中,一定要記得在使用連結、遊标、流等位置,記得關閉!否則會造成不可預料的問題。
問題順利解決,收起我的 5 米大刀!
端起我的碧螺春,輕抿一口!
窗外不知何時漂起了小雨!
轉載于:https://my.oschina.net/Rayn/blog/2032408