生产环境 Redis 连接,长时间无响应被服务器断开问题

2022-04-10 01:00:38 +08:00
 wayn111

上个月线上生产环境有几个接口出现异常响应,查看生产日志后发现,如下错误

线上 Redis 客户端使用的是SpringBoot默认的Lettuce客户端,并且没有指定连接池,connection reset by peer这个错误是当前客户端连接在不知情的情况下被服务端断开后产生,也就是说当前客户端 Redis 连接已经在服务端断开了,但是客户端并不知道,当请求进来时,Lettuce继续使用当前 Redis 连接请求数据时,就会提示connection reset by peer

一般情况下服务端断开连接都会发送FIN包通知客户端,但是当我在用tcpdump监控服务端 tcp 传输后,发现 Redis 服务端 tcp 连接在无活动一段时间,比如 10 分钟后会收到来自客户端的RST包,然而我的客户端也在使用 wireshark 抓包中,并没有发送给服务端RST包,这就很奇怪了,猜测这里是可能是服务器对 tcp 连接的限制导致,对长时间无活动的 tcp 连接强制断开处理。所以这里线上环境 Redis 连接偶尔产生connection reset by peer错误是被我复现出来了。

既然这里知道是 Redis 连接长时间无活动后被断开导致的 bug ,那怎么解决?

博主一开始以为重试可以解决,但是发现事情没有想象的简单。上代码

   // 查询 Redis
    public <T> T getCacheObject(final String key) {
        try {
            ValueOperations<String, T> operation = redisTemplate.opsForValue();
            return operation.get(key);
        } catch (Exception e) {
            log.error(e.getMessage(), e);
            return retryGetCacheObject(key, 3);
        }
    }
   // 重试查询 Redis
    public <T> T retryGetCacheObject(final String key, int retryCount) {
        try {
            log.info("retryGetCacheObject, key:{}, retryCount:{}", key, retryCount);
            if (retryCount <= 0) {
                return null;
            }
            Thread.sleep(200L);
            retryCount--;
            ValueOperations<String, T> operation = redisTemplate.opsForValue();
            return operation.get(key);
        } catch (Exception e) {
            log.error(e.getMessage(), e);
            return retryGetCacheObject(key, retryCount);
        }
    }

上面代码的意思是第一次查询 Redis 发生异常后,每隔 200 毫秒在查 3 次。当实际运行时,发现这里会提示三次connection reset by peer错误,一直没有取到新的 Redis 连接。

到这里这个问题的我的解决思路其实就是怎么在 Redis 连接发生异常后,怎么创建一条新的连接进行代替。

不多说直接上代码:

    // Lettuce 连接工厂
    @Autowired
    private LettuceConnectionFactory lettuceConnectionFactory;

    /**
     * 获得缓存的基本对象。
     *
     * @param key 缓存键值
     * @return 缓存键值对应的数据
     */
    public <T> T getCacheObject(final String key) {
        try {
            ValueOperations<String, T> operation = redisTemplate.opsForValue();
            return operation.get(key);
        } catch (Exception e) {
            log.error(e.getMessage(), e);
            return retryGetCacheObject(key, 1);
        }
    }

    public <T> T retryGetCacheObject(final String key, int retryCount) {
        try {
            log.info("retryGetCacheObject, key:{}, retryCount:{}", key, retryCount);
            if (retryCount <= 0) {
                return null;
            }
            lettuceConnectionFactory.resetConnection();
            Thread.sleep(200L);
            retryCount--;
            ValueOperations<String, T> operation = redisTemplate.opsForValue();
            return operation.get(key);
        } catch (Exception e) {
            log.error(e.getMessage(), e);
            return retryGetCacheObject(key, retryCount);
        }
    }

在用当前 Redis 连接获取数据发生异常超过timeout间隔后,抛出异常,进入重试方法,使用 lettuceConnectionFactory.resetConnection()方法进行连接重置,创建一条新的连接后,继续获取数据,从而正常响应客户端。lettuceConnectionFactory对象是对Lettuce无池化连接的工厂实现,提供了 lettuceConnectionFactory.getConnection(); lettuceConnectionFactory.initConnection(); lettuceConnectionFactory.resetConnection();等获取、初始化、重置连接的方法

配合springboot配置timeout将获取数据的超时时间设置为 2 秒,从而将接口请求耗时也控制在 2 秒左右

  redis:
    xx: xx
    timeout: 2000

到此生产环境这里SpringBoot项目下Lettuce客户端无池化连接偶尔断开的 bug 算是解决了

最后贴一下实战项目地址newbeemall,newbee-mall 商城的 mybatis plus 版本 实现了优惠卷领取, 支付宝沙箱支付,后台添加搜索,RedisSearch 分词检索

1739 次点击
所在节点    程序员
5 条回复
lianjun1991
2022-04-10 22:36:44 +08:00
是不是防火墙的问题,长时间未使用的连接,在下一次使用的时候被防火墙 reset 了
kchenzhi
2022-04-11 09:58:09 +08:00
一楼讲的不错,我曾经遇到过类似的问题,客户端和服务器都还保持这链接,但是由于过久没有报文,防火墙把端口回收了。可以抓一下规律,一般是固定几分钟后防火墙会释放端口,如果此时在 redis 服务器上用 netstat 查看链接信息仍然是 ESTABLISHED 的话,那说明服务端也还保持着这个连接。
解决方案有两个:
1 、增加防火墙的空闲端口回收时间。但是这样对网络影响较大,不太建议用。
2 、查找 lettuce 文档,看是否有心跳参数,把心跳间隔调整得比防火墙超时时间短。
3 、自己定时发送指令,实现心跳机制。
leeyuzhe
2022-04-11 13:34:54 +08:00
我之前遇到过类似的问题,应该不是防火墙的原因,因为我这边程序会一直使用 redis ,后来换成 jedis 不了了之
wayn111
2022-04-11 22:53:18 +08:00
@lianjun1991 是由这个可能,在无活动一段时间后,这个 tcp 连接收到了客户端的 rst 包,但是客户都安并没有发包
wayn111
2022-04-11 22:54:04 +08:00
@kchenzhi 自己定时发送指令,实现心跳机制
最后一步是使用这一招

这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。

https://www.v2ex.com/t/845978

V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。

V2EX is a community of developers, designers and creative people.

© 2021 V2EX