1. 程式人生 > >Dubbo 優雅停機演進之路

Dubbo 優雅停機演進之路

一、前言

在 『ShutdownHook- Java 優雅停機解決方案』 一文中我們聊到了 Java 實現優雅停機原理。接下來我們就跟根據上面知識點,深入 Dubbo 內部,去了解一下 Dubbo 如何實現優雅停機。

二、Dubbo 優雅停機待解決的問題

為了實現優雅停機,Dubbo 需要解決一些問題:

  1. 新的請求不能再發往正在停機的 Dubbo 服務提供者。
  2. 若關閉服務提供者,已經接收到服務請求,需要處理完畢才能下線服務。
  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<Protocol> 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 優雅停機主要分為兩步:

  1. 登出註冊中心
  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 可以及時通知消費者下線服務,新的請求也不再發往下線的節點,也就解決上面提到的第一個問題:新的請求不能再發往正在停機的 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!");
}

預設等待時間為 10000ms,可以通過設定 dubbo.service.shutdown.wait 覆蓋預設引數。10s 只是一個經驗值,可以根據實際情設定。不過這個等待時間設定比較講究,不能設定成太短,太短將會導致消費端還未收到 ZK 通知,提供者就停機了。也不能設定太長,太長又會導致關停應用時間邊長,影響釋出體驗。

3.3、登出 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);
    }
}

loader#getLoadedExtensions 將會返回兩種 Protocol 子類,分別為 DubboProtocolInjvmProtocol

DubboProtocol 用與服務端請求互動,而 InjvmProtocol 用於內部請求互動。如果應用呼叫自己提供 Dubbo 服務,不會再執行網路呼叫,直接執行內部方法。

這裡我們主要來分析一下 DubboProtocol 內部邏輯。

DubboProtocol#destroy 原始碼:

public void destroy() {
    // 關閉 Server
    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(ConfigUtils.getServerShutdownTimeout());
            } catch (Throwable t) {
                logger.warn(t.getMessage(), t);
            }
        }
    }
    // 關閉 Client
    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(ConfigUtils.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(ConfigUtils.getServerShutdownTimeout());
            } catch (Throwable t) {
                logger.warn(t.getMessage(), t);
            }
        }
    }
    stubServiceMethodsMap.clear();
    super.destroy();
}

Dubbo 預設使用 Netty 作為其底層的通訊框架,分為 ServerClientServer 用於接收其他消費者 Client 發出的請求。

上面原始碼中首先關閉 Server ,停止接收新的請求,然後再關閉 Client。這樣做就降低服務被消費者呼叫的可能性。

3.4、關閉 Server

首先將會呼叫 HeaderExchangeServer#close,原始碼如下:

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

這裡將會向服務消費者傳送 READ_ONLY 事件。消費者接受之後,主動排除這個節點,將請求發往其他正常節點。這樣又進一步降低了註冊中心通知延遲帶來的影響。

接下來將會關閉心跳檢測,關閉底層通訊框架 NettyServer。這裡將會呼叫 NettyServer#close 方法,這個方法實際在 AbstractServer 處實現。

AbstractServer#close 原始碼如下:

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

這裡首先關閉業務執行緒池,這個過程將會盡可能將執行緒池中的任務執行完畢,再關閉執行緒池,最後在再關閉 Netty 通訊底層 Server。

Dubbo 預設將會把請求/心跳等請求派發到業務執行緒池中處理。

關閉 Server,優雅等待執行緒池關閉,解決了上面提到的第二個問題:若關閉服務提供者,已經接收到服務請求,需要處理完畢才能下線服務。

Dubbo 服務提供者關閉流程如圖:

ps:為了方便除錯原始碼,附上 Server 關閉呼叫聯。

DubboProtocol#destroy
    ->HeaderExchangeServer#close
        ->AbstractServer#close
            ->NettyServer#doClose                

3.5 關閉 Client

Client 關閉方式大致同 Server,這裡主要介紹一下處理已經發出請求邏輯,程式碼位於HeaderExchangeChannel#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 請求會暫存在 DefaultFuture Map 中,所以只要簡單判斷一下 Map 就能知道請求是否都收到響應。

通過這一點我們就解決了第三個問題:若關閉服務消費者,已經發出的服務請求,需要等待響應返回。

Dubbo 優雅停機總體流程如圖所示。

ps: Client 關閉呼叫鏈如下所示:

DubboProtocol#close
    ->ReferenceCountExchangeClient#close
        ->HeaderExchangeChannel#close
            ->AbstractClient#close

四、2.7.X

Dubbo 一般與 Spring 框架一起使用,2.5.X 版本的停機過程可能導致優雅停機失效。這是因為 Spring 框架關閉時也會觸發相應的 ShutdownHook 事件,登出相關 Bean。這個過程若 Spring 率先執行停機,登出相關 Bean。而這時 Dubbo 關閉事件中引用到 Spring 中 Bean,這就將會使停機過程中發生異常,導致優雅停機失效。

為了解決該問題,Dubbo 在 2.6.X 版本開始重構這部分邏輯,並且不斷迭代,直到 2.7.X 版本。

