RedisCluster集羣模式下master宕機主從切換期間Lettuce連接Redis無法使用的問題

最新一次線上生產環境下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/ASKredirections

後臺基於持續的斷開和移動/重定向的自適應更新

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的節點信息,沒有搞的那麼複雜

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章