服务重启时带来的问题 

        项目分布式服务场景中,系统之间通过RPC服务方式进行交互。经常在服务提供方provider服务重启或者发布的过程中,如果此时业务正处于高峰期,就会有大量的RPC调用失败。如果consumer侧没有重试机制就会发生业务异常。

        本文基于业务流程的角度,记录一下dubbo启动、下线过程。后面会给予源码的维度,详细记录启动、下线过程。

dubbo启动

         对于provider,dubbo会监听Spring容器启动的刷新事件(ContextRefreshedEvent),调用Export暴露服务。

provider

URL装配:读取provider配置,封装URL。协议暴露:创建NettyServer,为URL创建一个本地方法的代理,并将二者映射。NettyServer接受请求,就会调用对应的本地方法向注册中心注册:将包装好的URL信息,注册到注册中心,完成服务暴露

consumer

想注册中心注册Consummer信息创建监听器,订阅provider节点的信息变化。

更新内存Provider列表:provider上下线,引起zk节点变化。监听器感知变化后,会调用NotifyListener.notify方法,更新内存provider列表。更新本地文件缓存:consumer还会讲最新的provider列表写入到~/.dubbo的文件目录下,这保证Zk挂掉的情况下,consumer依然能够通过本地缓存文件找到provider地址。

dubbo下线

        服务下线过程中,有两处代码来处理dubbo下线。

 ServiceBean的Destory,由Spring销毁Bean的时候调用。AbstractConfig中的DubboShutdownHook,是JVM退出时的钩子线程,在JVM退出之前执行。

ServiceBean.Destory

provider

        删除zk中的provider节点信息

        对于provider来讲,就是删除zk中的provider节点。这样consumer监听到后,就会删除内存和本地文件中的provider列表,新的RPC就不会调用删除的provider了。

consumer

        destroy方法是上面订阅的逆过程

关闭监听器删除zk中的consumer节点信息

AbstractConfig的DubboShutdownHook

        AbstractConfig类中静态代码块,然后将DubboShutdownHook类,注册到虚拟机关闭钩子中,当虚拟机关闭时,就会调用对应的钩类。

Java虚拟机会关闭以响应两种类型的事件:

当最后一个非守护进程线程退出或调用exit(相当于System.exit)方法时,程序正常退出,或者

虚拟机在响应用户中断(如键入^C)或系统范围事件(如用户注销或系统关闭)时终止。

static {

legacyProperties.put("dubbo.protocol.name", "dubbo.service.protocol");

legacyProperties.put("dubbo.protocol.host", "dubbo.service.server.host");

legacyProperties.put("dubbo.protocol.port", "dubbo.service.server.port");

legacyProperties.put("dubbo.protocol.threads", "dubbo.service.max.thread.pool.size");

legacyProperties.put("dubbo.consumer.timeout", "dubbo.service.invoke.timeout");

legacyProperties.put("dubbo.consumer.retries", "dubbo.service.max.retry.providers");

legacyProperties.put("dubbo.consumer.check", "dubbo.service.allow.no.provider");

legacyProperties.put("dubbo.service.url", "dubbo.service.address");

DubboShutdownHook.getDubboShutdownHook().register();

}

public void register() {

if (!this.registered.get() && this.registered.compareAndSet(false, true)) {

Runtime.getRuntime().addShutdownHook(getDubboShutdownHook());

}

}

        当JVM关闭时,回调用DubboShutdownHook类中的doDestroy-》调用AbstractRegistryFactroy.destroyAll()【调用zkClient的关闭方法】-》调用destroyProtocols方法,遍历所有Protocol协议类,调用Protocol的destroy方法。

private void destroyProtocols() {

ExtensionLoader loader = ExtensionLoader.getExtensionLoader(Protocol.class);

Iterator var2 = loader.getLoadedExtensions().iterator();

while(var2.hasNext()) {

String protocolName = (String)var2.next();

try {

Protocol protocol = (Protocol)loader.getLoadedExtension(protocolName);

if (protocol != null) {

protocol.destroy();

}

} catch (Throwable var5) {

logger.warn(var5.getMessage(), var5);

}

}

}

        因为我们使用Dubbo协议,DubboProtocol的destroy方法如下

