dubbo 优雅停机原理

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接: https://blog.csdn.net/liujunzxcv/article/details/102751555

dubbo优雅停机的实现,首先主要依赖于jvm的ShutdownHook钩子函数,例如dubbo 2.5.x版本,在AbstractConfig中定义了:

static {
        Runtime.getRuntime().addShutdownHook(new Thread(new Runnable() {
            public void run() {
                if (logger.isInfoEnabled()) {
                    logger.info("Run shutdown hook now.");
                }
                ProtocolConfig.destroyAll();
            }
        }, "DubboShutdownHook"));
    }

在静态块里面注册了一个关闭钩子,当jvm准备关闭时(tomcat shutdown命令、kill pid等),会自动触发注册好的关闭钩子,执行ProtocolConfig.destroyAll()方法,此方法主要做了从注册中心(比如zookeeper)解注册、关闭provider、关闭consumer三件事情。

在这里插入图片描述

1、第一步解注册,consumer和provider都会在zookeeper注册临时节点,在停机时首先调用了AbstractRegistryFactory的destroyAll():

public static void destroyAll() {
        if (LOGGER.isInfoEnabled()) {
            LOGGER.info("Close all registries " + getRegistries());
        }
        // Lock up the registry shutdown process
        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();
        }
    }

在该方法中首先拿到本机所有的registry(dubbo官方的角色说明:registry是服务注册与发现的注册中心。为了便于理解,registry可以不严谨地看做本机整体服务注册到注册中心的信息),然后调用registry的destroy()方法执行解注册。值得一提的是,dubbo里面使用了很多设计模式,整个注册中心的逻辑部分使用了模板模式:

在这里插入图片描述

比如我们使用的zookeeper,那么会来到ZookeeperRegistry.destroy(),

@Override
    public void destroy() {
        super.destroy();
        try {
            zkClient.close();
        } catch (Exception e) {
            logger.warn("Failed to close zookeeper client " + getUrl() + ", cause: " + e.getMessage(), e);
        }
    }

里面依次调用其父类FailbackRegistry及其祖父类AbstractRegistry.destroy(),在其中会执行

   for (URL url : new HashSet<>(getRegistered())) {
                if (url.getParameter(DYNAMIC_KEY, true)) {
                    try {
                        unregister(url);
                        if (logger.isInfoEnabled()) {
                            logger.info("Destroy unregister url " + url);
                        }
                    } catch (Throwable t) {
                        logger.warn("Failed to unregister url " + url + " to registry " + getUrl() + " on destroy, cause: " + t.getMessage(), t);
                    }
                }
            }

这么做是为了在虚基类中执行一些公共处理,不依赖于具体注册中心,比如将 private final Set <URL> registered ,注册URL Set中对应的URL(provider和consumer)删掉。
以及在ZookeeperRegistry中:

@Override
    public void doUnregister(URL url) {
        try {
            zkClient.delete(toUrlPath(url));
        } catch (Throwable e) {
            throw new RpcException("Failed to unregister " + url + " to zookeeper " + getUrl() + ", cause: " + e.getMessage(), e);
        }
    }

删掉在zookeeper中添加的节点。以及前面提到的zkClient.close();关闭zk客户端。
解注册的过程基本如此。

2、从注册中心解注册之后,本机跟注册中心之间的连接就断开了,接下来销毁所有的protocol。protocol属于远程调用层,封装 RPC 调用。

private void destroyProtocols() {
		//获取Protocol的扩展点实现类
        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);
            }
        }
    }

通过循环遍历得到所有protocol扩展点实现,包括实际外部通信协议(这里是dubbo)、injvm(本机通信)、registry protocol,这里只重点关注dubbo protocol,来到DubboProtocol.destroy():

