一文聊透 Dubbo 优雅停机( 二 )


public interface Protocol { <T> Exporter<T> export(Invoker<T> invoker) throws RpcException; <T> Invoker<T> refer(Class<T> type, URL url) throws RpcException; void destroy();}它定义了暴露、订阅、注销这三个生命周期方法,所以不难理解为什么 Dubbo 会把 shutdown hook 触发后的注销方法定义在 ProtocolConfig 中了 。
回到 ProtocolConfig 的源码中,我把 ProtocolConfig 中执行的优雅停机逻辑分成了三部分,其中第 1,2 部分和注册中心(Registry)相关,第 3 部分和协议/流程(Protocol)相关,分成下面的 3.3 和 3.4 两部分来介绍 。
3.3 注册中心注销逻辑
public abstract class AbstractRegistryFactory implements RegistryFactory { public static void destroyAll() { LOCK.lock(); try { for (Registry registry : getRegistries()) { try { registry.destroy(); } catch (Throwable e) { LOGGER.error(e.getMessage(), e); } } REGISTRIES.clear(); } finally { // Release the lock LOCK.unlock(); } }}【一文聊透 Dubbo 优雅停机】这段代码对应了 3.2 小节 ProtocolConfig 源码的第 1 部分,代表了注册中心的注销逻辑,更深一层的源码不需要 debug 进去了,大致的逻辑就是删除掉注册中心中本节点对应的服务提供者地址 。
// Wait for registry notificationtry { Thread.sleep(ConfigUtils.getServerShutdownTimeout());} catch (InterruptedException e) { logger.warn("Interrupted unexpectedly when waiting for registry notification during shutdown process!");}这段代码对应了 3.2 小节 ProtocolConfig 源码的第 2 部分,ConfigUtils.getServerShutdownTimeout() 默认值是 10s,为什么需要在 shutdown hook 中等待 10s 呢?在注释中可以发现这段代码的端倪,原来是为了给服务消费者一点时间,确保等到注册中心的通知 。10s 显然是一个经验值,这里也不妨和大家探讨一下,如何稳妥地设置这个值呢?

  • 设置的过短 。由于注册中心通知消费者取消订阅某个地址是异步通知过去的,可能消费者还没收到通知,提供者这边就停机了,这就违背了我们的诉求 1:服务消费者不应该请求到已经下线的服务提供者 。
  • 设置的过长 。这会导致发布时间变长,带来不必要的等待 。
两个情况对比下,起码可以得出一个实践经验:如果拿捏不准等待时间,尽量设置一个宽松的一点的等待时间 。
这个值主要取决三点因素:
  • 集群规模的大小 。如果只有几个服务,每个服务只有几个实例,那么再弱鸡的注册中心也能很快的下发通知 。
  • 注册中心的选型 。以 Naocs 和 Zookeeper 为例,同等规模服务实例下 Nacos 在推送地址方面的能力远超 Zookeeper 。
  • 网络状况 。服务提供者和服务消费者与注册中心的交互逻辑走的 TCP 通信,网络状况也会影响到推送时间 。
所以需要根据实际部署场景测量出最合适的值 。
3.4 协议/流程注销逻辑
ExtensionLoader<Protocol> loader = ExtensionLoader.getExtensionLoader(Protocol.class);for (String protocolName : loader.getLoadedExtensions()) { try { Protocol protocol = loader.getLoadedExtension(protocolName); if (protocol != null) { protocol.destroy(); } } catch (Throwable t) { logger.warn(t.getMessage(), t); }}这段代码对应了 3.2 小节 ProtocolConfig 源码的第 3 部分,在运行时,loader.getLoadedExtension(protocolName) 这段代码会加载到两个协议 :DubboProtocol 和 Injvm。后者 Injvm 实在没啥好讲的,主要来分析一下 DubboProtocol 的逻辑 。
DubboProtocol 实现了我们前面提到的 Protocol 接口,它的 destory 方法是我们重点要看的 。
public class DubboProtocol extends AbstractProtocol { public void destroy() { for (String key : new ArrayList<String>(serverMap.keySet())) { ExchangeServer server = serverMap.remove(key); if (server != null) { server.close(ConfigUtils.getServerShutdownTimeout()); } } for (String key : new ArrayList<String>(referenceClientMap.keySet())) { ExchangeClient client = referenceClientMap.remove(key); if (client != null) { client.close(ConfigUtils.getServerShutdownTimeout()); } } for (String key : new ArrayList<String>(ghostClientMap.keySet())) { ExchangeClient client = ghostClientMap.remove(key); if (client != null) { client.close(ConfigUtils.getServerShutdownTimeout()); } } stubServiceMethodsMap.clear(); super.destroy(); }}主要分成了两部分注销逻辑:server 和 client,注意这里是先注销了服务提供者后,再注销了服务消费者,这样做是有意为之 。在 RPC 调用中,经常是一个远程调用触发一个远程调用,所以在关闭一个节点时,应该先切断上游的流量,所以这里是先注销了服务提供者,这样从一定程度上,降低了后面服务消费者被调用到的可能性(当然,服务消费者也有可能被单独调用到) 。由于 server 和 client 的流程类似,所以我只选取了 server 部分来分析具体的注销逻辑 。


推荐阅读