乔拓云智能建站官网登录入口,wordpress 伪静态html,做留言的网站,四川鼎能建设集团网站文/朱季谦
Dubbo如何实现优雅下线#xff1f;
这个问题困扰了我一阵#xff0c;既然有优雅下线这种说法#xff0c;那么#xff0c;是否有非优雅下线的说法呢#xff1f;
这#xff0c;还真有。
可以从linux进程关闭说起#xff0c;其实#xff0c;我们经常使用到杀…
文/朱季谦
Dubbo如何实现优雅下线
这个问题困扰了我一阵既然有优雅下线这种说法那么是否有非优雅下线的说法呢
这还真有。
可以从linux进程关闭说起其实我们经常使用到杀进程的指令背后就涉及到是否优雅下线的理念。
在日常开发当中经常用到kill来关掉正在运行的进程可能你曾看到过一些文章是不推荐使用kill -9 pid的指令来删除进程。当执行该执行时系统会发出一个SIGKILL信号给将被关掉的进程接收到该信号的进程都立即结束运行假如此时内部仍有请求还没有执行完那怎么办你想整个进程都被立即杀死了线程作为进程里的某一部分还能活吗
打个比方假如你正在吃东西物业突然打电话给你说房子立马就要被炸掉了你必须立马关门离开这时你只能把还没吃完的饭丢下什么贵重的东西都来不及打理立马就被迫关门跑路了。
这样强制执行的后果可能就会造成一些贵重东西的丢失。
这种就属于非优雅下线简单粗暴不管三七二十一统统停止关闭。
一般而言是不推荐使用kill -9 pid来强制杀死进程。
在线上环境用到更多的是kill pid指令这个指令等同于kill -15 pid指令因此当你在网上看到一些介绍kill -15 pid指令时不用纠结好像没用到过其实就是你用到最多的kill pid指令。使用这个指令时系统会对pid进程发送一个SIGTERM信号就像给pid打了一个电话告诉他你的房子就要到期了麻烦快点清理好东西搬走。这时你仍有充裕的时间把自己的东西打包好好好清理下房间没问题了再搬出去。
换到具体程序代码中就是执行kill pid指令后该程序不会立马被强制关闭而是会接受到一个通知可以在这个通知方法内做一些清理操作若是Dubbo容器则可以关闭zookeeper注册暂停新的请求可以把已经执行一半的请求先执行完成等等。
这种下线操作就属于优雅下线。
指令kill -15 pid是操作系统级别的优雅下线操作那么在具体进程当中是如何根据SIGTERM信号来进行具体的优雅下线处理呢
在Dubbo官网上关于优雅停机的操作有相关介绍
优雅停机
Dubbo 是通过 JDK 的 ShutdownHook 来完成优雅停机的所以如果用户使用 kill -9 PID 等强制关闭指令是不会执行优雅停机的只有通过 kill PID 时才会执行。
原理
服务提供方
停止时先标记为不接收新请求新请求过来时直接报错让客户端重试其它机器。然后检测线程池中的线程是否正在运行如果有等待所有线程执行完成除非超时则强制关闭。
服务消费方
停止时不再发起新的调用请求所有新的调用在客户端即报错。然后检测有没有请求的响应还没有返回等待响应返回除非超时则强制关闭。
设置方式
设置优雅停机超时时间缺省超时时间是 10 秒如果超时则强制关闭。
# dubbo.properties
dubbo.service.shutdown.wait15000如果 ShutdownHook 不能生效可以自行调用使用tomcat等容器部署的場景建议通过扩展ContextListener等自行调用以下代码实现优雅停机
ProtocolConfig.destroyAll();根据以上信息可以得知其实Dubbo的优雅实现其实是依赖了JVM的ShutdownHook来实现的JDK提供了一个在JVM关闭时会执行的方法可以在该方法当中执行ProtocolConfig.destroyAll()来实现Dubbo的优雅停机操作而这个JDK的 ShutdownHook方法正是在系统执行kill -15 pid时会执行的方法这样我们就可以在该方法里做一些关闭前的清理工作了。
Runtime.getRuntime().addShutdownHook(new Thread(() - {ProtocolConfig.destroyAll();
}));这几行代码具体都实现了什么呢
简单而言这里通过JDK注册了一个shutdownHook钩子函数一旦应用停机就会触发该方法进而执行ProtocolConfig.destroyAll()。
这个ProtocolConfig.destroyAll()源码如下
public static void destroyAll() {//1.注销注册中心AbstractRegistryFactory.destroyAll();ExtensionLoaderProtocol loader ExtensionLoader.getExtensionLoader(Protocol.class);Iterator var1 loader.getLoadedExtensions().iterator();// 2.循环获取存活的协议while(var1.hasNext()) {String protocolName (String)var1.next();try {Protocol protocol (Protocol)loader.getLoadedExtension(protocolName);if (protocol ! null) {//关闭暴露协议protocol.destroy();}} catch (Throwable var4) {logger.warn(var4.getMessage(), var4);}
这个destroyAll()里边主要做了两件事
首先注销注册中心即断开与注册中心的连接Dubbo注册到ZK的是临时节点故而当连接断开后临时节点及底下的数据就会被自动删除关闭provider和consumer暴露的协议接口这样新的请求就无法再继续进行
下面主要按照这两个模块大体介绍下其底层逻辑
一、注销注册中心
public static void destroyAll() {if (LOGGER.isInfoEnabled()) {LOGGER.info(Close all registries getRegistries());}//加锁防止关闭多次LOCK.lock();try {Iterator var0 getRegistries().iterator();//关闭所有已创建的注册中心while(var0.hasNext()) {Registry registry (Registry)var0.next();try {registry.destroy();} catch (Throwable var6) {LOGGER.error(var6.getMessage(), var6);}}REGISTRIES.clear();} finally {//释放锁LOCK.unlock();}
}首先获取到所有的注册中心连接封装成迭代器模式
Iterator var0 getRegistries().iterator();接下来迭代获取每一个注册连接对象进行关闭
registry.destroy();该destroy方法定义在接口Node当中其具体实现将会在对应的Dubbo注册对象里
public interface Node {URL getUrl();boolean isAvailable();void destroy();
}这里Dubbo使用的注册中心是Zookeeper故而destroy会在ZookeeperRegistry类中具体实现 进入到ZookeeperRegistry类找到registry.destroy()对应的destroy()方法可以看到,调用destroy()其本质是关闭zk客户端连接当客户端关闭之后其注册到zk里的生产者或者消费者信息都会被自动删除。
public void destroy() {super.destroy();try {// 关闭zk客户端this.zkClient.close();} catch (Exception var2) {logger.warn(Failed to close zookeeper client this.getUrl() , cause: var2.getMessage(), var2);}
}在这里还有一个需要进一步研究的地方即 super.destroy()这个方法实现了什么功能呢从源码当中可以看出其有一行这样的 this.retryFuture.cancel(true)代码这行代码大概意思是将失败重试取消方式设置为true即取消了失败重试的操作我的理解是这里是关闭了失败重试可以在下线过程当中避免出现因RPC生产者接口缺少而发生反复的失败重试操作因为到这一步已经不需要再有失败重试的操作了。
public void destroy() {//移除内存中已经注册的服务取消所有服务订阅super.destroy();try {//取消失败重试this.retryFuture.cancel(true);} catch (Throwable var2) {this.logger.warn(var2.getMessage(), var2);}
}注意一点这里在取消失败重试机制之前还执行了一行 super.destroy()代码这行代码的主要功能包括两个
第一是移除内存中已经注册的服务第二是取消所有服务订阅。
我们先来看一下其方法详情
public void destroy() {if (this.logger.isInfoEnabled()) {this.logger.info(Destroy registry: this.getUrl());}// 1.移除内存中已经注册的服务SetURL destroyRegistered new HashSet(this.getRegistered());if (!destroyRegistered.isEmpty()) {Iterator var2 (new HashSet(this.getRegistered())).iterator();while(var2.hasNext()) {URL url (URL)var2.next();if (url.getParameter(dynamic, true)) {try {this.unregister(url);if (this.logger.isInfoEnabled()) {this.logger.info(Destroy unregister url url);}} catch (Throwable var10) {this.logger.warn(Failed to unregister url url to registry this.getUrl() on destroy, cause: var10.getMessage(), var10);}}}}//2.取消所有的服务订阅MapURL, SetNotifyListener destroySubscribed new HashMap(this.getSubscribed());if (!destroySubscribed.isEmpty()) {Iterator var12 destroySubscribed.entrySet().iterator();while(var12.hasNext()) {Map.EntryURL, SetNotifyListener entry (Map.Entry)var12.next();URL url (URL)entry.getKey();Iterator var6 ((Set)entry.getValue()).iterator();while(var6.hasNext()) {NotifyListener listener (NotifyListener)var6.next();try {this.unsubscribe(url, listener);if (this.logger.isInfoEnabled()) {this.logger.info(Destroy unsubscribe url url);}} catch (Throwable var9) {this.logger.warn(Failed to unsubscribe url url to registry this.getUrl() on destroy, cause: var9.getMessage(), var9);}}}}
}1.移除内存中已经注册的服务 // 1.移除内存中已经注册的服务SetURL destroyRegistered new HashSet(this.getRegistered());if (!destroyRegistered.isEmpty()) {Iterator var2 (new HashSet(this.getRegistered())).iterator();while(var2.hasNext()) {URL url (URL)var2.next();if (url.getParameter(dynamic, true)) {try {this.unregister(url);if (this.logger.isInfoEnabled()) {this.logger.info(Destroy unregister url url);}} catch (Throwable var10) {this.logger.warn(Failed to unregister url url to registry this.getUrl() on destroy, cause: var10.getMessage(), var10);}}}}这部分代码主要是将内存当中的注册信息移除这部分缓存记录是在容器启动时当向注册中心订阅成功后会同步缓存一份到内存当中。可见若注册中心挂掉了Dubbo仍然可以通过缓存获取到远程RPC服务但是无法获取到新增的RPC服务。
这里主要分析两个方法this.getRegistered()和 this.unregister(url)。
this.getRegistered()——
private final SetURL registered new ConcurrentHashSet();public SetURL getRegistered() {return this.registered;
}这是获取缓存URL的集合。
this.unregister(url)——
public void unregister(URL url) {if (url null) {throw new IllegalArgumentException(unregister url null);} else {if (this.logger.isInfoEnabled()) {this.logger.info(Unregister: url);}this.registered.remove(url);}
}这是将URL从Set集合当中移除的操作。这部分代码其实我有点想明白为何还需要从Set获取到所有URL然后再通过迭代器方式一个一个取出去进行移除直接将Set置空不是更好些吗当然这里面应该还有一些我没有考虑到的细节还有待进一步进行研究。
2.取消所有服务订阅 //2.取消所有的服务订阅MapURL, SetNotifyListener destroySubscribed new HashMap(this.getSubscribed());if (!destroySubscribed.isEmpty()) {Iterator var12 destroySubscribed.entrySet().iterator();while(var12.hasNext()) {Map.EntryURL, SetNotifyListener entry (Map.Entry)var12.next();URL url (URL)entry.getKey();Iterator var6 ((Set)entry.getValue()).iterator();while(var6.hasNext()) {NotifyListener listener (NotifyListener)var6.next();try {this.unsubscribe(url, listener);if (this.logger.isInfoEnabled()) {this.logger.info(Destroy unsubscribe url url);}} catch (Throwable var9) {this.logger.warn(Failed to unsubscribe url url to registry this.getUrl() on destroy, cause: var9.getMessage(), var9);}}}}这部分逻辑与移除内存url都很类型都是先从缓存里把所有订阅信息都取出来然后再跌代移除。 二、关闭protocol协议
这部分个关闭主要是关闭provider和consumer即对应前边提到的服务提供方会先标记不再接受新请求新请求过来直接报错然后检查线程池中的线程是否还在运行如果有等待线程完成若超时则强制关闭服务消费者则不再发起新请求同时检测看还有没有请求的响应没有返回若有等待返回若超时则强制关闭。
下面大概分析一下其源码逻辑。
protocol.destroy()其方法在接口里定义具体实现是在RegistryProtocol当中。
SPI(dubbo)
public interface Protocol {int getDefaultPort();AdaptiveT ExporterT export(InvokerT var1) throws RpcException;AdaptiveT InvokerT refer(ClassT var1, URL var2) throws RpcException;void destroy();
}RegistryProtocol的具体实现如下
public void destroy() {ListExporter? exporters new ArrayList(this.bounds.values());Iterator var2 exporters.iterator();while(var2.hasNext()) {Exporter? exporter (Exporter)var2.next();exporter.unexport();}this.bounds.clear();
}这里的核心方法是exporter.unexport()根据命名就可以推测出大概就是说不暴露对外接口协议的方法也就是关闭那些对外暴露的服务。
该exporter.unexport()方法具体实现有两类一个是DubboExporter一个是AbstractExporter这里主要分析下AbstractExporter里面的逻辑。
AbstractExporter内部关于unexport()的方法如下
public void unexport() {if (!this.unexported) {this.unexported true;this.getInvoker().destroy();}
} this.getInvoker().destroy()的实现如下
public void destroy() {Iterator var1 (new ArrayList(this.serverMap.keySet())).iterator();String key;//关停所有的Serverprovider不再接收新的请求while(var1.hasNext()) {key (String)var1.next();ExchangeServer server (ExchangeServer)this.serverMap.remove(key);if (server ! null) {try {if (this.logger.isInfoEnabled()) {this.logger.info(Close dubbo server: server.getLocalAddress());}// HeaderExchangeServer中会停止发送心态的任务关闭channelserver.close(getServerShutdownTimeout());} catch (Throwable var7) {this.logger.warn(var7.getMessage(), var7);}}}var1 (new ArrayList(this.referenceClientMap.keySet())).iterator();ExchangeClient client;//关停所有Clientconsumer将不再发送新的请求while(var1.hasNext()) {key (String)var1.next();client (ExchangeClient)this.referenceClientMap.remove(key);if (client ! null) {try {if (this.logger.isInfoEnabled()) {this.logger.info(Close dubbo connect: client.getLocalAddress() -- client.getRemoteAddress());}// HeaderExchangeClient中会停止发送心态的任务关闭channelclient.close();} catch (Throwable var6) {this.logger.warn(var6.getMessage(), var6);}}}
......
}总结一下Dubbo的优雅下线若是通过JDK的shutdownHook来完成优雅停机的这时当用户对该Dubbo进行执行kill pid后在关闭JVM时会发起一个线程执行ShutdownHook进而执行 ProtocolConfig.destroyAll()方法该方法在关掉进行前主要做了以下一些清理工作
1、关闭zk客户端
2、 客户端断开ZK连接后ZK会自动删除临时注册节点
3、 取消重试机制
4 、清除内存中已经注册的服务
5、 取消所有的服务订阅
6、关闭provider和consumer停止新的请求
后面还有一步没分析到是若仍有在执行的线程会等待其执行完成。
最后在清理完一系列工作后就可以关闭该进程了。
这就是Dubbo的优雅下线大概的原理。