服务重启时带来的问题
项目分布式服务场景中,系统之间通过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
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()增加等待时间。
相关阅读
发表评论