1、前言html
在 『ShutdownHook- Java 優雅停機解決方案』 一文中咱們聊到了 Java 實現優雅停機原理。接下來咱們就跟根據上面知識點,深刻 Dubbo 內部,去了解一下 Dubbo 如何實現優雅停機。java
爲了實現優雅停機,Dubbo 須要解決一些問題:網絡
解決以上三個問題,才能使停機對業務影響下降到最低,作到優雅停機。數據結構
Dubbo 優雅停機在 2.5.X 版本實現比較完整,這個版本的實現相對簡單,比較容易理解。因此咱們先以 Dubbo 2.5.X 版本源碼爲基礎,先來看一下 Dubbo 如何實現優雅停機。併發
優雅停機入口類位於 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()
。jvm
ProtocolConfig.destroyAll()
源碼以下:ide
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 優雅停機主要分爲兩步:this
Protocol
註銷註冊中心源碼以下:idea
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 通知,提供者就停機了。也不能設置太長,太長又會致使關停應用時間邊長,影響發佈體驗。
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
子類,分別爲 DubboProtocol
與 InjvmProtocol
。
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 做爲其底層的通信框架,分爲 Server
與 Client
。Server
用於接收其餘消費者 Client
發出的請求。
上面源碼中首先關閉 Server
,中止接收新的請求,而後再關閉 Client
。這樣作就下降服務被消費者調用的可能性。
首先將會調用 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
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
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 的實現邏輯。
1.若是有人問你 Dubbo 中註冊中心工做原理,就把這篇文章給他
2.不知道如何實現服務的動態發現?快來看看 Dubbo 是如何作到的
3.Dubbo Zk 數據結構
4.緣起 Dubbo ,講講 Spring XML Schema 擴展機制
一、強烈推薦閱讀 kirito 大神文章:一文聊透 Dubbo 優雅停機
歡迎關注個人公衆號:程序通事,得到平常乾貨推送。若是您對個人專題內容感興趣,也能夠關注個人博客: studyidea.cn