Redis Lettuce长时间超时问题

1. 背景

新上线了一个服务,在压测的时候大量返回错误,查看报错是io.lettuce.core.RedisCommandTimeoutException: Command timed out after 1 minute(s)

  1. 在系统长时间无请求之后会必现
  2. 出现之后在十几分钟内不会自动重连

对于刚上线的系统,是很有可能出现上述情况的,例如灰度期间,凌晨无人访问的时候会出现,而且会严重影响线上服务

2. 分析

通过现象可以看出,我们的应用应该是拿一个已经断开的Redis连接,所以一直会超时,而且Lettuce也没有去自动重连

3. 调研

通过网上查询,发现这个是个普遍的现象,并提供了解决方法:

  1. 使用Jedis替换Lettuce
  2. 写定时任务不断请求Redis

使用Jedis算作是绕开了这个问题,写定时任务一是增加应用代码,二是会增加了Redis的请求

那么SpringBoot默认指定的Redis连接池Lettuce为什么会有这种问题呢?

3.1 官方Github

3.1.1 FAQ

Lettuce Github FAQ中发现了这个问题,官方提出几个可能的原因:

  1. Redis服务崩了或者网络问题,并且在指定时间未恢复
  2. 命令没有在超时时间内完成
  3. 配置的超时时间和Redis性能不匹配
  4. 阻塞了EventLoop, 例如在RedisFuture的回调方法中
  5. 手动控制setAutoFlushCommands(true/false),但没有flushCommands()

说实话,官方这个回答和我们碰见的问题是完全不沾边,没有请求更别说性能问题了

3.1.2 Issues

看到一个阿里员工提的issue,指出了可能发生这种问题的原因

Lettuce connects to a Redis host and reads and writes normally. However, if the host fails (the hardware problem directly causes the shutdown, and there is no RST reply to the client at this time), the client will continue to time out until the tcp retransmission ends, and it can be recovered. At this time, it takes about 925.6 s in Linux ( Refer to tcp_retries2 ).

指出因为硬件原因,未返回RST到Lettuce客户端,就会导致客户端在925.6秒(根据tcp_retries2)之内使用一个断开的连接,和我们的情况完全一致

会在下述情况出现:

  1. 硬件问题或断电导致的Redis Server宕机
  2. SLB负载均衡,后端地址变了的时候

也指出了这就是阿里云不推荐使用Lettuce,而使用Jedis的原因

不过遗憾的是,这个issue还处于open状态,还没有解决方法

4. 解决

在网上看到一些同样有好奇心的同学试图解决这个问题,死磕生菜 -- lettuce 间歇性发生 RedisCommandTimeoutException 的深层原理及解决方案

这个同学提供了三种解决方案

  1. 设置 Linux 的 TCP_RETRIES2 参数
  2. 设置 Socket Option 的 TCP_USER_TIMEOUT 参数
  3. 定制 lettuce:增加心跳机制

因为第一种会影响全局,所以没有试验,通过尝试了下面两种,发现都不生效,因为对Netty不是特别熟悉,也没有继续深究

最后怎么解决的,使用定时任务ping Redis Server成功解决

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.jetbrains.annotations.NotNull;
import org.springframework.context.annotation.Configuration;
import org.springframework.dao.DataAccessException;
import org.springframework.data.redis.connection.RedisConnection;
import org.springframework.data.redis.core.RedisCallback;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.scheduling.annotation.Scheduled;

import javax.annotation.Resource;

@Configuration
@EnableScheduling
public class RedisScheduleTask {

    public static final Log log = LogFactory.getLog(RedisScheduleTask.class);

    @Resource
    private StringRedisTemplate dupShowMasterRedisTemplate;

    // 1 minutes
    @Scheduled(fixedRate = 60000)
    private void configureTasks() {
        log.debug("ping redis");
        dupShowMasterRedisTemplate.execute(new RedisCallback<String>() {
            @Override
            public String doInRedis(@NotNull RedisConnection connection) throws DataAccessException {
                return connection.ping();
            }
        });
    }

}

参考

[1] Add support for disconnect on timeout to recover early from no RST packet failures
[2] 死磕生菜 -- lettuce 间歇性发生 RedisCommandTimeoutException 的深层原理及解决方案
[3] Lettuce Github FAQ

posted @ 2022-10-20 14:10  songtianer  阅读(7755)  评论(0编辑  收藏  举报