1.问题描述
运维大佬反应redis客户端连接数太多了,已超过了默认的最大限制数字1w。在redis服务器端执行命令./redis-cli -h host -p port info clients, 可看到这里有大量redis客户端连接存活,但大部分连接都是空闲状态。
登陆了一个执行定时任务服务实例看下,这个服务实例的基本是没有流量,但是依然有40个redis连接,可以肯定我们的一些连接是一开始创建并保存到现在的。由于我们用的是统一redis组件和配置,只是在个别服务上增加了特殊的配置参数。目前整个服务领域大概有个一百多个docker容器实例,这就是意味着有几千的空闲连接, 所以空闲连接多了,对服务器的性能肯定造成了影响。由于终端输出有问题,会导致线程信息丢失,建议输出到文件中再分析。
jstack pid > stack.log
cat stack.log | grep redis/reidsson/jedis
这里可以看到有大量redisson连接线程信息,大概可以推测出是redisson客户端保持着这些连接,这只是推测,具体还要看线上的实际环境。
2.分析问题
我们封装了redis客户端组件,其中使用了多种redis客户端(jedis/redisson/redisTemplate),如果我们能直接查找到这里连接对象就非常容易定位问题了。 为了在线上环境更快地找出问题,推荐arthas工具使用ognl表达式可查看实例的成员变量。sc和orgnl需要配合起来,使用例子如下所示:
sc -d org.apache.dubbo.config.spring.extension.SpringExtensionFactor
拿到对应的classLoader实例后执行ognl可以看到成员变量名
ognl -c 18b4aac2 '#context=@org.apache.dubbo.config.spring.extension.SpringExtensionFactory@CONTEXTS.iterator.next, #context.getBean("redisson").getConnectionManager()'
2.1 jedis连接
jedis的连接管理较简单,框架使用的是JedisPool(底层使用的是apache的GenericObjectPool) 管理连接,一般连接的使用流程是这样的:
JedisPool(GenericObjectPool) JedisFactory
-> Jedis
-> BinaryJedis
-> Client
-> socket
所以我们拿到jedis实例对象的GenericObjectPool对象池就可以获取到总的连接情况信息。看到连接池(对象池) 里面只有10个对象,所以这三十多个连接只能是redisson客户端的了。
ognl -c 18b4aac2 '#context=@org.apache.dubbo.config.spring.extension.SpringExtensionFactory@CONTEXTS.iterator.next, #context.getBean("jedisPool").internalPool'
2.2 redisson连接
相对于简单的jedis框架, redisson框架是基于基于NIO的Netty通信框架上,提供了一系列的分布式事务操作与数据结构。redisson的框架源码是基于AIO编程模型来实现的,阅读起来还是有些难度,整个框架代码量比较多,我们这里只关注与redis连接相关的类信息。
其中redisson的网络连接管理都是通过ConnectionManager实现的,其中Map<RedisClient, MasterSlaveEntry> client2entry存储连接池信息,IdleConnectionWatcher connectionWatcher 启动了一个定时任务来扫描并删除无用或空闲连接,所以我们获取两者中的一种就可以看到当前客户端的所有连接情况。我们可通过执行arthas的命令拿到client2entry列表来看下,就能知道当前连接信息。执行结果如下所示,这些连接都是redisson创建的。
ognl -c 18b4aac2 '#context=@org.apache.dubbo.config.spring.extension.SpringExtensionFactory@CONTEXTS.iterator.next, #context.getBean("redisson").getConnectionManager().getEntrySet().iterator().next().masterEntry.allConnections'
这个redisson客户端使用的是SingleServer模式,连接的初始化调用过程如下所示,
MasterSlaveConnectionManager#initSingleEntry()
->MasterSlaveEntry#setupMasterEntry
->MasterConnectionPool#add()
-> ConnectionPool#add()
-> ConnectionPool#initConnection()
-> ConnectionPool#createConnection()
ConnectionPool#createConnection()中进行了初始化连接操作, 这里minimumIdleSize对应的就是暴露出来的connectionMinimumIdleSize属性, 我们如果将这个配置属性变小则控制了连接的初始化,这里框架的连接池默认启动就有32个连ConnectionPool#createConnection()中进行了初始化连接操作,这里minimumIdleSize对应的就是暴露出来的connectionMinimumIdleSize属性, 我们如果将这个配置属性变小则控制了连接的初始化,这里框架的连接池默认启动就有32个连接。
private void createConnection(boolean checkFreezed, AtomicInteger requests, ClientConnectionsEntry entry, RPromise initPromise,
int minimumIdleSize, AtomicInteger initializedConnections) {
.....
acquireConnection(entry, new Runnable() {
@Override
public void run() {
RPromise promise = new RedissonPromise();
//这里调用了RedisClient#connectAsyncI()异步连接redis服务器
createConnection(entry, promise);
promise.onComplete((conn, e) -> {
....
//获取剩下需要初始化的连接数,可以看到minimumIdleSize在这里起到了作用
int totalInitializedConnections = minimumIdleSize - initializedConnections.get();
....
int value = initializedConnections.decrementAndGet();
if (value == 0) {
//已初始化完成连接
} else if (value > 0 && !initPromise.isDone()) {
if (requests.incrementAndGet() <= minimumIdleSize) {
//继续调用创建连接
createConnection(checkFreezed, requests, entry, initPromise, minimumIdleSize, initializedConnections);
}
}
});
}
});
3.解决方案
为了减少空闲连接对redis服务器资源的占用,在组件中增加了Redisson中的connectionMinimumIdleSize和connectionPoolSize两个属性配置逻辑,再次上线执行后,配置生效,空闲连接数回到正常水平。
4.总结
在项目中,在这里主要是使用了jstack与arthas工具(ognl工具真的非常香),逐步定位分析出redisl连接过多的问题的原因。在这过程中也去了解了jedis和redisson的相关实现原理,收获还是不小的。经常使用到的客户端如redis/mysql/dubbo/zookeeper等等,连接配置参数其实是非常有讲究的,需要根据不同的运行环境与性能要求进行合理的配置。
参考资料
jstack相关用法