Redis配置踩坑

1,202 阅读3分钟
  • 踩坑点: 压测过程出现获取redis链接数慢的情况
  • 踩坑原因: 对应redis的配置不熟悉

还原背景

1、压测过程中当TPS达到一定位置时,开始上不去了,RT稳定在某一个值上下,核心服务CPU高,可以肯定的是存在某些方法执行慢,且RDS(show processlist未发现慢sql),采用 Arthas 发现部分操作 Redis 的方法执行慢. 具体在 RedisTemplate.opsForValue.get 执行慢,进入里边一步一步的 trace ,最终定位在获取 链接慢 , 有时也存在执行慢的情况. 但大体上是获取 链接慢 (是经过许久才发现的).

2、当时的环境

  • ECS Load开始飙高
  • 由于压测产生的日志较多,IO也产生了影响.
  • netstat 发现链接 Redis 服务器的 TIME_WAIT 也比较多.

3、当时的我的行动是 :( 手足无措 **

  • 由于第一时间是核心服务的CPU飙升,打印堆栈发现是 Jedis 的读取线程 CPU 特别高,后续直接将 jedis 替换成 Lettuce , 线程CPU下滑. (服务本身也是非常吃CPU资源的)
  • 怀疑是不是 Redis 抗不住了,查看 Redis 的监控是此时的 QPS 达到2W+ , 进出口流量较高 (但是临时提升到48M并不是这个问题),压测导致内存基本上打满了(也不是这个问题),链接数虽然形成了相对飙升,但是相对最大链接1w还是小case,并且使用 DMS 查询 redis 的数据的时候是非常快

** 应对方案

  • 因为核心链路日志(使用logback)已经接入到ELK,但是还存在部分 log4j 实现的日志即未注释也没有处理,所以直接把这部分日志干掉. load有好转,但是RT依旧下不来.
  • Jedis 替换成 Lettuce 
  • ... 无

4、源代码找问题. 当时的配置

	<bean id="fff" class="redis.clients.jedis.JedisPoolConfig">
	     <property name="maxIdle" value="8"/>
	     <property name="maxTotal" value="600"/>
	     <property name="maxWaitMillis" value="${redis.maxWaitMillis}"/>
	     <property name="testOnBorrow" value="${redis.testOnBorrow}"/>
	</bean>

4.1、拷贝本地启动执行

使用 ab 进行压测

ab -r -n 1000 -c 调节这里的数字 -t 100 -s 30 'http://localhost:8082/redis'

使用 netstat 查看链接情况

 netstat -ant |grep 6379|grep  

根据 并发数设置的不同 产生的情况也是不一样的

  • 当并发数设置在5的时候,基本上没有 TIME_WAIT 
  • 当并发数设置在100的时候, TIME_WAIT 疯长

最后的源代码显示确实是在并发上去以后出现问题的,但是核心还是配置理解不到位. image.png org.springframework.data.redis.core.RedisTemplate#execute 

	@Nullable
	public <T> T execute(RedisCallback<T> action, boolean exposeConnection, boolean pipeline) {
		//其他变量初始化
		try {
            //...获取链接执行请求
			return postProcessResult(result, connToUse, existingConnection);
		} finally {
            //释放链接(问题在这里)
			RedisConnectionUtils.releaseConnection(conn, factory, enableTransactionSupport);
		}
	}
//	-------------我是漂亮的分割线-------------- RedisConnectionUtils
	public static void releaseConnection(@Nullable RedisConnection conn, RedisConnectionFactory factory,
			boolean transactionSupport) {

		if (isConnectionTransactional(conn, factory)) {
			//事务消息去掉
		} else {
			//这里的日志排查问题可以用到,具体使用方法不介绍
			if (log.isDebugEnabled()) {
				log.debug("Closing Redis Connection.");
			}
			conn.close();//关闭内幕
		}
	}
//	-------------我是漂亮的分割线--------------LettuceConnection
	@Override
	public void close() throws DataAccessException {
		//...省略
		if (asyncDedicatedConn != null) {
			connectionProvider.release(asyncDedicatedConn);
		}
		//...省略
	}
//	-------------我是漂亮的分割线--------------LettucePoolingConnectionProvider
	default void release(StatefulConnection<?, ?> connection) {
        //...省略
		pool.returnObject(connection);
	}
//  -------------我是漂亮的分割线--------------GenericObjectPool#归还数据到连接池
    @Override
    public void returnObject(final T obj) {
        //...省略
        final int maxIdleSave = getMaxIdle(); //获取maxIdle的配置 8
        //idleObjects.size() 最大是200 但是肯定超过8,所以被destroy了
        if (isClosed() || maxIdleSave > -1 && maxIdleSave <= idleObjects.size()) {
            try {
                destroy(p);//socket.close
            } catch (final Exception e) {
                swallowException(e);
            }
        } else {
	        //...省略
        }
        updateStatsReturn(activeTime);
    }

到这异步基本上真相大白了,当然也可以回头看看链接是如何创建的,看完后发现我们的配置其实是不合理的(当然跟本次的问题无关)

这里的本质问题还是链接数配置的太低了,基本上生效的不会太多. 导致频繁的执行TCP的握手和挥手去建立和销毁链接

最好的配置还是需要根据运行的环境来进行配置. 如果 TPS高,那就多配一点,maxIdle配置的多一点,maxTotal最大配置个200(tomcat的最大线程数)就够了,如果链接数浪费的起,那么maxIdle和maxTotal可以配置成200,minIdle配置的小一点,等等定时任务回收就可以了。

最终分析得出来的原因是,链接数最终生效的只有8条,导致TPS高的时候大家都在等待Redis的链接,所以我们看到的问题就是获取链接数慢... 进而体现在TPS上不去。

本地排查过程中遇到的问题(需要并发才能确定,但是并发会导致日志飙升)

  • 日志太多了如何排查/流量太多了wireshark抓包无用(如果我想抓一个特定的URL/package该怎么去做)
  • idle的含义不同,想当然的代入某个含义

lettuce 配置问题

  • 默认是共享链接(只有一条TCP链接建立),在TPS高的时候是不够的
    • tomcat# <property name="shareNativeConnection" value="false"/> 
    • SpringBoot
    @Autowired //这种操作还是第一次看到 :)
    public void setLettuceConnectionFactory(LettuceConnectionFactory lettuceConnectionFactory){
        lettuceConnectionFactory.setShareNativeConnection(false);
    }
  • 链接回收的配置问题