0


Spring Cloud Zookeeper 优雅下线优化

这里是weihubeats,觉得文章不错可以关注公众号小奏技术,文章首发。拒绝营销号,拒绝标题党

背景

由于一些老项目,使用的注册中心还是

  1. Zookeeper

,众所周知在

  1. Spring Cloud

组件中是有客户端的负载均衡组件

  1. Spring Cloud LoadBalancer

会存在客户端缓存。
那么就会出现一个问题:
由于服务提供者已经在

  1. Zookeeper

下线了,而客户端缓存了旧的

  1. ServiceInstance

数据,导致调用失败。

之前也在spring-cloud-zookeeper提过这个issues,不过没人理我,所以需要自己改造

改造思路

知道了问题所在改造起来就非常容易了,思路很简单,就是服务提供者在

  1. Zookeeper

下线后需要客户端去删除客户端的本地缓存
所以我们需要知道

  1. Zookeeper

本地缓存在哪。接下来就是我们源码分析找找看

客户端获取消费者(

  1. ServiceInstance

)源码分析

我们知道

  1. Spring Cloud

统一了服务变成模型,有一个

  1. DiscoveryClient

接口,所以我们直接看

  1. DiscoveryClient

接口的实现类

然后我们简单看看

  1. ZookeeperDiscoveryClient

获取服务的方法实现

这一段方法比较简单,就是去

  1. zookeeper

获取注册数据,没有缓存,那么客户端缓存是再哪里缓存的呢。我们必须找到调用的缓存的地方

可以看到这里是响应式获取数据,也没有缓存,我还需要向上寻找

功夫不负有心人,我们总算找到了这个缓存类

  1. ObjectProvider<LoadBalancerCacheManager> cacheManagerProvider = context
  2. .getBeanProvider(LoadBalancerCacheManager.class);

如果看过我之前的这篇Spring Cloud落地之Spring Cloud LoadBalancer 线上优化方案
就知道他的缓存用的什么缓存,这里我们就不再介绍使用的什么缓存了。只需要知道我们拿到了这个缓存,就可以做我们想做的事情了。

改造缓存分布式删除

首先这里的客户端缓存是本地缓存,我们的机器一般是部署了多个节点,我们需要删除所有节点的缓存。
所以我们可以这么设计

  1. 客户端(网关)直接使用Zookeeper的事件监听然后去删除缓存
  2. 由下线的服务提供者去调用客户端(网关的接口),然后客户端通知其他节点一起删除缓存

现在又两种方案,最简单的方案肯定是第一种

Zookeeper事件监听

实现代码大致如下

  1. @Component@Slf4jpublicclassZookeeperListenerimplementsApplicationContextAware{privateApplicationContext applicationContext;@ResourceprivateCuratorFramework curatorClient;@Value("${spring.cloud.zookeeper.discovery.root}")privateString path;@PostConstructpublicvoidinit(){//当前节点CuratorCache curatorCache =CuratorCache.builder(curatorClient, path).build();//监听子节点,不监听当前节点CuratorCacheListener pathCacheListener =CuratorCacheListener.builder().forPathChildrenCache(path, curatorClient,(client, event)->{String type = event.getType().name();
  2. log.info("PathChildrenCacheListener {}", type);if(Objects.equals(event.getType(),PathChildrenCacheEvent.Type.CHILD_REMOVED)){ObjectProvider<LoadBalancerCacheManager> cacheManagerProvider = applicationContext
  3. .getBeanProvider(LoadBalancerCacheManager.class);LoadBalancerCacheManager ifAvailable = cacheManagerProvider.getIfAvailable();assert ifAvailable !=null;Cache cache = ifAvailable.getCache(SERVICE_INSTANCE_CACHE_NAME);if(Objects.nonNull(cache)){// todo 这里需要删除指定key 而不是全量清除缓存
  4. cache.clear();}
  5. log.info("本地缓存清除完成");}}).build();
  6. curatorCache.listenable().addListener(pathCacheListener);
  7. curatorCache.start();}@OverridepublicvoidsetApplicationContext(ApplicationContext applicationContext)throwsBeansException{this.applicationContext = applicationContext;}}

在写完代码上线测试发现比较多的问题,大致如下

  1. Zookeeper 不同版本导致事件监听失效

由于我们zk线上版本是3.5,测试是3.7.导致这段代码测试环境有效线上报错

  1. Zookeeper 事件延迟
  2. Zookeeper 事件存在丢失的情况

http删除缓存

  1. Zookeeper

事件监听不靠谱我们就使用第二种方案

