天天看點

記一次 Redis 連接配接池洩漏問題排查    看到如此之問題,豈不是很蛋疼,立即跳起來,左手叉腰,右手指向天花闆。大喊一聲:拿我的 5 米長刀來。    幹!!!    結論:

    這一天風和日麗,我很榮幸的參加進入組織的活動,這個組織依然是一群悶騷的少年,熱火朝天的甩着膀子,寫着神聖的 Java 代碼,偌大的辦公室,隻能聽見噼裡啪啦的敲擊鍵盤聲!

    好騷氣的組織!!!

------------------------------------------------------------------------------------------------------------------------

    進入組織後,給我随手就撩過來一個 git 位址,習慣性的通過 git clone <git-url> ,将代碼 dang  下來!

    我的 IDEA 已經饑渴難耐,說時遲那時快。滑鼠飛快的飛到桌面的 

記一次 Redis 連接配接池洩漏問題排查    看到如此之問題,豈不是很蛋疼,立即跳起來,左手叉腰,右手指向天花闆。大喊一聲:拿我的 5 米長刀來。    幹!!!    結論:

 , 以盲狙的速度進行了輕按兩下!經過 10s 的長達等待。主界面終于展示在我的眼前。仿佛看到了我夢中的女神——怦然心動,無法形容我的處境。

    作為一個不會技術的二溜子,豈能就此作罷,掃視一眼後發現,原來是仰慕已久的 Web 項目基佬(這麼說主要是因為教育訓練機構 3 個月能生産一批,而且都是各行各業的男性同胞),瞬間心情大落!吾等倒要看看你是什麼妖孽,沒有妖孽也要給你制造一批出來。

    拉出我的湯姆貓( tomcat ), 将它迅速加載進來。緊接着一個飄逸的  “Shift + F10” (IDEA 的 快捷鍵)閃過。

記一次 Redis 連接配接池洩漏問題排查    看到如此之問題,豈不是很蛋疼,立即跳起來,左手叉腰,右手指向天花闆。大喊一聲:拿我的 5 米長刀來。    幹!!!    結論:

    一個 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 。

記一次 Redis 連接配接池洩漏問題排查    看到如此之問題,豈不是很蛋疼,立即跳起來,左手叉腰,右手指向天花闆。大喊一聲:拿我的 5 米長刀來。    幹!!!    結論:

    點選記憶體占用最高的餅圖位置,會出現如下圖示的菜單可選擇。選擇 "show objects by class " ->  “ by incoming references ” !

記一次 Redis 連接配接池洩漏問題排查    看到如此之問題,豈不是很蛋疼,立即跳起來,左手叉腰,右手指向天花闆。大喊一聲:拿我的 5 米長刀來。    幹!!!    結論:

    随後會打開  "class  references" 視窗, 果斷的在  ClassName 頂部的搜尋框中輸入 “com” ( 我們隻關注我們關注的内容),由于是連接配接池洩漏,是以其他的内容我們可以不用理會了哈,直接看 org.apache.commons.pool2.impl.GenericObjectPool  即可。

記一次 Redis 連接配接池洩漏問題排查    看到如此之問題,豈不是很蛋疼,立即跳起來,左手叉腰,右手指向天花闆。大喊一聲:拿我的 5 米長刀來。    幹!!!    結論:

在該類上面右擊,選擇  “Java Basics” ——> "Open In Dominator Tree" ,打開 !

記一次 Redis 連接配接池洩漏問題排查    看到如此之問題,豈不是很蛋疼,立即跳起來,左手叉腰,右手指向天花闆。大喊一聲:拿我的 5 米長刀來。    幹!!!    結論:

    一步步的展開  org.apache.commons.pool2.impl.GenericObjectPool 我們可以看到如下圖。哈,,,大大的   hscan !

記一次 Redis 連接配接池洩漏問題排查    看到如此之問題,豈不是很蛋疼,立即跳起來,左手叉腰,右手指向天花闆。大喊一聲:拿我的 5 米長刀來。    幹!!!    結論:

        到這一步,我們已經很清晰了。 在 IDEA 中搜尋  hscan 相關的代碼。果然,找到了一些使用 scan 指令的地方,再細細端詳才發現, 罪魁禍首為: 

Cursor<Map.Entry<String, Bean>> cursor = hashOperations.scan(scanOption);      

 cursor 使用完畢後,沒有看到調用  .close() 的地方。

    果斷加上,再測一把!!!順利通過。

    結論:

        在平時的代碼中,一定要記得在使用連結、遊标、流等位置,記得關閉!否則會造成不可預料的問題。

        問題順利解決,收起我的 5 米大刀!

        端起我的碧螺春,輕抿一口!

        窗外不知何時漂起了小雨!

轉載于:https://my.oschina.net/Rayn/blog/2032408