新版本新增 ShutdownHookListener,繼承 Spring ApplicationListener 介面,用以監聽 Spring 相關事件。這裡 ShutdownHookListener 僅僅監聽 Spring 關閉事件,當 Spring 開始關閉,將會觸發 ShutdownHookListener 內部邏輯。


public class SpringExtensionFactory implements ExtensionFactory {
    private static final Logger logger = LoggerFactory.getLogger(SpringExtensionFactory.class);

    private static final Set<ApplicationContext> CONTEXTS = new ConcurrentHashSet<ApplicationContext>();
    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 框架開始初始化之後,將會觸發 SpringExtensionFactory 邏輯,之後將會登出 AbstractConfig 註冊 ShutdownHook,然後增加 ShutdownHookListener。這樣就完美解決上面『雙 hook』 問題。

五、最後

優雅停機看起來實現不難,但是裡面設計細枝末節卻非常多,一個點實現有問題,就會導致優雅停機失效。如果你也正在實現優雅停機,不妨參考一下 Dubbo 的實現邏輯。

Dubbo 系列文章推薦

1.如果有人問你 Dubbo 中註冊中心工作原理,就把這篇文章給他
2.不知道如何實現服務的動態發現?快來看看 Dubbo 是如何做到的
3.Dubbo Zk 資料結構
4.緣起 Dubbo ,講講 Spring XML Schema 擴充套件機制

幫助文章

1、強烈推薦閱讀 kirito 大神文章:一文聊透 Dubbo 優雅停機

歡迎關注我的公眾號:程式通事,獲得日常乾貨推送。如果您對我的專題內容感興趣,也可以關注我的部落格:studyidea.cn

相關推薦

Dubbo 優雅停機演進

一、前言 在 『ShutdownHook- Java 優雅停機解決方案』 一文中我們聊到了 Java 實現優雅停機原理。接下來我們就跟根據上面知識點,深入 Dubbo 內部,去了解一下 Dubbo 如何實現優雅停機。 二、Dubbo 優雅停機待解決的問題 為了實現優雅停機,Dubbo 需要解決一些問題: 新

Netflix 的微服務演進

JFrog Netflix DevOps Jenkins 背景Netflix 是全球領先的視頻網站,影片類型包括好萊塢制作,獨立制作電影,本地電影等等,自主研發了“紙牌屋”等知名的電視劇。全球有8千多萬的訂閱會員,覆蓋190個國家(暫未覆蓋中國…),支持一千多種設備類型。Netflix 是 A

Java企業級電商項目架構演進Tomcat集群與Redis分布式

TomcatJava企業級電商項目架構演進之路Tomcat集群與Redis分布式網盤地址:https://pan.baidu.com/s/1taAooW3AhdGcdGSvOLqjkg 密碼:nwip備用地址(騰訊微雲):https://share.weiyun.com/5JdkNHX 密碼:s9pm74 第

專訪UCloud徐亮:UCloud虛擬網絡的演進

路由策略 offload 性問題 通信 報文 ide 本地 多隊列 交換 服務器虛擬化改變了IT運營方式,隨之而來的是越來越多的網絡被虛擬化。 當今,幾乎所有的IT基礎架構都在朝雲的方向發展。在基礎架構中,服務器和存儲虛擬化已經發展的比較成熟,作為基礎架構中的虛擬網絡,為了

七牛雲馮立元:邊緣儲存的演進

2018 年 10 月 18 日-20 日,由極客邦科技與 InfoQ 中國主辦的 QCon 全球軟體開發大會在上海寶華萬豪酒店舉行。作為一場綜合性的技術盛會,QCon 全球軟體開發大會每年都會在倫敦、北京、東京、紐約、聖保羅、上海、舊金山召開,而此次 QCon 大會上海站更是設定了 30 餘個專題,邀

阿里雲檔案儲存的高效能架構演進

摘要: 10月27日下午,2018中國計算機大會上舉辦了主題“資料中心計算”的技術論壇,一起探討解決資料中心所面臨的挑戰。論壇上,阿里雲分散式儲存團隊高階技術專家田磊磊進行了《阿里雲檔案儲存的高效能架構演進之路》的報告。 10月27日下午,2018中國計算機大會上舉辦了主題“資料中心計算”的技術論壇,一起探

阿裏雲文件存儲的高性能架構演進

探討 延時 領域 高級 .com 上大 list 51cto 需要 摘要: 10月27日下午,2018中國計算機大會上舉辦了主題“數據中心計算”的技術論壇,一起探討解決數據中心所面臨的挑戰。論壇上,阿裏雲分布式存儲團隊高級技術專家田磊磊進行了《阿裏雲文件存儲的高性能架構演進

叢集排程框架的架構演進

叢集架構是現代資料中心非常重要的元件,在最近幾年中有長足發展。架構也從單體式設計轉向更加靈活、去中心化和分散式設計。然而,許多現代開源實現仍然是單體式設計或者缺少很多功能,而這些功能對實際使用者非常有用。這篇部落格是關於大型機群任務排程系列的第一篇,資源排程在Amazon、Google、Fa

今日頭條架構演進——高壓下的架構演進專題

合服 51cto 還需要 ESS color 壓力 一點 日誌 規劃 今天給大家分享今日頭條架構演進,前面幾位講師講了很多具體的幹貨,我的分享偏重基礎設施及架構思路的介紹,我們想法是通過提供更好的基礎設施,幫助架構做更好的叠代。 從架構的角度,技術團隊應對的壓力最主要來自三

Netty權威指南_札記01_I/O演進

文章目錄 1. Linux網路I/O模型 1.1 阻塞 I/O 模型 1.2 非阻塞 I/O 模型 1.3 I/O 複用模型 1.4 訊號驅動 I/O 模型 1.5 非同步

專訪UCloud徐亮:UCloud虛擬網路的演進

伺服器虛擬化改變了IT運營方式,隨之而來的是越來越多的網路被虛擬化。 當今,幾乎所有的IT基礎架構都在朝雲的方向發展。在基礎架構中,伺服器和儲存虛擬化已經發展的比較成熟,作為基礎架構中的虛擬網路,為了迎合雲端計算和網際網路發展的需求,迎來了新的挑戰,UCloud虛擬網路平臺負責人徐亮對此進行了梳

圖解分散式系統架構演進

介紹 分散式和叢集的概念經常被搞混,現在一句話讓你明白兩者的區別。 分散式:一個業務拆分成多個子業務,部署在不同的伺服器上 叢集:同一個業務,部署在多個伺服器上 例如:電商系統可以拆分成商品,訂單,使用者等子系統。這就是分散式,而為了應對併發,同時部署好幾個使用者系統,這就是

Java企業級電商專案架構演進 Tomcat叢集與Redis分散式分享

第1章 課程介紹與前置專案回顧【配合一期課程,效果最佳】 本章首先會對一期成果進行回顧、然後確定本次進階課程的演進目標以及進階課程的內容安排。然後會介紹課程使用各種技術版本,以方便大家的環境和課程保持一致,減少因版本不同而踩的沒必要的坑。之後會對二期專案初始化進行講解,包括IDEA中匯入二期原

Java企業級電商專案架構演進 Tomcat叢集與Redis分散式

6-1 本章概要 6-2 使用者模組一期回顧與二期任務 6-3 Redis連線池構建與測試-1 6-4 Redis連線池構建與測試-2 6-5 Jedis api封裝與除錯 6-6 Jsonutil 封裝及除錯-1 6-7 Jsonutil 封裝及除錯-2 6-8 Jsonutil

Flutter持續化整合上的演進

作者:騰訊 - 小德(koudleren 任曉帥) ##存在的問題 為了使用Flutter,剛開始的時候為了快速上線,採用將Flutter和Native程式碼混合開發的模式,具體的工程目錄如下: 可以明顯的看到工程目錄很亂,android目錄下有多個程式碼目錄,lib目錄下是dart程式

你所不知道的Python|函式引數的演進

