天天看点

记一次 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