Nacos 实现服务平滑上下线(Ribbon 和 LB)
前言
不知道各位在使用 SpringCloud Gateway + Nacos
的时候有没有遇到过服务刚上线偶尔会出现一段时间的503 Service Unavailable
,或者服务下线后,下线服务仍然被调用的问题。而以上问题都是由于Ribbon
或者LoadBalancer
的默认处理策略有关,其中Ribbon
默认是 30s 更新一次服务信息,LoadBalancer
则是默认 35s 更新一次缓存。接下来本文讲解则如何通过监听Nacos
的服务变更事件来实时进行相关服务的更新,以实现服务的平滑上下线。
监听 Nacos 服务变更实现
首先我们要知道的是,在前言中提到的服务上线未被及时感知是由于使用Ribbon
或者LoadBalancer
组件的默认处理策略所导致的,Nacos
是可以及时感知并触发服务上下线的事件,因为我们要做的就是监听Nacos
的这个事件,然后在事件处理中,自己手动去调用相关的更新操作以实现需求。而com.alibaba.nacos.client.naming.event.InstancesChangeEvent
这个事件则正是符合我们需求的事件,然后我们就可以参考com.alibaba.nacos.client.naming.event.InstancesChangeNotifier
这个类来实现一个我们自己的订阅类:
import com.alibaba.nacos.api.common.Constants; import com.alibaba.nacos.client.naming.event.InstancesChangeEvent; import com.alibaba.nacos.common.notify.NotifyCenter; import com.alibaba.nacos.common.notify.listener.Subscriber; import com.alibaba.nacos.common.notify.Event; import com.netflix.loadbalancer.*; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Component; import javax.annotation.PostConstruct; /** * 服务变更监听 * * @author butterfly * @date 2023-09-24 */ @Slf4j @Component public class ServiceChangeNotifier extends Subscriber<InstancesChangeEvent> { @PostConstruct public void init() { // 注册当前自定义的订阅者以获取通知 NotifyCenter.registerSubscriber(this); } @Override public void onEvent(InstancesChangeEvent event) { String serviceName = event.getServiceName(); // 使用 dubbo 时包含 rpc 服务类会注册以 providers: 或者 consumers: 开头的服务 // 由于不是正式的服务, 这里需要进行排除, 如果未使用 dubbo 则不需要该处理 if (serviceName.contains(":")) { return; } // serviceName 格式为 groupName@@name String split = Constants.SERVICE_INFO_SPLITER; if (serviceName.contains(split)) { serviceName = serviceName.substring(serviceName.indexOf(split) + split.length()); } log.info("服务上下线: {}", serviceName); // 针对服务进行后续更新操作 } @Override public Class<? extends Event> subscribeType() { return InstancesChangeEvent.class; } }
nacos-client 为 2.1.1 时会出现订阅失效的 bug,需要重写以下方法:
@Override public boolean scopeMatches(InstancesChangeEvent event) { return true; } 具体原因参考issue。
基于 Ribbon 的实现
Ribbon
默认情况下是 30s 刷新一次服务列表,详情可看com.netflix.loadbalancer.PollingServerListUpdater
,其中部分代码如下:
public class PollingServerListUpdater implements ServerListUpdater { private static int LISTOFSERVERS_CACHE_REPEAT_INTERVAL = 30 * 1000; // msecs; private static long getRefreshIntervalMs(IClientConfig clientConfig) { return clientConfig.get(CommonClientConfigKey.ServerListRefreshInterval, LISTOFSERVERS_CACHE_REPEAT_INTERVAL); } }
这里的时间间隔可以通过ribbon.ServerListRefreshInterval=xxx
进行配置,其中xxx
对应自定义的毫秒时间间隔,而通过监听Nacos
的服务变更事件,则不必调整时间间隔即可实现服务的平滑上下线,具体代码如下:
import com.alibaba.nacos.api.common.Constants; import com.alibaba.nacos.client.naming.event.InstancesChangeEvent; import com.alibaba.nacos.common.notify.NotifyCenter; import com.alibaba.nacos.common.notify.listener.Subscriber; import com.alibaba.nacos.common.notify.Event; import com.netflix.loadbalancer.*; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Component; import org.springframework.cloud.netflix.ribbon.SpringClientFactory; import javax.annotation.PostConstruct; import javax.annotation.Resource; import java.util.Optional; /** * 服务变更监听 * * @author butterfly * @date 2023-09-24 */ @Slf4j @Component public class ServiceChangeNotifier extends Subscriber<InstancesChangeEvent> { @Resource private SpringClientFactory springClientFactory; @PostConstruct public void init() { // 注册当前自定义的订阅者以获取通知 NotifyCenter.registerSubscriber(this); } @Override public void onEvent(InstancesChangeEvent event) { String serviceName = event.getServiceName(); // 使用 dubbo 时包含 rpc 服务类会注册以 providers: 或者 consumers: 开头的服务 // 由于不是正式的服务, 这里需要进行排除, 如果未使用 dubbo 则不需要该处理 if (serviceName.contains(":")) { return; } // serviceName 格式为 groupName@@name String split = Constants.SERVICE_INFO_SPLITER; if (serviceName.contains(split)) { serviceName = serviceName.substring(serviceName.indexOf(split) + split.length()); } log.info("服务上下线: {}", serviceName); // 手动更新服务列表 // 如果自定义负载均衡方式则将默认的 ZoneAwareLoadBalancer 替换为自己的实现即可 Optional.ofNullable(springClientFactory.getLoadBalancer(serviceName)) .ifPresent(loadBalancer -> ((ZoneAwareLoadBalancer<?>) loadBalancer).updateListOfServers()); } @Override public Class<? extends Event> subscribeType() { return InstancesChangeEvent.class; } }
基于 LoadBalancer 的实现
默认情况下LoadBalancer
的缓存时间是 35s,可通过spring.cloud.loadbalancer.cache.ttl=35s
进行设置,在org.springframework.cloud.loadbalancer.cache.DefaultLoadBalancerCacheManager
类中可以看到,下面是部分代码:
public class DefaultLoadBalancerCacheManager implements LoadBalancerCacheManager { private Set<DefaultLoadBalancerCache> createCaches(String[] cacheNames, LoadBalancerCacheProperties loadBalancerCacheProperties) { // loadBalancerCacheProperties.getTtl().toMillis() 则是进行缓存的设置 return Arrays.stream(cacheNames).distinct() .map(name -> new DefaultLoadBalancerCache(name, new ConcurrentHashMapWithTimedEviction<>( loadBalancerCacheProperties.getCapacity(), new DelayedTaskEvictionScheduler<>()), loadBalancerCacheProperties.getTtl().toMillis(), false)) .collect(Collectors.toSet()); } }
同样的,通过监听Nacos
的事件,我们可以在服务上下线时使相应的缓存失效即可:
import com.alibaba.nacos.api.common.Constants; import com.alibaba.nacos.client.naming.event.InstancesChangeEvent; import com.alibaba.nacos.common.notify.NotifyCenter; import com.alibaba.nacos.common.notify.listener.Subscriber; import com.alibaba.nacos.common.notify.Event; import com.netflix.loadbalancer.*; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Component; import org.springframework.cache.Cache; import org.springframework.cache.CacheManager; import org.springframework.cloud.loadbalancer.core.CachingServiceInstanceListSupplier; import javax.annotation.PostConstruct; import javax.annotation.Resource; /** * 服务变更监听 * * @author butterfly * @date 2023-09-24 */ @Slf4j @Component public class ServiceChangeNotifier extends Subscriber<InstancesChangeEvent> { /** * 由于会有多个类型的 CacheManager bean, 这里的 defaultLoadBalancerCacheManager 名称不可修改 */ @Resource private CacheManager defaultLoadBalancerCacheManager; @PostConstruct public void init() { // 注册当前自定义的订阅者以获取通知 NotifyCenter.registerSubscriber(this); } @Override public void onEvent(InstancesChangeEvent event) { String serviceName = event.getServiceName(); // 使用 dubbo 时包含 rpc 服务类会注册以 providers: 或者 consumers: 开头的服务 // 由于不是正式的服务, 这里需要进行排除, 如果未使用 dubbo 则不需要该处理 if (serviceName.contains(":")) { return; } // serviceName 格式为 groupName@@name String split = Constants.SERVICE_INFO_SPLITER; if (serviceName.contains(split)) { serviceName = serviceName.substring(serviceName.indexOf(split) + split.length()); } log.info("服务上下线: {}", serviceName); // 手动更新服务列表 Cache cache = defaultLoadBalancerCacheManager.getCache( CachingServiceInstanceListSupplier.SERVICE_INSTANCE_CACHE_NAME); if (cache != null) { cache.evictIfPresent(serviceName); } } @Override public Class<? extends Event> subscribeType() { return InstancesChangeEvent.class; } }
参考资料
本文作者:庄周de蝴蝶
本文链接:https://www.cnblogs.com/butterfly-fish/p/17762264.html
版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 2.5 中国大陆许可协议进行许可。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步