0x00 起源

项目的一些微服务集成了 Spring Data Redis,而底层的 Redis 客户端是 lettuce,这也是默认的客户端。微服务在某些环境中运行很正常,但在另一些环境中运行就会间歇性的发生 RedisCommandTimeoutException:有时长时间没人使用(当然也不操作 Redis 了),例如一个晚上没人操作系统,第二天早上使用时就会发生这个异常。而且发生该异常之后,访问 Redis 就会一直抛这个异常,但过了一段时间后,又正常了。或者立即重启微服务,也会正常了。

  • lettuce 版本:5.3.0
  • Redis 版本:官方 docker 镜像, 5.0,默认配置
  • Spring boot 版本:2.1.x

经过日志排查(lettuce 的日志级别需要开启 DEBUGTRACE),发生RedisCommandTimeoutException 的原因时lettuce的 Connection 已经断了,发生异常后大约 15 分钟,lettuceConnectionWatchdog会进行自动重连。

那么为何 lettuce 的 Connection 为什么会断呢?而 ConnectionWatchdog为什么没有立即重连呢?又怎么解决这些问题呢?这些问题如果不弄清楚不解决,会严重影响系统的可用性,总不能让用户等十几分钟再用吧,也不能总重启应用吧。

网上也搜到了类似的问题,看来还是挺多人遇到相同的问题的。但大部分都说清楚这个现象的原因,也没说真正的解决方法。网上几乎全部的解决方法都是将lettuce换成了 jedis,回避了这个问题。

0x01 本质

换成jedis固然可以解决问题,但既然 lettuce能成为Spring默认的客户端,还是有先进的地方的。而且遇到问题不搞清楚,心里也痒痒的。下面会阐述这些问题的来龙去脉。

1.1 为什么 Redis 连接会断

其实这个问题并不是很重要,因为Socket连接断已经是事实,而且在分布式环境中,网络分区是必然的。在网络环境,Redis 服务器主动断掉连接是很正常的,lettuce 的作者也提及 lettuce 一天发生一两次重连是很正常的。

那么哪些情况会导致连接断呢:

  • Linux 内核的 keepalive 功能可能会一直收不到客户端的回应;
  • 收到与该连接相关的 ICMP 错误信息;
  • 其他网络链路问题等等;

如果要需要真正查明原因,需要 tcp dump 进行抓包,但意义不大,除非断线的概率大,像一天一两次或者几天才一次不必花这么大力气去查这个。而最主要的问题是 lettuce 客户端能否及时检测到连接已断,并尽快重连。

1.2 为何 lettuce 没有立刻重连

lettuce的重连机制这里进行赘述,有兴趣的同学可以参考 Redis客户端Lettuce源码【四】 这篇文档或者自行阅读 lettuceConnectionWatchdog的源码。

根据ConnectionWatchdog重连的机制(收到nettyChannelInactived事件后启动重连的线程不断进行连接)可以确定,连接是由 Redis 服务端断开的,因为如果是客户端主动断开连接,那么一定能收到ChannelInactived,因此,之所以lettuce要等 15 分钟后才重连,是因为没收到ChanelInactived事件。

那么为什么客户端没有到ChannelInactived事件呢?很多情况都会,例如:

  • 客户端没收到服务端 FIN 包;
  • 网络链路断了,例如拔网线,断电等等;

在我们这个情况,应该是没收到服务端的 FIN 包。

好了,我们再来看另一个问题:日志显示发生RedisCommandTimeoutException后,15 分钟后收到ChannelInactived事件。那么,为什么会大约是 15 分钟而不是别的时间呢?

其实,这是与 Linux 底层Socket的实现有关--这就是超时重传机制。也就是/proc/sys/net/ipv4/tcp_retries2参数,关于重传机制,可以看这篇文章:

Linux TCP_RTO_MIN, TCP_RTO_MAX and the tcp_retries2 sysctl

根据重传机制,发生RedisCommandTimeoutException的命令会重传 tcp_retries2这么多次,刚刚好是 15 分钟左右。

小结:

问题的原因已经清楚了,这里需要对 lettuce的重连机制、netty的工作原理、Linux socket实现原理有一定的了解。既然问题的原因找到了,如何解决呢?显然无论是网上说的替换Jedis客户端,还是重启应用、还是等 15 分钟,都不是好办法。

