一 背景
公司的Redis集群原本是部署在三台机器上,三主三从的集群,如这篇文章Redis三主三从集群Docker方式搭建的方式搭建,只不过是三台机器主从交错,避免同一个节点的主从在同一台机器,每台机器部署了两个实例。
以上方式的部署存在一个弊端:某些情况下,自动切换了主从,会导致两个主都落在同一台机器上,如果这台机器突然down掉,等于两个主直接down掉了(过半的主一起down掉),会直接导致集群不可用。
为了避免以上问题,我们增加了三台机器,先扩展到了三主六从,再把以前的三个从踢掉降到三主三从(具体操作请参考文章Redis三主三从集群Docker方式搭建中的增加增加节点和删除节点相关小节),但是此操作对我们的SpringBoot微服务造成了一些影响。
二 报错与解决方案
具体的报错如下:
对于使用SpringBoot2.x的应用默认使用lettuce的redis客户端的情况
服务日志如下会不断的报如下日志:1
2
3
42021-02-25 07:16:17.136 WARN 39164 --- [ioEventLoop-4-7] i.l.core.protocol.ConnectionWatchdog : Cannot reconnect to [10.0.35.249:6380]: Connection refused: /10.0.35.249:6380
2021-02-25 07:16:17.336 WARN 39164 --- [ioEventLoop-4-8] i.l.core.protocol.ConnectionWatchdog : Cannot reconnect to [10.0.35.249:6380]: Connection refused: /10.0.35.249:6380
2021-02-25 07:16:47.240 WARN 39164 --- [ioEventLoop-4-1] i.l.core.protocol.ConnectionWatchdog : Cannot reconnect to [10.0.35.249:6380]: Connection refused: /10.0.35.249:6380
2021-02-25 07:16:47.438 WARN 39164 --- [ioEventLoop-4-2] i.l.core.protocol.ConnectionWatchdog : Cannot reconnect to [10.0.35.249:6380]: Connection refused: /10.0.35.249:6380
解决方案:直接重启这些服务
那么如何避免呢,如何让服务感知到redis集群拓扑的变更。
针对spring boot 2.3.0+版本,可以直接通过添加以下配置让应用及时刷新redis集群的拓扑(默认是false)
1
2
3
4
5
6 spring:
redis:
lettuce:
cluster:
refresh:
adaptive: true
针对 spring boot 2.3.0以下版本,可以通过增加以下配置即使刷新redis集群拓扑
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35 public LettuceConnectionFactory redisConnectionFactory(ClientResources clientResources) {
// redis单节点
if (null == redisProperties.getCluster() || null == redisProperties.getCluster().getNodes()) {
RedisStandaloneConfiguration configuration = new RedisStandaloneConfiguration(redisProperties.getHost(),
redisProperties.getPort());
configuration.setPassword(redisProperties.getPassword());
return new LettuceConnectionFactory(configuration);
}
ClusterTopologyRefreshOptions topologyRefreshOptions = ClusterTopologyRefreshOptions.builder()
// //按照周期刷新拓扑
.enablePeriodicRefresh(Duration.ofSeconds(30))
//根据事件刷新拓扑
.enableAllAdaptiveRefreshTriggers()
.build();
ClusterClientOptions clusterClientOptions = ClusterClientOptions.builder()
//redis命令超时时间,超时后才会使用新的拓扑信息重新建立连接
.timeoutOptions(TimeoutOptions.enabled(Duration.ofSeconds(10)))
.topologyRefreshOptions(topologyRefreshOptions)
.build();
LettuceClientConfiguration clientConfiguration = LettuceClientConfiguration.builder()
.clientResources(clientResources)
.clientOptions(clusterClientOptions)
.build();
RedisClusterConfiguration clusterConfig = new RedisClusterConfiguration(redisProperties.getCluster().getNodes());
if (redisProperties.getCluster().getMaxRedirects() != null) {
clusterConfig.setMaxRedirects(redisProperties.getCluster().getMaxRedirects());
}
clusterConfig.setPassword(RedisPassword.of(redisProperties.getPassword()));
return new LettuceConnectionFactory(clusterConfig, clientConfiguration);
}
如果服务是部署在k8s,并且存活探针用了actuator的health地址,那k8s容器里的服务也一样会down掉,也会导致服务不可用,即使服务层面已经刷新了redis集群的拓扑,服务/actuator/health健康情况依然会是down状态(原因是配置的redis集群nodes的每个node都会检查是否健康,不管这个node是主节点还是从节点),如下:1
2
3
4
5
6"redis": {
"status": "DOWN",
"details": {
"error": "org.springframework.data.redis.RedisConnectionFailureException: Redis connection failed; nested exception is io.lettuce.core.RedisConnectionException: Unable to connect to 10.0.35.249:6380"
}
},
所以针对这种情况,可以考虑把redis集群的健康监控关闭(或者重写redis集群健康监控的Indicator),避免down掉一个节点导致整个服务不可用。关闭方法如下:1
management.health.redis.enabled: false
对于使用SpringBoot2.x的应用替换使用jedis的redis客户端的情况
仅仅在切换过程中会报如下异常,很快就会恢复正常,/actuator/health健康监控中redis一直都是健康状态1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16org.springframework.data.redis.TooManyClusterRedirectionsException: No more cluster attempts left.; nested exception is redis.clients.jedis.exceptions.JedisClusterMaxAttemptsException: No more cluster attempts left.
at org.springframework.data.redis.connection.jedis.JedisExceptionConverter.convert(JedisExceptionConverter.java:55) ~[spring-data-redis-2.3.4.RELEASE.jar:2.3.4.RELEASE]
at org.springframework.data.redis.connection.jedis.JedisExceptionConverter.convert(JedisExceptionConverter.java:42) ~[spring-data-redis-2.3.4.RELEASE.jar:2.3.4.RELEASE]
at org.springframework.data.redis.PassThroughExceptionTranslationStrategy.translate(PassThroughExceptionTranslationStrategy.java:44) ~[spring-data-redis-2.3.4.RELEASE.jar:2.3.4.RELEASE]
at org.springframework.data.redis.FallbackExceptionTranslationStrategy.translate(FallbackExceptionTranslationStrategy.java:42) ~[spring-data-redis-2.3.4.RELEASE.jar:2.3.4.RELEASE]
at org.springframework.data.redis.connection.jedis.JedisClusterConnection.convertJedisAccessException(JedisClusterConnection.java:777) ~[spring-data-redis-2.3.4.RELEASE.jar:2.3.4.RELEASE]
at org.springframework.data.redis.connection.jedis.JedisClusterStringCommands.convertJedisAccessException(JedisClusterStringCommands.java:525) ~[spring-data-redis-2.3.4.RELEASE.jar:2.3.4.RELEASE]
at org.springframework.data.redis.connection.jedis.JedisClusterStringCommands.setEx(JedisClusterStringCommands.java:177) ~[spring-data-redis-2.3.4.RELEASE.jar:2.3.4.RELEASE]
at org.springframework.data.redis.connection.DefaultedRedisConnection.setEx(DefaultedRedisConnection.java:308) ~[spring-data-redis-2.3.4.RELEASE.jar:2.3.4.RELEASE]
at org.springframework.data.redis.connection.DefaultStringRedisConnection.setEx(DefaultStringRedisConnection.java:1009) ~[spring-data-redis-2.3.4.RELEASE.jar:2.3.4.RELEASE]
at org.springframework.data.redis.core.DefaultValueOperations$4.potentiallyUsePsetEx(DefaultValueOperations.java:268) ~[spring-data-redis-2.3.4.RELEASE.jar:2.3.4.RELEASE]
at org.springframework.data.redis.core.DefaultValueOperations$4.doInRedis(DefaultValueOperations.java:261) ~[spring-data-redis-2.3.4.RELEASE.jar:2.3.4.RELEASE]
at org.springframework.data.redis.core.RedisTemplate.execute(RedisTemplate.java:228) ~[spring-data-redis-2.3.4.RELEASE.jar:2.3.4.RELEASE]
at org.springframework.data.redis.core.RedisTemplate.execute(RedisTemplate.java:188) ~[spring-data-redis-2.3.4.RELEASE.jar:2.3.4.RELEASE]
at org.springframework.data.redis.core.AbstractOperations.execute(AbstractOperations.java:96) ~[spring-data-redis-2.3.4.RELEASE.jar:2.3.4.RELEASE]
at org.springframework.data.redis.core.DefaultValueOperations.set(DefaultValueOperations.java:256) ~[spring-data-redis-2.3.4.RELEASE.jar:2.3.4.RELEASE]
三 结论
综上,对于spring boot 2.x,如果连接的redis是集群方式的,建议通过jedis客户端来连,可以避免节点down掉后导致服务不可用的情况。
虽然spring boot 2.x默认的redis客户端是lettuce,官方也是建议使用lettuce客户端,但是这块对redis集群的支持感觉还不是特别好,如果使用的redis主从,可能直接使用lettuce客户端是OK的,但是针对redis集群的情况下,还是使用jedis比较靠谱一些。