  函式引數處理機制是Python中一個非常重要的知識點,隨著Python的演進,引數處理機制的靈活性和豐富性也在不斷增加,使得我們不僅可以寫出簡化的程式碼,也能處理複雜的呼叫。 關鍵字引數 呼叫時指定引數的名稱,且與函式宣告時的引數名稱一致。 關鍵字引數是Pyt

回顧·神馬搜尋技術演進

轉載自 DataFunTalk 公眾號  http://www.6aiq.com/article/1535003242764 前言 國內搜尋引擎大事記 1998年,Google釋出;2000年,百度釋出;2004年,搜狗釋出;2006年,搜搜釋出;2010年,Goog

從0到1:微信後臺系統的演進---張文瑞

2個月的開發時間,微信後臺系統經歷了從0到1的過程。從小步慢跑到快速成長,經歷了平臺化到走出國門,微信交出的這份優異答卷,解題思路是怎樣的?本文由張文瑞,微信後臺團隊出品。 從無到有 2011.1.21 微信正式釋出。這一天距離微信專案啟動日約為2個月。就在這2個月

阿里畢玄:阿里十年,從分散式到雲時代的架構演進

這是一篇來自鯤鵬會的文章,其內容是畢玄在TGO 鯤鵬會杭州分會活動現場分享的《雲時代的軟體架構》的整理。特別轉載到雲棲社群,讓更多開發者深入瞭解阿里架構的變遷和對雲技術的一些新的想法。 2018 年 12 月 15 日,TGO 鯤鵬會杭州分會拉開了 TGO 特有的技術人年會「E 家宴」的帷幕

從無到有:微信後臺系統的演進

從無到有 2011.1.21 微信正式釋出。這一天距離微信專案啟動日約為2個月。就在這2個月裡,微信從無到有,大家可能會好奇這期間微信後臺做的最重要的事情是什麼? 我想應該是以下三件事: 1. 確定了微信的訊息模型 微信起初定位是一個通訊工具,作為通訊工具最核心的功能是收發訊息。微信團隊源於廣硏團隊