Redis|常见线上问题&性能问题分析(下)

1,169 阅读6分钟

网络问题

现象 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_probestcp_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覆盖了linuxtcp_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重连的机制(收到nettyChannelInactived事件后启动重连的线程不断进行连接)可以确定,连接是由 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)使用定时任务PingRedis Server

@Scheduled(fixedRate = 60000)
private void configureTasks() {
  redisTemplate.execute(new RedisCallback<String>() {
    @Override
    public String doInRedis(@NotNull RedisConnection connection) throws DataAccessException {
      return connection.ping();
    }
  });
}