dubbo 優雅停機原理

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關閉鉤子時存在的問題。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章