多节点的缓存删除我们使用redis作通知

  • RedissonConfig
  1. @ConfigurationpublicclassRedissonConfig{@Value("${redis..host}")privateString redisLoginHost;@Value("${redis..port}")privateInteger redisLoginPort;@Value("${redis..password}")privateString redisLoginPassword;@BeanpublicRedissonClientredissonClient(){returncreateRedis(redisLoginHost, redisLoginPort, redisLoginPassword);}privateRedissonClientcreateRedis(String redisHost,Integer redisPort,String redisPassword){Config config =newConfig();SingleServerConfig singleServerConfig = config.useSingleServer();
  2. singleServerConfig.setAddress("redis://"+ redisHost +":"+ redisPort +"");if(DataUtils.isNotEmpty(redisPassword)){
  3. singleServerConfig.setPassword(redisPassword);}returnRedisson.create(config);}}
  • RedisSubscriber
  1. @Component@Slf4jpublicclassRedisSubscriberimplementsApplicationRunner,ApplicationContextAware{publicstaticfinalString GRACEFUL_SHUTDOWN ="graceful-shutdown";privateApplicationContext applicationContext;@AutowiredprivateRedissonClient redisson;@OverridepublicvoidsetApplicationContext(ApplicationContext applicationContext)throwsBeansException{this.applicationContext = applicationContext;}@Overridepublicvoidrun(ApplicationArguments args){RTopic topic = redisson.getTopic(GRACEFUL_SHUTDOWN);
  2. topic.addListener(ClientDTO.class,(channel, clientDTO)->{String applicationName = clientDTO.getApplicationName();ObjectProvider<LoadBalancerCacheManager> cacheManagerProvider = applicationContext
  3. .getBeanProvider(LoadBalancerCacheManager.class);LoadBalancerCacheManager ifAvailable = cacheManagerProvider.getIfAvailable();assert ifAvailable !=null;Cache cache = ifAvailable.getCache(SERVICE_INSTANCE_CACHE_NAME);if(Objects.nonNull(cache)){List<ZookeeperServiceInstance> serviceInstances = cache.get(applicationName,List.class);if(DataUtils.isNotEmpty(serviceInstances)){List<ZookeeperServiceInstance> collect = serviceInstances.stream().filter(s ->{ServiceInstance<ZookeeperInstance> serviceInstance = s.getServiceInstance();String id = serviceInstance.getId();return!Objects.equals(id, clientDTO.getId());}).collect(Collectors.toList());
  4. cache.put(applicationName, collect);
  5. log.info("本地缓存清除完成 id {} ", clientDTO.getId());}else{
  6. log.info("本地缓存null");}}});}}
  • controller
  1. @GetMapping("/flushCache")publicMap<String,Object>flushCache(ClientDTO clientDTO){
  2. log.info("flushCache, applicationName : {}", clientDTO.getApplicationName());if(DataUtils.isNotEmpty(clientDTO)){RTopic topic = redissonClient.getTopic(GRACEFUL_SHUTDOWN);
  3. topic.publish(clientDTO);
  4. log.info("flushCache 发送缓存topic, applicationName : {}", clientDTO.getApplicationName());}Map<String,Object> result =newHashMap<>();
  5. result.put("code",100);
  6. result.put("message","ok");return result;}

这样我们服务提供者在销毁的时候注销zk,然后调用该接口去删除客户端缓存,就可以解决如下问题。实现

  1. Spring Cloud Zookeeper

的优雅下线

客户端优雅下线sdk

我们可以给接入的服务消费者提供一个简单的sdk,在接受到Spring ContextClosedEvent事件后进行调用上面的接口清除缓存

核心代码如下

  1. publicvoidgracefulShutdown(){this.serviceRegistry.deregister(this.serviceInstanceRegistration);
  2. log.info("shutdown 注销Zookeeper服务");this.serviceRegistry.close();
  3. log.info("shutdown 关闭Zookeeper连接");try{ServiceInstance<ZookeeperInstance> instance =this.serviceInstanceRegistration.getServiceInstance();String serviceName =this.serviceInstanceRegistration.getServiceInstance().getName();String host =this.serviceInstanceRegistration.getServiceInstance().getAddress();String id =this.serviceInstanceRegistration.getServiceInstance().getId();String url =String.format("%s?applicationName=%s&host=%s&id=%s",this.flushCacheUrl, serviceName, host, id);String ret =OkHttpUtils.get(url);
  4. log.info("ret: {}", ret);}catch(Exception var7){
  5. log.error("flush cache error : {}",this.flushCacheUrl);}}

总结

基于该方案改造后,线上服务发版下线就再也没有报错了,非常优雅


本文转载自: https://blog.csdn.net/qq_42651904/article/details/127144784
版权归原作者 weihubeats 所有, 如有侵权,请联系我们删除。

“Spring Cloud Zookeeper 优雅下线优化”的评论:

还没有评论