public void destroy() {
        for (String key : new ArrayList<String>(serverMap.keySet())) {
            ExchangeServer server = serverMap.remove(key);
            if (server != null) {
                try {
                    if (logger.isInfoEnabled()) {
                        logger.info("Close dubbo server: " + server.getLocalAddress());
                    }
                    server.close(getServerShutdownTimeout());
                } catch (Throwable t) {
                    logger.warn(t.getMessage(), t);
                }
            }
        }

        for (String key : new ArrayList<String>(referenceClientMap.keySet())) {
            ExchangeClient client = referenceClientMap.remove(key);
            if (client != null) {
                try {
                    if (logger.isInfoEnabled()) {
                        logger.info("Close dubbo connect: " + client.getLocalAddress() + "-->" + client.getRemoteAddress());
                    }
                    client.close(getServerShutdownTimeout());
                } catch (Throwable t) {
                    logger.warn(t.getMessage(), t);
                }
            }
        }

        for (String key : new ArrayList<String>(ghostClientMap.keySet())) {
            ExchangeClient client = ghostClientMap.remove(key);
            if (client != null) {
                try {
                    if (logger.isInfoEnabled()) {
                        logger.info("Close dubbo connect: " + client.getLocalAddress() + "-->" + client.getRemoteAddress());
                    }
                    client.close(getServerShutdownTimeout());
                } catch (Throwable t) {
                    logger.warn(t.getMessage(), t);
                }
            }
        }
        stubServiceMethodsMap.clear();
        super.destroy();
    }

这里首先做的服务端的关闭,调用ExchangeServer接口的实现类HeaderExchangeServer

在这里插入图片描述

public void close(final int timeout) {
        startClose();
        if (timeout > 0) {
            final long max = (long) timeout;
            final long start = System.currentTimeMillis();
            if (getUrl().getParameter(Constants.CHANNEL_SEND_READONLYEVENT_KEY, true)) {
                logger.info("============getUrl():"  + getUrl());
                sendChannelReadOnlyEvent();
            }
            while (HeaderExchangeServer.this.isRunning()
                    && System.currentTimeMillis() - start < max) {
                try {
                    Thread.sleep(10);
                } catch (InterruptedException e) {
                    logger.warn(e.getMessage(), e);
                }
            }
        }
        doClose();
        server.close(timeout);
    }

如果设置了发送readonly事件,则先执行sendChannelReadOnlyEvent();这是一个单向请求,由服务端发给消费端,告诉consumer我要关闭channel了,只能从channle中读未读取完的内容。
HeaderExchangeServer.this.isRunning()判断是否还有客户端持有连接,如果有的话sleep 10ms然后再一直尝试直到能够关闭(此处2.5.x版本存在bug,导致不能正确判断客户端持有连接,因此2.5版本里面需要在解注册后sleep 10s后才执行protocol的关闭)。

在 doClose()里面:

private void doClose() {
        if (!closed.compareAndSet(false, true)) {
            return;
        }
        stopHeartbeatTimer();
        try {
            scheduled.shutdown();
        } catch (Throwable t) {
            logger.warn(t.getMessage(), t);
        }
    }

执行关闭服务端客户端之间TCP长连接的心跳检测。

在server.close(timeout);方法:

在这里插入图片描述

仍然使用模板模式,首先AbstractServer关闭服务线程池:

public void close(int timeout) {
        ExecutorUtil.gracefulShutdown(executor, timeout);
        close();
    }


public void close() {
        if (logger.isInfoEnabled()) {
            logger.info("Close " + getClass().getSimpleName() + " bind " + getBindAddress() + ", export " + getLocalAddress());
        }
        ExecutorUtil.shutdownNow(executor, 100);
        try {
            super.close();
        } catch (Throwable e) {
            logger.warn(e.getMessage(), e);
        }
        try {
            doClose();
        } catch (Throwable e) {
            logger.warn(e.getMessage(), e);
        }
    }

然后doClose();到实际的实现类(比如NettyServer)关闭了实际建立的连接。

到这里服务端的关闭完成,接下来关闭客户端,由于客户端与服务端关闭十分类似,不再赘述。

总结

整个dubbo服务关闭的过程,可大致归纳为:

  • jvm关闭,关闭钩子调用dubbo关闭服务
  • 注册中心解注册,删除consumer和provider节点
  • 服务端关闭
  • protocol的注销
  • 发送readonly
  • 停止与客户端长连接心跳检测
  • 服务线程池关闭
  • NettyServer关闭

待续:dubbo注册jvm关闭钩子时存在的问题。

猜你喜欢

转载自blog.csdn.net/liujunzxcv/article/details/102751555