最新一次線上生產環境下Redis集羣服務器某一個主節點發生故障,Cluster節點下的從節點快速進行遷移升級爲主節點,節點遷移時間大概爲15秒,這15秒期間Redis服務不可用,程序無法讀寫Redis數據,但是15秒過後服務依舊無法使用,大概持續了6分鐘,而在業務高峯期間這6分鐘也會造成很大的用戶感知,爲何要持續這麼久Redis才能恢復,成爲了未知的謎團!
聯合運維和雲廠商做了很多測試,發現凡是使用jedis客戶端的服務都可以在15秒主從切換後恢復,而使用lettuce作爲redis客戶端的服務則無法恢復使用,一直拋超時的異常,做了實驗發現,使用lettuce作爲客戶端的服務,在15秒主從切換後一直要等待redis服務的宕機節點拉起成功後纔可以恢復,而這時間大概持續了2分鐘,從網上搜了很多答案發現也有一些遇到了同樣問題的情況發生。Lettuce的節點切換15秒是來源於 cluster-node-timeout這個配置的默認時間,這個是時間節點宕機發現時間,也就是Redis羣集節點不可用的最長時間,因爲RedisCluster是無中心設計,節點探測的時間設置太小會因爲網絡抖動造成的節點下線,時間太長又無法快速處理節點切換,這個可以具體瞭解Cluster集羣主從切換的原理。相關閱讀https://www.cnblogs.com/kaleidoscope/p/9636264.html
因爲所有微服務使用SpringBoot2.1.7版本SpringBoot2.X版本開始Redis默認的連接池都是採用的Lettuce,之前的文章也有介紹過Lettuce連接池的使用,爲了避免後續出現硬件故障,導致服務連接Redis一段時間不可用的情況,所以也就急需要解決節點宕機的恢復時間問題。
經過大量的調研和實驗最後發現有關,官方的描述是https://github.com/lettuce-io/lettuce-core/wiki/Redis-Cluster#user-content-refreshing-the-cluster-topology-view, Lettuce需要刷新節點拓撲視圖,
大致意思是,Redis集羣配置在運行期間可能會改變,可以添加新的節點,爲特定插槽的主節點可以發生改變,Lettuce處理Moved和Ask永久重定向,但是由於命令重定向,你必須刷新節點拓撲視圖,拓撲是綁定到RedisClusterClient的示例,所有由一個RedisClusterClient實例創建的節點連接共享相同的節點拓撲視圖,視圖可以採用以下三種方式更新
1、Either by calling RedisClusterClient.reloadPartitions
通過調用RedisClusterClient.reloadPartitions
2、Periodic updates in the background based on an interval
後臺基於時間間隔的週期刷新
3、Adaptive updates in the background based on persistent disconnects and MOVED
/ASK
redirections
後臺基於持續的斷開和移動/重定向的自適應更新
By default, commands follow -ASK
and -MOVED
redirects up to 5 times until the command execution is considered to be failed. Background topology updating starts with the first connection obtained through RedisClusterClient
.
默認的 命令跟隨ASK
和移MOVED
命令執行重定向到5次,直到被認爲是失敗了,後臺拓撲更新始於第一次RedisClusterClient鏈接
相關閱讀 https://github.com/lettuce-io/lettuce-core/wiki/Client-options#periodic-cluster-topology-refresh
所以說在RedisCluster集羣模式下可以通過 3種方式去刷新節點拓撲視圖去解決節點重新識別的問題,
第一種方式是通過RedisClusterClient,SpringBoot通過Sprint Redis Data構建Redis時,沒有顯式構建RedisClusterClient,所以只能通過其他兩種方式
https://github.com/lettuce-io/lettuce-core/wiki/Client-Options
這裏描述了很多特殊場景下設置的客戶端選項,可以視自身情況去設置調整
@Autowired
private RedisProperties redisProperties;
@Bean
public GenericObjectPoolConfig<?> genericObjectPoolConfig(Pool properties) {
GenericObjectPoolConfig<?> config = new GenericObjectPoolConfig<>();
config.setMaxTotal(properties.getMaxActive());
config.setMaxIdle(properties.getMaxIdle());
config.setMinIdle(properties.getMinIdle());
if (properties.getTimeBetweenEvictionRuns() != null) {
config.setTimeBetweenEvictionRunsMillis(properties.getTimeBetweenEvictionRuns().toMillis());
}
if (properties.getMaxWait() != null) {
config.setMaxWaitMillis(properties.getMaxWait().toMillis());
}
return config;
}
@Bean(destroyMethod = "destroy")
public LettuceConnectionFactory lettuceConnectionFactory() {
//開啓 自適應集羣拓撲刷新和週期拓撲刷新
ClusterTopologyRefreshOptions clusterTopologyRefreshOptions = ClusterTopologyRefreshOptions.builder()
// 開啓全部自適應刷新
.enableAllAdaptiveRefreshTriggers() // 開啓自適應刷新,自適應刷新不開啓,Redis集羣變更時將會導致連接異常
// 自適應刷新超時時間(默認30秒)
.adaptiveRefreshTriggersTimeout(Duration.ofSeconds(30)) //默認關閉開啓後時間爲30秒
// 開週期刷新
.enablePeriodicRefresh(Duration.ofSeconds(20)) // 默認關閉開啓後時間爲60秒 ClusterTopologyRefreshOptions.DEFAULT_REFRESH_PERIOD 60 .enablePeriodicRefresh(Duration.ofSeconds(2)) = .enablePeriodicRefresh().refreshPeriod(Duration.ofSeconds(2))
.build();
// https://github.com/lettuce-io/lettuce-core/wiki/Client-Options
ClientOptions clientOptions = ClusterClientOptions.builder()
.topologyRefreshOptions(clusterTopologyRefreshOptions)
.build();
LettuceClientConfiguration clientConfig = LettucePoolingClientConfiguration.builder()
.poolConfig(genericObjectPoolConfig(redisProperties.getLettuce().getPool()))
//.readFrom(ReadFrom.MASTER_PREFERRED)
.clientOptions(clientOptions)
.commandTimeout(redisProperties.getTimeout()) //默認RedisURI.DEFAULT_TIMEOUT 60
.build();
List<String> clusterNodes = redisProperties.getCluster().getNodes();
Set<RedisNode> nodes = new HashSet<RedisNode>();
clusterNodes.forEach(address -> nodes.add(new RedisNode(address.split(":")[0].trim(), Integer.valueOf(address.split(":")[1]))));
RedisClusterConfiguration clusterConfiguration = new RedisClusterConfiguration();
clusterConfiguration.setClusterNodes(nodes);
clusterConfiguration.setPassword(RedisPassword.of(redisProperties.getPassword()));
clusterConfiguration.setMaxRedirects(redisProperties.getCluster().getMaxRedirects());
LettuceConnectionFactory lettuceConnectionFactory = new LettuceConnectionFactory(clusterConfiguration, clientConfig);
// lettuceConnectionFactory.setShareNativeConnection(false); //是否允許多個線程操作共用同一個緩存連接,默認true,false時每個操作都將開闢新的連接
// lettuceConnectionFactory.resetConnection(); // 重置底層共享連接, 在接下來的訪問時初始化
return lettuceConnectionFactory;
}
開啓自適應刷新並設定刷新頻率
可以看到設定前,週期刷新和拓撲刷新都是false
調整後周期刷新和拓撲刷新都是true
enablePeriodicRefresh意思就是開啓並設定週期刷新時間
開關的開啓後的控制實際是RedisClusterClient.activateTopologyRefreshIfNeeded在這個方法內完成的,如果開關開啓則會創建一個ScheduledFuture 根據你設置的節點刷新事件定期的去調用,當RedisClusterClient初始化後,定時器會週期性的執行,
如果 定時器執行通過,則RedisClusterClient.doLoadPartitions會返回loadedPartitions,如果半截Return掉,則不再返回新的節點信息。
相關閱讀https://github.com/lettuce-io/lettuce-core/issues/240
相關閱讀https://blog.csdn.net/weixin_42182797/article/details/95210437#_1
當然,如果你想就此放棄lettuce轉用jedis也是可以的 Spring Boot2.X版本,只要在pom.xml裏,調整一下依賴包的引用
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
<exclusions>
<exclusion>
<groupId>io.lettuce</groupId>
<artifactId>lettuce-core</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
</dependency>
配置上lettuce換成jedis的,既可以完成底層對jedis的替換
spring:
redis:
jedis:
pool:
max-active: ${redis.config.maxTotal:1024}
max-idle: ${redis.config.maxIdle:50}
min-idle: ${redis.config.minIdle:1}
max-wait: ${redis.config.maxWaitMillis:5000}
#lettuce:
#pool:
#max-active: ${redis.config.maxTotal:1024}
#max-idle: ${redis.config.maxIdle:50}
#min-idle: ${redis.config.minIdle:1}
#max-wait: ${redis.config.maxWaitMillis:5000}
因爲jedis的節點信息,沒有搞的那麼複雜