性能优化-线上redis连接过多问题排查

8,092 阅读4分钟

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相关用法

zshell.cc/2017/09/24/…