5.6实现自动注销和负载均衡策略
5.6.1服务自动注销
上一节我们实现了服务的自动注册和发现,但是有些细心的同学就可能会发现,如果你启动完成服务端后把服务端给关闭了,并不会自动地注销 Nacos 中对应的服务信息,这样就导致了当客户端再次向 Nacos 请求服务时,会获取到已经关闭的服务端信息,最终就有可能因为连接不到服务器而调用失败。
那么我们就需要一种办法,在服务端关闭之前自动向 Nacos 注销服务。但是有一个问题,我们不知道什么时候服务器会关闭,也就不知道这个方法调用的时机,就没有办法手工去调用。这时,我们就需要钩子。
钩子是什么呢?是在某些事件发生后自动去调用的方法。那么我们只需要把注销服务的方法写到关闭系统的钩子方法里就行了。
首先先写向 Nacos 注销所有服务的方法,这部分被放在了 NacosUtils 中作为一个静态方法,NacosUtils 是一个 Nacos 相关的工具类:
public static void clearRegistry() { if(!serviceNames.isEmpty() && address != null) { String host = address.getHostName(); int port = address.getPort(); Iterator<String> iterator = serviceNames.iterator(); while(iterator.hasNext()) { String serviceName = iterator.next(); try { namingService.deregisterInstance(serviceName, host, port); } catch (NacosException e) { logger.error("注销服务 {} 失败", serviceName, e); } } } }
所有的服务名称都被存储在 NacosUtils 类中的 serviceNames 中,在注销时只需要用迭代器迭代所有服务名,调用 deregisterInstance 注销服务。
接着就是钩子了,新建一个类,ShutdownHook:
public class ShutdownHook { private static final Logger logger = LoggerFactory.getLogger(ShutdownHook.class); private final ExecutorService threadPool = ThreadPoolFactory.createDefaultThreadPool("shutdown-hook"); private static final ShutdownHook shutdownHook = new ShutdownHook(); public static ShutdownHook getShutdownHook() { return shutdownHook; } public void addClearAllHook() { logger.info("关闭后将自动注销所有服务"); Runtime.getRuntime().addShutdownHook(new Thread(() -> { NacosUtil.clearRegistry(); threadPool.shutdown(); })); } }
使用了单例模式创建其对象,在 addClearAllHook 中,Runtime 对象是 JVM 虚拟机的运行时环境,调用其 addShutdownHook 方法增加一个钩子函数,创建一个新线程调用 clearRegistry 方法完成注销工作。这个钩子函数会在 JVM 关闭之前被调用。
这样在 RpcServer 启动之前,只需要调用 addClearAllHook,就可以注册这个钩子了。例如在 NettyServer 中:
ChannelFuture future = serverBootstrap.bind(host, port).sync(); + ShutdownHook.getShutdownHook().addClearAllHook(); future.channel().closeFuture().sync();
启动服务端后再关闭,就会发现 Nacos 中的注册信息都被注销了。
5.6.2负载均衡策略
负载均衡大家应该都熟悉,在上一节中客户端在 lookupService 方法中,从 Nacos 获取到的是所有提供这个服务的服务端信息列表,我们就需要从中选择一个,这便涉及到客户端侧的负载均衡策略。我们新建一个接口:LoadBalancer
public interface LoadBalancer { Instance select(List<Instance> instances); }
接口中的 select 方法用于从一系列 Instance 中选择一个。这里我就实现两个比较经典的算法:随机和转轮。
随机算法顾名思义,就是随机选一个,毫无技术含量:
public class RandomLoadBalancer implements LoadBalancer { @Override public Instance select(List<Instance> instances) { // nextInt():生成一个介于[0, instances.size())的int型随机值 return instances.get(new Random().nextInt(instances.size())); } }
而转轮算法大家也应该了解,按照顺序依次选择第一个、第二个、第三个……这里就需要一个变量来表示当前选到了第几个:
public class RoundRobinLoadBalancer implements LoadBalancer { private int index = 0; @Override public Instance select(List<Instance> instances) { if(index >= instances.size()) { index %= instances.size(); } return instances.get(index++); } }
index 就表示当前选到了第几个服务器,并且每次选择后都会自增一。
最后在 NacosServiceRegistry 中集成就可以了,这里选择外部传入的方式传入 LoadBalancer:
public class NacosServiceDiscovery implements ServiceDiscovery { private final LoadBalancer loadBalancer; public NacosServiceDiscovery(LoadBalancer loadBalancer) { if(loadBalancer == null) this.loadBalancer = new RandomLoadBalancer(); else this.loadBalancer = loadBalancer; } public InetSocketAddress lookupService(String serviceName) { try { List<Instance> instances = NacosUtil.getAllInstance(serviceName); Instance instance = loadBalancer.select(instances); return new InetSocketAddress(instance.getIp(), instance.getPort()); } catch (NacosException e) { logger.error("获取服务时有错误发生:", e); } return null; } }
而这个负载均衡策略,也可以在创建客户端时指定,例如无参构造 NettyClient 时就用默认的策略,也可以有参构造传入策略,具体的实现留给大家。