网络问题
现象 Redis Java客户端报错超时或者获取不到连接
connection time out
,或者 Connection reset by peer
redis.clients.jedis.exceptions.JedisConnectionException: Could not get a resource from the pool Caused by: redis.clients.jedis.exceptions.JedisConnectionException: java.net.ConnectException: Connection timed out (Connection timed out)
Caused by: io.lettuce.core.RedisCommandTimeoutException: Command timed out after 6 second(s) at io.lettuce.core.ExceptionFactory.createTimeoutException(ExceptionFactory.java:51) at io.lettuce.core.LettuceFutures.awaitOrCancel(LettuceFutures.java:114) at io.lettuce.core.cluster.ClusterFutureSyncInvocationHandler.handleInvocation(ClusterFutureSyncInvocationHandler.java:123) at io.lettuce.core.internal.AbstractInvocationHandler.invoke(AbstractInvocationHandler.java:80) at com.sun.proxy.$Proxy134.mget(Unknown Source) at org.springframework.data.redis.connection.lettuce.LettuceStringCommands.mGet(LettuceStringCommands.java:119) ... 15 common frames omitted
原因
除了防火墙,宕机,重启等原因,主要有下列
- 并发连接超过了最大连接数
- 数据长度不一致
- Timeout太短
- 没有设置KeepAlive
- 收到通信对端的
TCP RST
信号 - 对端的Socket被关闭,发送数据端发送的第一个数据包引发该异常
解决这一类问题的思路
(1)慢查询阻塞:连接池连接都被hang住
(2)资源池参数不合理:比如QPS高,连接池小
(3)连接泄露(没有close)
(4)将spring redis设置的超时时间 比redis.conf中的timeout
要小
比如spring redis中设置超时30秒,那么redis服务器中设置为40秒;
另外设置redis.conf中的tcp-keepalive
:
1.1 Redis服务器超时参数
1.1.1 timeout参数
timeout
参数值的单位为秒(s),默认值为0,表示无限制,取值范围为0~100000
查看空闲超时时间
127.0.0.1:1000> config get timeout
0 //默认,表示关闭该功能
设置超时时间
config set timeout 60
或者在
redis.conf
配置文件中添加重启生效。
1.1.2 tcp-keepalive
TCP KeepAlive
linux保活定时器相关参数:
-
tcp_keepalive_time
,在TCP保活打开的情况下,最后一次数据交换到TCP发送第一个保活探测包的间隔,即允许的持续空闲时长,或者说每次正常发送心跳的周期,默认值为7200s(2h)。 -
tcp_keepalive_probes
在tcp_keepalive_time
之后,没有接收到对方确认,继续发送保活探测包次数,默认值为9(次) -
tcp_keepalive_intvl
,在tcp_keepalive_time
之后,没有接收到对方确认,继续发送保活探测包的发送频率,默认值为75s。
>sysctl -a | grep keepalive
net.ipv4.tcp_keepalive_intvl = 75
net.ipv4.tcp_keepalive_probes = 9
net.ipv4.tcp_keepalive_time = 7200
redis的tcp-keepalive
覆盖了linux
的tcp_keepalive_time
。建议值为60秒。
127.0.0.1:6379> config get tcp-keepalive
1) "tcp-keepalive"
2) "6"
1.2 连接数
查看最大连接数
127.0.0.1:6379> CONFIG GET maxclients
1) "maxclients"
2) "10000"
修改最大连接数
127.0.0.1:6379> CONFIG set maxclients 10
OK
查看当前连接数
127.0.0.1:6379> info clients
#Clients
connected_clients:621
client_longest_output_list:0
client_biggest_input_buf:0
blocked_clients:0
127.0.0.1:6379>
或者
sudo netstat -antp | grep 6379 | wc -l
操作系统限制
一个客户端连接对应着一个TCP连接,在LINUX系统对应着一个文件句柄,系统级别连接句柄用完了,也就无法再进行连接了。
查看系统限制:
ulimit -n
全连接队列大小限制
对Redis服务器
TCP全连接队列大小 = min(tcp-backlog,somaxconn)
- linux查看已完成连接队列的长度
$ /proc/sys/net/core/somaxconn
- redis配置文件查看tcp_backlog大小
tcp_backlog //默认511
- 查看全连接队列溢出
// 队列溢出是查看现有来结束这是否大于队列大小, 如果大于就丢弃,overflowed + 1
netstat -s | grep overflowed
1.3 JAVA客户端连接池
Lettuce
和 Jedis
的都是连接Redis Server
的客户端程序。Jedis
在实现上是直连redis server
,多线程环境下非线程安全,除非使用连接池,为每个Jedis实例增加物理连接。Lettuce
基于Netty的连接实例(StatefulRedisConnection),可以在多个线程间并发访问,且线程安全,满足多线程环境下的并发访问,同时它是可伸缩的设计,一个连接实例不够的情况也可以按需增加连接实例。
1.3.1 如何设置超时时间
连接池中的超时时间主要有:
- connectTimeout 建立 TCP 连接的超时时间
- maxWait 从连接池获取连接的最长等待时间
- socketTimeout 发送请求后等待响应的超时时间
connectTimeout 建议不要小于 1200ms
。TCP 在建立连接时,SYN 包的超时重传时间为 1s。connectTimeout 设置过短,应用发布时,初始化连接池过程中由于网络抖动,造成连接池初始化失败
socketTimeout 可以根据应用最长的查询返回时间设置。过长会造成生网络问题;过短会造成频繁请求超时。不要短于 300ms
。TCP 的最小 RTO 为 200ms,并根据延迟动态调整。过短的超时时间会造成单个丢包就造成请求超时。生产环境数据库都配置有Kill自动杀死执行时间过长的请求。因此,设置过长的也是没有意义的。
maxWait 可以根据应用期待的等待时间设置。过高容易造成雪崩。在连接池中连接数不足时,过短的时间会造成需要新建连接时造成大量超时。建议不要低于 100ms
,不要高于800ms。
1.3.2 Jedis
重要参数
maxTotal:#最大连接数,默认8
maxIdle : #最大空闲连接数,默认8
minIdle: #最少空闲连接数数,默认0
maxWaitMillis:#最大阻塞等待时间, 默认-1
timeout :命令执行超时时间,单位:毫秒
connectTimeout 默认2000ms
new JedisPool(config,ip, port, 2000, ...)
其他参数
testOnBorrow:向连接池借用连接时是否做连接有效性检测(ping),无效连接会被移除,每次借用多执行一次 ping 命令,默认值为 false。
testOnReturn:向连接池归还连接时是否做连接有效性检测(ping),无效连接会被移除,每次归还多执行一次 ping 命令,默认值为 false。
testOWhileIdle:向连接池借用连接时是否做连接空闲检测,空闲超时的连接会被移除,默认值为 false。
blockWhenExhausted:当连接池用尽后,调用者是否要等待,这个参数是和 maxWaitMillis 对应的,只有当此参数为 true 时,maxWaitMillis 才会生效。默认值为 true。
minEvictableIdleTimeMillis:连接的最小空闲时间,达到此值后空闲连接将被移除,默认值 30 分钟。
timeBetweenEvictionRunsMillis:空闲连接的检测周期(单位为毫秒),默认值为 -1,表示不做检测。
numTestsPerEvictionRun:做空闲连接检测时,每次的采样数,默认为 3。
1.3.3 Luttuce
重要参数
max-active #最大连接数,默认8
max-idle #最大空闲连接数,默认8
min-idle #最少空闲连接数数,默认0
max-wait #最大阻塞等待时间, 默认-1
timeout #命令执行超时时间,单位:毫秒
time-between-eviction-runs #eviction线程调度时间间隔
1.3.4 Luttuce断连案例
参考 www.cnblogs.com/wingcode/p/…
现象
在系统长时间无请求之后会必现 出现之后在十几分钟内不会自动重连
io.lettuce.core.RedisCommandTimeoutException: Command timed out after 1 minute(s)
经过日志排查(lettuce 的日志级别需要开启 DEBUG
或 TRACE
),发生RedisCommandTimeoutException
的原因时lettuce
的 Connection 已经断了,发生异常后大约 15 分钟,lettuce
的 ConnectionWatchdog
会进行自动重连
为什么 Redis 连接会断
在分布式环境中,网络分区是必然的。在网络环境,Redis 服务器主动断掉连接是很正常的,lettuce 的作者也提及 lettuce 一天发生一两次重连是很正常的。
那么哪些情况会导致连接断呢:
- Linux 内核的 keepalive 功能可能会一直收不到客户端的回应;
- 收到与该连接相关的 ICMP 错误信息;
- 其他网络链路问题等等;
为何 lettuce 没有立刻重连
根据ConnectionWatchdog
重连的机制(收到netty
的ChannelInactived
事件后启动重连的线程不断进行连接)可以确定,连接是由 Redis 服务端断开的,因为如果是客户端主动断开连接,那么一定能收到ChannelInactived
,因此,之所以lettuce
要等 15 分钟后才重连,是因为没收到ChanelInactived
事件。
那么为什么客户端没有到ChannelInactived
事件呢?很多情况都会,例如:
- 客户端没收到服务端 FIN 包;
- 网络链路断了,例如拔网线,断电等等;
在我们这个情况,应该是没收到服务端的 FIN 包。
日志显示发生RedisCommandTimeoutException
后,15 分钟后收到ChannelInactived
事件。
这与Linux的超时重传机制,也就是/proc/sys/net/ipv4/tcp_retries2
参数有关。据重传机制,发生RedisCommandTimeoutException
的命令会重传 tcp_retries2
这么多次,刚刚好是 15 分钟左右
解决
(1)在应用层增加心跳机制,定制 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
事件一定会被触发了。但是这个方案有个缺点,增加了客户端的压力。
(2)使用定时任务Ping
Redis Server
@Scheduled(fixedRate = 60000)
private void configureTasks() {
redisTemplate.execute(new RedisCallback<String>() {
@Override
public String doInRedis(@NotNull RedisConnection connection) throws DataAccessException {
return connection.ping();
}
});
}