public void destroy() {

Iterator var1 = (new ArrayList(this.serverMap.keySet())).iterator();

String key;

//循环遍历server,调用server.close。关闭provider

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());

}

server.close(ConfigUtils.getServerShutdownTimeout());

} catch (Throwable var7) {

this.logger.warn(var7.getMessage(), var7);

}

}

}

var1 = (new ArrayList(this.referenceClientMap.keySet())).iterator();

ExchangeClient client;

//循环遍历consumer,调用client.close。关闭consumer

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());

}

client.close(ConfigUtils.getServerShutdownTimeout());

} catch (Throwable var6) {

this.logger.warn(var6.getMessage(), var6);

}

}

}

var1 = (new ArrayList(this.ghostClientMap.keySet())).iterator();

while(var1.hasNext()) {

key = (String)var1.next();

client = (ExchangeClient)this.ghostClientMap.remove(key);

if (client != null) {

try {

if (this.logger.isInfoEnabled()) {

this.logger.info("Close dubbo connect: " + client.getLocalAddress() + "-->" + client.getRemoteAddress());

}

client.close(ConfigUtils.getServerShutdownTimeout());

} catch (Throwable var5) {

this.logger.warn(var5.getMessage(), var5);

}

}

}

this.stubServiceMethodsMap.clear();

super.destroy();

}

        DubboProtocol的destroy():先关闭provider,再关闭consumer,如果先关闭consumer后关闭provider,那么上游服务的请求依然能够被provider处理,如果provider依赖consummer,会导致调用链路失败。

关闭Provider代码:

public void close(int timeout) {

this.startClose();

if (timeout > 0) {

long max = (long)timeout;

long start = System.currentTimeMillis();

if (this.getUrl().getParameter("channel.readonly.send", true)) {

//发送readonly信号

this.sendChannelReadOnlyEvent();

}

//等待任务正在运行的任务执行完成

while(this.isRunning() && System.currentTimeMillis() - start < max) {

try {

Thread.sleep(10L);

} catch (InterruptedException var7) {

this.logger.warn(var7.getMessage(), var7);

}

}

}

//停止与consumer的心跳

this.doClose();

//关闭NettyServer

this.server.close(timeout);

}

关闭Consumer代码:

public void close(int timeout) {

this.startClose();

//停止与provider心跳

this.doClose();

//关闭NettyClient

this.channel.close(timeout);

}

具体步骤如下

遍历关闭provider:

发送readonly信号:调用HeaderExchangerServer.sendChannelReadOnlyEvent(),向consumer发送readonly信号,目的告诉consumer不要想我发送请求。由于consumer在Zk挂掉的情况下依然可以读取本地的provider,readonly信号的存在为consumer提供了另一种剔除provider的方式等待正在运行的任务执行完毕或者超时:while循环,等待正在执行的任务完成或超时停止与consumer的心跳关闭NettyServer.遍历关闭consumer:

停止与provider心跳关闭NettyClient

优雅下线

下线问题

        2.4.9版本中,NettyClient 中创建ChannelFactory构造器中,创建了一个HashEdWheelTimer的非Daemon线程。在dubbo销毁过程中,没有显示释放HashedWheelTimer线程,导致Jvm无法正常退出,导致DubboShutdown没有被执行。

        总而言之,再JVM停止时,没有执行到DubboShutdownHook,优雅下线根本没有执行。导致没有按照dubbo涉及所期待的那样与运行。

优雅下线方法

方法1

        Provider摘除节点后,consumer收到通知更新provider列表这两步并不是同步的原子操作。可能Provider摘除节点,检测没有进行中的调用后,立马关闭服务,consumer还未来得及更新provider列表。导致上游consumer调用失败。

        provider摘除节点后,需要给consumer足够的时间更新服务列表。简单的解决方式provider摘除ZK节点之后,销毁协议之前,主动sleep一段时间。从而减少consumer调用失败的概率。

方法2

        Provider对外关闭暴露,并且已有任务执行成功后。不应该里面关闭consumer,即client.close ()。考虑到业务中有异步或者定时任务调用consumer,立即关闭可能导致这部分业务失败。所以可以在调用client.close()增加等待时间。

相关阅读

评论可见,请评论后查看内容,谢谢!!!
 您阅读本篇文章共花了: