Spring Cloud O
penFeign默认会将从注册中心Consul获取的服务实例信息在Caffine缓存中缓存35秒,这会出现服务下线后,下线服务仍然被调用的问题,导致上游服务在缓存有效期内不可用。本文讲解如何通过Consul Watch机制来实时监听相关服务的更新,以实现服务的平滑上下线。
RoundRobinLoadBalancer使用CachingServiceInstanceListSupplierle加载服务实例信息
public CachingServiceInstanceListSupplier(ServiceInstanceListSupplier delegate, CacheManager cacheManager) {
super(delegate);
this.serviceInstances = CacheFlux.lookup(key -> {
// TODO: configurable cache name
//首先从缓存中获取
Cache cache = cacheManager.getCache(SERVICE_INSTANCE_CACHE_NAME);
if (cache == null) {
if (log.isErrorEnabled()) {
log.error("Unable to find cache: " + SERVICE_INSTANCE_CACHE_NAME);
}
return Mono.empty();
}
List<ServiceInstance> list = cache.get(key, List.class);
if (list == null || list.isEmpty()) {
return Mono.empty();
}
return Flux.just(list).materialize().collectList();
}, delegate.getServiceId())
//未命中缓存时,使用DiscoveryClient从Consul查询
.onCacheMissResume(delegate.get().take(1))
.andWriteWith((key, signals) -> Flux.fromIterable(signals).dematerialize().doOnNext(instances -> {
Cache cache = cacheManager.getCache(SERVICE_INSTANCE_CACHE_NAME);
if (cache == null) {
if (log.isErrorEnabled()) {
log.error("Unable to find cache for writing: " + SERVICE_INSTANCE_CACHE_NAME);
}
}
else {
//写入缓存
cache.put(key, instances);
}
}).then());
}
参考Spring Cloud Config的源码,首先定义Consul心跳监视器类 HeartbeatMonitor,就是Copy源码...
/**
* Helper class for listeners to the {@link HeartbeatEvent}, providing a convenient way to
* determine if there has been a change in state.
*
* @author Dave Syer
*/
public class HeartbeatMonitor {
private AtomicReference<Object> latestHeartbeat = new AtomicReference<>();
/**
* @param value The latest heartbeat.
* @return True if the state changed.
*/
public boolean update(Object value) {
Object last = this.latestHeartbeat.get();
if (value != null && !value.equals(last)) {
return this.latestHeartbeat.compareAndSet(last, value);
}
return false;
}
}
源码中ConsulCatalogWatch类会定时监听(默认1秒)Consul的服务状态变化,仿照Spring Cloud Config,监听Consul心跳事件,在Consul中注册的服务实例信息变更时,刷新Caffine缓存。
//参考 Spring Cloud Config - ConfigServerInstanceMonitor类
@Slf4j
@Component
public class ServiceInstanceMonitor implements ApplicationListener<HeartbeatEvent>,
ApplicationContextAware, InitializingBean {
private final HeartbeatMonitor monitor = new HeartbeatMonitor();
private ApplicationContext applicationContext;
@Autowired
private DiscoveryClient discoveryClient;
@Autowired
private Environment environment;
private List<String> feignServiceIdList = new ArrayList<>();
@Override
public void onApplicationEvent(HeartbeatEvent event) {
if (this.monitor.update(event.getValue())) {
refresh();
}
}
public void refresh() {
ObjectProvider<LoadBalancerCacheManager> cacheManagerProvider = applicationContext
.getBeanProvider(LoadBalancerCacheManager.class);
if (cacheManagerProvider.getIfAvailable() != null) {
//Feign Service Instances Cache刷新
LoadBalancerCacheManager cacheManager = cacheManagerProvider.getIfAvailable();
Cache cache = cacheManager.getCache(SERVICE_INSTANCE_CACHE_NAME);
for (String serviceId : feignServiceIdList) {
List<ServiceInstance> serviceInstances = discoveryClient.getInstances(serviceId);
if (CollectionUtils.isEmpty(serviceInstances)) {
log.warn("refresh | {} can not discovery.", serviceId);
continue;
}
cache.put(serviceId, serviceInstances);
}
}
}
@Override
public void setApplicationContext(ApplicationContext applicationContext)
throws BeansException {
this.applicationContext = applicationContext;
}
@Override
public void afterPropertiesSet() throws Exception {
Map<String, Object> feignServiceMap =
applicationContext.getBeansWithAnnotation(FeignClient.class);
for (Object feignBean : feignServiceMap.values()) {
FeignClient annotation =
AnnotationUtils.findAnnotation(feignBean.getClass(), FeignClient.class);
String url = (String) AnnotationUtils.getValue(annotation, "url");
url = environment.resolvePlaceholders(url);
//@FeignClient的url属性值不为空时,说明是指定服务请求URL的,不需要load-balancer
if (StringUtils.isBlank(url)) {
String serviceId = (String) AnnotationUtils.getValue(annotation);
serviceId = environment.resolvePlaceholders(serviceId);
if(StringUtils.isBlank(serviceId)){
throw new IllegalArgumentException("feign service-name must be not empty");
}
feignServiceIdList.add(serviceId);
}
}
}
}
加入上面代码后,在服务上下线时使相应的服务实例缓存刷新即可。