0x02 解决方案

既然找到了问题原因所在,那么可以根据这些原因来解决。主要有三种解决的方案:

2.1 设置 Linux 的 TCP_RETRIES2 参数

针对等待 15 分钟,那么就可以猜想是不是可以设置 Linux 的 TCP_RETRIES2 参数小点来缩短等待时间呢?答案是肯定的;这个参数 Linux 的默认值是 15,而有些应用(如 Oracle)要求设置为 3。

其实,一般情况下,tcp数据包超时了,重发 3 次都不成功,重发再多几次也是枉然的。

但是这个方案有个缺点:

如果修改了这个参数,也会影响到其他应用,因为这个是全局的参数。那么能否单独针对某个应用程序设置 Socket Option呢?很遗憾的是,笔者在 netty里并没找到该选项的设置,无论是EpollChannelOption 还是 JDK 的ExtendedSocketOptions

所幸的是:

netty提供另一个参数的设置:TCP_USER_TIMEOUT,这个参数就是为了针对单独设置某个应用程序的超时重传的设置。下面一小节讲述如何使用。

2.2 设置 Socket Option 的 TCP_USER_TIMEOUT 参数

Spring Bootauto-configuration中,ClientResources的初始化是默认的 ClientResources,因此,我们可以自定义一个 ClientResources

@Bean
public ClientResources clientResources(){
return ClientResources clientResources = ClientResources.builder()
.nettyCustomizer(new NettyCustomizer() { @Override
public void afterBootstrapInitialized(Bootstrap bootstrap) {
bootstrap.option(EpollChannelOption.TCP_USER_TIMEOUT, 10);
}
})
.build();
}

2.3 定制 lettuce:增加心跳机制

上面两个方案,缩短了等待的时长,都是依赖操作系统底层的通知。如果不想依赖底层操作系统的通知,唯一的办法就是自己在应用层增加心跳机制。

如上述的方案,lettuce提供了NettyCustomizer进行扩展,熟悉netty的同学,应该听说过netty所提供的心跳机制--IdleStateHandler,结合这两者,就很容易在初始化netty时增加心跳机制:


@Bean
public ClientResources clientResources(){ NettyCustomizer nettyCustomizer = new NettyCustomizer() { @Override
public void afterChannelInitialized(Channel channel) {
channel.pipeline().addLast(
new IdleStateHandler(readerIdleTimeSeconds, writerIdleTimeSeconds, allIdleTimeSeconds));
channel.pipeline().addLast(new ChannelDuplexHandler() {
@Override
public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
if (evt instanceof IdleStateEvent) {
ctx.disconnect();
}
}
});
} @Override
public void afterBootstrapInitialized(Bootstrap bootstrap) {
} }; return ClientResources.builder().nettyCustomizer(nettyCustomizer ).build();
}

这里由客户端自己做心跳检测,一旦发现Channel死了,主动关闭ctx.close(),那么ChannelInactived事件一定会被触发了。但是这个方案有个缺点,增加了客户端的压力。

0x03 总结

lettuce是一个优秀的开源软件,设计和代码都很优美。通过这次的问题排查和解决问题,加深了自己对netty,Linux Socket机制、TCP/IP 协议的理解。

0x04 参考

4.1 Redis客户端Lettuce源码【三】

4.2 Redis客户端Lettuce源码【四】

4.3 Linux TCP_RTO_MIN, TCP_RTO_MAX and the tcp_retries2 sysctl

4.4 https://github.com/lettuce-io/lettuce-core/issues/762

