死磕生菜 -- lettuce 间歇性发生 RedisCommandTimeoutException 的深层原理及解决方案
0x00 起源
项目的一些微服务集成了 Spring Data Redis
,而底层的 Redis 客户端是 lettuce
,这也是默认的客户端。微服务在某些环境中运行很正常,但在另一些环境中运行就会间歇性的发生 RedisCommandTimeoutException
:有时长时间没人使用(当然也不操作 Redis 了),例如一个晚上没人操作系统,第二天早上使用时就会发生这个异常。而且发生该异常之后,访问 Redis 就会一直抛这个异常,但过了一段时间后,又正常了。或者立即重启微服务,也会正常了。
- lettuce 版本:5.3.0
- Redis 版本:官方 docker 镜像, 5.0,默认配置
- Spring boot 版本:2.1.x
经过日志排查(lettuce 的日志级别需要开启 DEBUG
或 TRACE
),发生RedisCommandTimeoutException
的原因时lettuce
的 Connection 已经断了,发生异常后大约 15 分钟,lettuce
的 ConnectionWatchdog
会进行自动重连。
那么为何 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源码【四】 这篇文档或者自行阅读 lettuce
中ConnectionWatchdog
的源码。
根据ConnectionWatchdog
重连的机制(收到netty
的ChannelInactived
事件后启动重连的线程不断进行连接)可以确定,连接是由 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 Boot
的auto-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 的深层原理及解决方案的更多相关文章
- 【死磕Java并发】-----深入分析volatile的实现原理
通过前面一章我们了解了synchronized是一个重量级的锁,虽然JVM对它做了很多优化,而下面介绍的volatile则是轻量级的synchronized.如果一个变量使用volatile,则它 ...
- 【死磕Java并发】-----深入分析synchronized的实现原理
记得刚刚開始学习Java的时候.一遇到多线程情况就是synchronized.相对于当时的我们来说synchronized是这么的奇妙而又强大,那个时候我们赋予它一个名字"同步". ...
- 【死磕Java并发】—–深入分析volatile的实现原理
通过前面一章我们了解了synchronized是一个重量级的锁,虽然JVM对它做了很多优化,而下面介绍的volatile则是轻量级的synchronized.如果一个变量使用volatile,则它比使 ...
- 【死磕Java并发】-----Java内存模型之happend-before
在上篇博客([死磕Java并发]-–深入分析volatile的实现原理)LZ提到过由于存在线程本地内存和主内存的原因,再加上重排序,会导致多线程环境下存在可见性的问题.那么我们正确使用同步.锁的情况下 ...
- 【死磕Java并发】----- 死磕 Java 并发精品合集
[死磕 Java 并发]系列是 LZ 在 2017 年写的第一个死磕系列,一直没有做一个合集,这篇博客则是将整个系列做一个概览. 先来一个总览图: [高清图,请关注"Java技术驿站&quo ...
- 【死磕Java并发】-----Java内存模型之happens-before
在上篇博客([死磕Java并发]-–深入分析volatile的实现原理)LZ提到过由于存在线程本地内存和主内存的原因,再加上重排序,会导致多线程环境下存在可见性的问题.那么我们正确使用同步.锁的情况下 ...
- 【死磕Java并发】-----内存模型之happens-before
在上篇博客([死磕Java并发]-----深入分析volatile的实现原理)LZ提到过由于存在线程本地内存和主内存的原因,再加上重排序,会导致多线程环境下存在可见性的问题.那么我们正确使用同步.锁的 ...
- 【死磕 Spring】----- IOC 之 加载 Bean
原文出自:http://cmsblogs.com 先看一段熟悉的代码: ClassPathResource resource = new ClassPathResource("bean.xm ...
- 死磕安卓前序:MVP架构探究之旅—基础篇
前言 了解相关更多技术,可参考<我就死磕安卓了,怎么了?>,接下来谈一谈我们来学习一下MVP的基本认识. 大家对MVC的架构模式再熟悉不过.今天我们就学习一下MVP架构模式. MVC和MV ...
随机推荐
- 信号量解决写者优先&读者优先&公平竞争(reader writer)
先说问题: 这里的rand都是伪随机.解决也很简单,srand即可.内容懒得改了~~ 描述及思路: 代码: 运行结果: 读者优先: 效果 ...
- codeforces 1013B 【思维+并查集建边】
题目链接:戳这里 转自:参考博客 题意:给一个n*m的矩阵,放入q个点,这q个点之间的关系是,若已知这样三个点(x1,y1),(x2,y1),(x1,y2),可以在(x2,y2)处生成一个新的点,对于 ...
- CodeForces - 803C Maximal GCD 【构造】
You are given positive integer number n. You should create such strictly increasing sequence of k po ...
- 关于虚继承的sizeof问题
首先关于虚继承和普通继承的知识,我总结一下: 1.普通继承时,无论派生类是否定义新的虚函数,基类和派生类总是共享一个虚函数表,不需要另加指向虚函数的指针,派生类只是将虚函数表中的元素改成了派生类的地址 ...
- Gym102361A Angle Beats(直角三角形 计算几何)题解
题意: \(n\)个点,\(q\)个询问,每次问包含询问点的直角三角形有几个 思路: 代码: #include<bits/stdc++.h> using namespace std; co ...
- POJ 2288 Islands and Bridges(状压DP)题解
题意:n个点,m有向边,w[i]表示i的价值,求价值最大的哈密顿图(只经过所有点一次).价值为:所有点的w之和,加上,每条边的价值 = w[i] * w[j],加上,如果连续的三个点相互连接的价值 = ...
- shiro<1.2.4反序列化分析
0x01.环境搭建 下载地址:https://codeload.github.com/apache/shiro/zip/shiro-root-1.2.4 环境:Tomcat 8.5.27 + idea ...
- config file language All In One
config file language All In One YAML YAML Ain't Markup Language .yaml / .yml https://yaml.org/ https ...
- TypeScript enum 枚举实现原理
TypeScript enum 枚举实现原理 反向映射 https://www.typescriptlang.org/docs/handbook/enums.html enum Direction { ...
- git笔记整理-learnGitBranching
声明 此篇文章内容是本人在 github上寻找到Peter Cottle的项目 https://github.com/pcottle/learnGitBranching.git 中学习git相关命令时 ...