Dubbo優雅なダウンタイムの進化の道

12621 ワード

一、前言
『ShutdownHook-Javaエレガントダウンタイムソリューション』では、Javaがエレガントダウンタイムを実現する原理について説明します.次に、上記の知識点に基づいて、Dubboの内部に深く入り込み、Dubboがどのように優雅な停止を実現するかを理解します.
二、Dubboの優雅な停止問題解決すべき問題
優雅なダウンタイムを実現するために、Dubboはいくつかの問題を解決する必要があります.
  • 新しいリクエストは、ダウンタイム中のDubboサービスプロバイダに送信できません.
  • サービスプロバイダを閉鎖し、サービス要求を受信した場合、サービスをオフラインにするには、処理が完了する必要があります.
  • サービス消費者が閉鎖されると、すでに発行されたサービス要求は、応答の返信を待つ必要がある.

  • 以上の3つの問題を解決してこそ、ダウンタイムが業務に与える影響を最小限に抑え、優雅なダウンタイムを実現することができます.
    三、2.5.X
    Dubboの優雅な停止は2.5.Xバージョンで比較的完全に実現され、このバージョンの実現は比較的簡単で、比較的理解しやすい.まずDubbo 2.5.Xバージョンのソースコードをベースに、Dubboが優雅なダウンタイムを実現する方法を見てみましょう.
    3.1、優雅なダウンタイムの全体実現方案
    エレガントダウンタイムエントリクラスはAbstractConfig静的コードにあり、ソースコードは以下の通りである.
    static {
        Runtime.getRuntime().addShutdownHook(new Thread(new Runnable() {
            public void run() {
                if (logger.isInfoEnabled()) {
                    logger.info("Run shutdown hook now.");
                }
                ProtocolConfig.destroyAll();
            }
        }, "DubboShutdownHook"));
    }

    ここでは、ShutdownHookが登録され、ダウンタイムが適用されると、ProtocolConfig.destroyAll()が呼び出されます.ProtocolConfig.destroyAll() ソースコードは次のとおりです.
    public static void destroyAll() {
        //       
        if (!destroyed.compareAndSet(false, true)) {
            return;
        }
        //        
        AbstractRegistryFactory.destroyAll();
    
        // Wait for registry notification
        try {
            Thread.sleep(ConfigUtils.getServerShutdownTimeout());
        } catch (InterruptedException e) {
            logger.warn("Interrupted unexpectedly when waiting for registry notification during shutdown process!");
        }
    
        ExtensionLoader loader = ExtensionLoader.getExtensionLoader(Protocol.class);
        //     Protocol
        for (String protocolName : loader.getLoadedExtensions()) {
            try {
                Protocol protocol = loader.getLoadedExtension(protocolName);
                if (protocol != null) {
                    protocol.destroy();
                }
            } catch (Throwable t) {
                logger.warn(t.getMessage(), t);
            }
        }
        }

    上から見ると、Dubboの優雅な停止は主に2つのステップに分かれています.
  • 登録抹消センター
  • すべてのProtocol
  • をログアウト
    3.2、登録センターを抹消する
    登録抹消センターのソースコードは以下の通りです.
    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();
        }
    }

    このメソッドは、内部生成レジストリ・センター・サービスをログアウトします.登録センターの内部ロジックをログアウトするのは簡単で、ここではソースコードに深く入り込まず、直接画像で展示します.
    ps:ソースコード:AbstractRegistry
    ZKの場合、Dubboは対応するサービスノードを削除し、サブスクリプションをキャンセルします.ZKノード情報の変更により、ZKサービス側はdubbo消費者にこのサービスノードをオフラインするように通知し、最後にサービスを閉じてZKに接続する.
    登録センターを通じて、Dubboは直ちに消費者にオフラインサービスを通知することができて、新しい要求もオフラインのノードに送らないで、上述の第1の問題を解決します:新しい要求は更に停止しているDubboサービスプロバイダに送ることができません.
    しかし、ここでは、ネットワークの分離により、ZKサービス側とDubboとの接続に一定の遅延が生じる可能性があり、ZK通知は消費側に最初の時間に通知できない可能性があるという弊害がある.このような状況を考慮すると、登録センターをログアウトした後、待機進数を追加します.コードは次のとおりです.
    // Wait for registry notification
    try {
        Thread.sleep(ConfigUtils.getServerShutdownTimeout());
    } catch (InterruptedException e) {
        logger.warn("Interrupted unexpectedly when waiting for registry notification during shutdown process!");
    }

    デフォルト待ち時間は10000 msであり、dubbo.service.shutdown.waitを設定することでデフォルトパラメータを上書きできます.10 sはただの経験値で、実際の状況に応じて設定することができます.しかし、この待ち時間の設定にはこだわりがあり、短すぎると消費側がZKの通知を受け取っていないため、プロバイダが停止します.あまり長く設定することもできません.長すぎると、アプリケーションを停止する時間が長くなり、パブリケーション体験に影響します.
    3.3、Protocolを抹消する
    ExtensionLoader 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);
        }
    }
    loader#getLoadedExtensionsは、ProtocolDubboProtocolの2つのサブクラスを返します.InjvmProtocolはサービス側要求と対話し、DubboProtocolは内部要求対話に用いられる.アプリケーション呼び出しが自分でDubboサービスを提供する場合、ネットワーク呼び出しは実行されず、内部メソッドを直接実行します.
    ここでは主にInjvmProtocolの内部論理を分析します.DubboProtocolソース:
    public void destroy() {
        //    Server
        for (String key : new ArrayList(serverMap.keySet())) {
            ExchangeServer server = serverMap.remove(key);
            if (server != null) {
                try {
                    if (logger.isInfoEnabled()) {
                        logger.info("Close dubbo server: " + server.getLocalAddress());
                    }
                    server.close(ConfigUtils.getServerShutdownTimeout());
                } catch (Throwable t) {
                    logger.warn(t.getMessage(), t);
                }
            }
        }
        //    Client
        for (String key : new ArrayList(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(ConfigUtils.getServerShutdownTimeout());
                } catch (Throwable t) {
                    logger.warn(t.getMessage(), t);
                }
            }
        }
    
        for (String key : new ArrayList(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(ConfigUtils.getServerShutdownTimeout());
                } catch (Throwable t) {
                    logger.warn(t.getMessage(), t);
                }
            }
        }
        stubServiceMethodsMap.clear();
        super.destroy();
    }

    Dubboのデフォルトでは、Nettyが最下位の通信フレームワークとして使用され、DubboProtocol#destroyServerに分かれています.Clientは、他の消費者Serverからの要求を受信するために使用される.
    上記のソースコードでは、まずClientを閉じ、新しい要求の受信を停止した後、Serverを閉じる.これにより、サービスが消費者に呼び出される可能性が低減される.
    3.4、サーバーを閉じる
    まず、Clientが呼び出されます.ソースコードは次のとおりです.
    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)) {
           //    READ_ONLY   
                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);
    }
    
    private void doClose() {
        if (!closed.compareAndSet(false, true)) {
            return;
        }
        stopHeartbeatTimer();
        try {
            scheduled.shutdown();
        } catch (Throwable t) {
            logger.warn(t.getMessage(), t);
        }
    }

    ここでは、HeaderExchangeServer#closeイベントがサービス消費者に送信される.消費者が受け入れた後、このノードを自発的に排除し、他の正常なノードに要求を送信します.これにより、登録センターの通知遅延による影響がさらに低減される.
    次に、ハートビート検出をオフにし、下位通信フレームワークNettyServerをオフにします.ここではREAD_ONLYメソッドが呼び出され、このメソッドは実際にNettyServer#closeで実装される.AbstractServerソースコードは次のとおりです.
    public void close(int timeout) {
        ExecutorUtil.gracefulShutdown(executor, timeout);
        close();
    }

    ここでは、まずビジネス・スレッド・プールを閉じます.このプロセスでは、スレッド・プール内のタスクをできるだけ実行し、スレッド・プールを閉じ、最後にNetty通信の最下位サーバを閉じます.
    Dubboのデフォルトでは、ビジネス・スレッド・プールにリクエスト/ハートビートなどのリクエストが送信されます.
    サーバを閉じ、スレッドプールが閉じるのを優雅に待つことで、上記の2つ目の問題を解決しました.サービスプロバイダを閉じると、サービスリクエストが受信され、処理が完了してからサービスをオフにする必要があります.
    Dubboサービスプロバイダのクローズプロセスを図に示す.
    ps:ソースコードのデバッグを容易にするために、Serverクローズコールを添付します.
    DubboProtocol#destroy
        ->HeaderExchangeServer#close
            ->AbstractServer#close
                ->NettyServer#doClose                

    3.5 Clientを閉じる
    Clientクローズ方式はほぼサーバと同じで、ここでは主に処理が要求ロジックを発行したことを紹介し、コードはAbstractServer#closeにある.
    // graceful close
    public void close(int timeout) {
        if (closed) {
            return;
        }
        closed = true;
        if (timeout > 0) {
            long start = System.currentTimeMillis();
        //            
            while (DefaultFuture.hasFuture(channel)
                    && System.currentTimeMillis() - start < timeout) {
                try {
                    Thread.sleep(10);
                } catch (InterruptedException e) {
                    logger.warn(e.getMessage(), e);
                }
            }
        }
        close();
    }
    

    Clientを閉じると、応答を受信していない情報リクエストがまだ存在する場合は、すべてのリクエストが応答を受信したことを確認するまで一定時間待機するか、タイムアウト時間を超えるまで待機します.
    ps:Dubboリクエストが一時的に存在するHeaderExchangeChannel#close Map中なので、Mapを簡単に判断すればリクエストに応答があるかどうかを知ることができます.
    この点で、サービス消費者を閉鎖すると、すでに発行されたサービス要求に応答が戻るのを待つ必要があるという3つ目の問題を解決しました.
    Dubboエレガントなダウンタイムの全体的な流れを図に示します.
    ps:Clientクローズコールチェーンは以下の通りです.
    DubboProtocol#close
        ->ReferenceCountExchangeClient#close
            ->HeaderExchangeChannel#close
                ->AbstractClient#close

    四、2.7.1 X
    Dubboは一般的にSpringフレームワークとともに使用され、2.5.Xバージョンのダウンタイムプロセスは優雅なダウンタイムを失効させる可能性があります.これはSpringフレームワークが閉じると、対応するShutdownHookイベントがトリガーされ、関連するBeanがログアウトされるためです.このプロセスはSpringが率先してダウンタイムを実行すると、関連するBeanをログアウトします.このときDubboクローズイベントでSpringのBeanに引用されると、ダウンタイム中に異常が発生し、優雅なダウンタイムが失効します.
    この問題を解決するために、Dubboは2.6.Xバージョンでこのロジックの再構築を開始し、2.7.1.Xバージョンまで反復し続けた.
    新しいバージョンではDefaultFutureが追加され、Spring ShutdownHookListenerインタフェースを継承し、Spring関連イベントをリスニングします.ここでApplicationListener はSpringクローズイベントのみをリスニングし、Springがクローズを開始するとShutdownHookListenerの内部ロジックがトリガーされる.
    
    public class SpringExtensionFactory implements ExtensionFactory {
        private static final Logger logger = LoggerFactory.getLogger(SpringExtensionFactory.class);
    
        private static final Set CONTEXTS = new ConcurrentHashSet();
        private static final ApplicationListener SHUTDOWN_HOOK_LISTENER = new ShutdownHookListener();
    
        public static void addApplicationContext(ApplicationContext context) {
            CONTEXTS.add(context);
            if (context instanceof ConfigurableApplicationContext) {
                //    ShutdownHook
                ((ConfigurableApplicationContext) context).registerShutdownHook();
                //    AbstractConfig     ShutdownHook   
                DubboShutdownHook.getDubboShutdownHook().unregister();
            }
            BeanFactoryUtils.addApplicationListener(context, SHUTDOWN_HOOK_LISTENER);
        }
        //    ApplicationListener,               
        private static class ShutdownHookListener implements ApplicationListener {
            @Override
            public void onApplicationEvent(ApplicationEvent event) {
                if (event instanceof ContextClosedEvent) {
                    DubboShutdownHook shutdownHook = DubboShutdownHook.getDubboShutdownHook();
                    shutdownHook.doDestroy();
                }
            }
        }
    }
    

    Springフレームワークが初期化を開始すると、ShutdownHookListenerロジックがトリガーされ、SpringExtensionFactory登録AbstractConfigがログアウトされ、ShutdownHookが追加されます.これで上の『ダブルホック』問題を完璧に解決します.
    五、最後
    エレガントなダウンタイムは実現が難しくないように見えますが、中には細部の設計が非常に多く、1つの点で実現に問題があり、エレガントなダウンタイムが失効します.もしあなたも優雅なダウンタイムを実現しているなら、Dubboの実現ロジックを参考にしてみてください.
    Dubboシリーズ記事のおすすめ
    1.Dubboに登録センターの動作原理を聞かれたら、この文章を彼にあげます.2.サービスの動的発見をどのように実現するか分かりませんか.Dubboがどのようにして3.Dubbo Zkデータ構造を実現したかを見てみましょう.4.Dubboから、Spring XML Schema拡張メカニズムについて話します.
    ヘルプ記事
    1、kirito大神文章を読むことを強くお勧めします:Dubbo優雅な停止
    私の公衆番号に注目してください:プログラムが通じて、日常の乾物のプッシュを獲得します.私のテーマの内容に興味があれば、私のブログにも注目してください:studyidea.cn