死磕生菜 -- lettuce 间歇性发生 RedisCommandTimeoutException 的深层原理及解决方案的更多相关文章

  1. 【死磕Java并发】-----深入分析volatile的实现原理

      通过前面一章我们了解了synchronized是一个重量级的锁,虽然JVM对它做了很多优化,而下面介绍的volatile则是轻量级的synchronized.如果一个变量使用volatile,则它 ...

  2. 【死磕Java并发】-----深入分析synchronized的实现原理

    记得刚刚開始学习Java的时候.一遇到多线程情况就是synchronized.相对于当时的我们来说synchronized是这么的奇妙而又强大,那个时候我们赋予它一个名字"同步". ...

  3. 【死磕Java并发】—–深入分析volatile的实现原理

    通过前面一章我们了解了synchronized是一个重量级的锁,虽然JVM对它做了很多优化,而下面介绍的volatile则是轻量级的synchronized.如果一个变量使用volatile,则它比使 ...

  4. 【死磕Java并发】-----Java内存模型之happend-before

    在上篇博客([死磕Java并发]-–深入分析volatile的实现原理)LZ提到过由于存在线程本地内存和主内存的原因,再加上重排序,会导致多线程环境下存在可见性的问题.那么我们正确使用同步.锁的情况下 ...

  5. 【死磕Java并发】----- 死磕 Java 并发精品合集

    [死磕 Java 并发]系列是 LZ 在 2017 年写的第一个死磕系列,一直没有做一个合集,这篇博客则是将整个系列做一个概览. 先来一个总览图: [高清图,请关注"Java技术驿站&quo ...

  6. 【死磕Java并发】-----Java内存模型之happens-before

    在上篇博客([死磕Java并发]-–深入分析volatile的实现原理)LZ提到过由于存在线程本地内存和主内存的原因,再加上重排序,会导致多线程环境下存在可见性的问题.那么我们正确使用同步.锁的情况下 ...

  7. 【死磕Java并发】-----内存模型之happens-before

    在上篇博客([死磕Java并发]-----深入分析volatile的实现原理)LZ提到过由于存在线程本地内存和主内存的原因,再加上重排序,会导致多线程环境下存在可见性的问题.那么我们正确使用同步.锁的 ...

  8. 【死磕 Spring】----- IOC 之 加载 Bean

    原文出自:http://cmsblogs.com 先看一段熟悉的代码: ClassPathResource resource = new ClassPathResource("bean.xm ...

  9. 死磕安卓前序:MVP架构探究之旅—基础篇

    前言 了解相关更多技术,可参考<我就死磕安卓了,怎么了?>,接下来谈一谈我们来学习一下MVP的基本认识. 大家对MVC的架构模式再熟悉不过.今天我们就学习一下MVP架构模式. MVC和MV ...

随机推荐

  1. github 无法访问

    描述: 1. ping 丢失 100% 2. git 失败 Failed to connect to github.com port 443: Timed out 3.打开网站 超时 解决: http ...

  2. PicGo:搭建图床

    PicGo:搭建图床 PicGo 免费搭建个人图床工具PicGo: 支持Windows.MacOS 和 Linux 软件目前覆盖的图床有8个平台: SM.MS图床.腾讯云COS.GitHub图床.七牛 ...

  3. JVM 报 GC Overhead limit exceeded 是什么意思?

    默认情况下,并不是等堆内存耗尽,才会报 OutOfMemoryError,而是如果 JVM 觉得 GC 效率不高,也会报这个错误. 那么怎么评价 GC 效率不高呢?来看下源码: 呢?来看下源码gcOv ...

  4. 【哈希表】leetcode454——四数相加II

    编号454:四数相加II 给定四个包含整数的数组列表 A , B , C , D ,计算有多少个元组 (i, j, k, l) ,使得 A[i] + B[j] + C[k] + D[l] = 0. 为 ...

  5. Python对excel的基本操作

    Python对excel的基本操作 目录 1. 前言 2. 实验环境 3. 基本操作 3.1 安装openpyxl第三方库 3.2 新建工作簿 3.2.1 新创建工作簿 3.2.2 缺省工作表 3.2 ...

  6. PerformanceObserver API All In One

    PerformanceObserver API All In One 性能监控 https://developer.mozilla.org/en-US/docs/Web/API/Performance ...

  7. Async Programming All in One

    Async Programming All in One Async & Await Frontend (async () => { const url = "https:// ...

  8. CORS OPTIONS

    CORS OPTIONS A CORS preflight request is a CORS request that checks to see if the CORS protocol is u ...

  9. USDN代币的特点

    USDN是NGK公链发行的算法型稳定币,采用智能合约发行,通过智能合约的透明化,能够让市场USND持有者获得算法稳定的背书.USDN是一种锚定全球通用的代币,更是连接全球数字经济的通用数字代币.USD ...

  10. Android 比较好看的注册登录界面

    各位看官姥爷: 对于一款android手机app而言,美观的界面使得用户有好的使用体验,而一款好看的注册登录界面也会给用户好的用户体验,那么话不多说,直接上代码 首先是一款简单的界面展示 1.登陆界面 ...