RedisCluster集羣模式下master宕機主從切換期間Lettuce鏈接Redis沒法使用報錯Redis command timed out的問題

最新一次線上生產環境下Redis集羣服務器某一個主節點發生故障,Cluster節點下的從節點快速進行遷移升級爲主節點,節點遷移時間大概爲15秒,這15秒期間Redis服務不可用,程序沒法讀寫Redis數據,報錯java.lang.RuntimeException: org.springframework.web.util.NestedServletException: Request processing failed; nested exception is org.springframework.dao.QueryTimeoutException: Redis command timed out; nested exception is io.lettuce.core.RedisCommandTimeoutException: Command timed out after 1 minute(s),可是15秒事後服務依舊沒法使用,大概持續了6分鐘,而在業務高峯期間這6分鐘也會形成很大的用戶感知,爲什麼要持續這麼久Redis才能恢復,成爲了未知的謎團!html

聯合運維和雲廠商作了不少測試,發現凡是使用jedis客戶端的服務均可以在15秒主從切換後恢復,而使用lettuce做爲redis客戶端的服務則沒法恢復使用,一直拋超時的異常,作了實驗發現,使用lettuce做爲客戶端的服務,在15秒主從切換後一直要等待redis服務的宕機節點拉起成功後才能夠恢復,而這時間大概持續了2分鐘,從網上搜了不少答案發現也有一些遇到了一樣問題的狀況發生。Lettuce的節點切換15秒是來源於 cluster-node-timeout這個配置的默認時間,這個是時間節點宕機發現時間,也就是Redis羣集節點不可用的最長時間,由於RedisCluster是無中心設計,節點探測的時間設置過小會由於網絡抖動形成的節點下線,時間太長又沒法快速處理節點切換,這個能夠具體瞭解Cluster集羣主從切換的原理。相關閱讀https://www.cnblogs.com/kaleidoscope/p/9636264.htmljava

由於全部微服務使用SpringBoot2.1.7版本SpringBoot2.X版本開始Redis默認的鏈接池都是採用的Lettuce,以前的文章也有介紹過Lettuce鏈接池的使用,爲了不後續出現硬件故障,致使服務鏈接Redis一段時間不可用的狀況,因此也就急須要解決節點宕機的恢復時間問題。node

通過大量的調研和實驗最後發現有關,官方的描述是https://github.com/lettuce-io/lettuce-core/wiki/Redis-Cluster#user-content-refreshing-the-cluster-topology-view, Lettuce須要刷新節點拓撲視圖,git

大體意思是,Redis集羣配置在運行期間可能會改變,能夠添加新的節點,爲特定插槽的主節點能夠發生改變,Lettuce處理Moved和Ask永久重定向,可是因爲命令重定向,你必須刷新節點拓撲視圖,拓撲是綁定到RedisClusterClient的示例,全部由一個RedisClusterClient實例建立的節點鏈接共享相同的節點拓撲視圖,視圖能夠採用如下三種方式更新github

一、Either by calling RedisClusterClient.reloadPartitionsweb

經過調用RedisClusterClient.reloadPartitionsredis

二、Periodic updates in the background based on an intervalspring

後臺基於時間間隔的週期刷新緩存

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

相關文章
相關標籤/搜索