Dubbo原始碼學習--優雅停機原理及在SpringBoot中遇到的問題

dav2100發表於2021-09-09

前言

主要是前一陣子換了工作,第一個任務就是解決目前團隊在 Dubbo 停機時產生的問題,同時最近又看了一下 Dubbo 的原始碼,想重新寫一下 Dubbo 相關的文章。

優雅停機原理

對於一個 java 應用,如果想在關閉應用時,執行一些釋放資源的操作一般是透過註冊一個 ShutDownHook ,當關閉應用時,不是呼叫 kill -9 命令來直接終止應用,而是透過呼叫 kill -15 命令來觸發這個 ShutDownHook 進行停機前的釋放資源操作。
對於 Dubbo 來說,需要停機前執行的操作包括兩部分:

  1. 對於服務的提供者,需要通知註冊中心來把自己在服務列表中摘除掉。

  2. 根據所配置的協議,關閉協議的埠和連線。

而何為優雅停機呢?就是在叢集環境下,有一個應用停機,並不會出現異常。下面來看一下 Dubbo 是怎麼做的。

註冊ShutDownHook

Duubo 在 AbstractConfig 的靜態建構函式中註冊了 JVM 的 ShutDownHook,而 ShutdownHook 主要是呼叫 ProtocolConfig.destroyAll() ,原始碼如下:

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

ProtocolConfig.destroyAll()

先看一下 ProtocolConfig.destroyAll() 原始碼:

  public static void destroyAll() {        if (!destroyed.compareAndSet(false, true)) {            return;
        }
        AbstractRegistryFactory.destroyAll();  //1.

        // Wait for registry notification
        try {
            Thread.sleep(ConfigUtils.getServerShutdownTimeout()); //2.
        } catch (InterruptedException e) {
            logger.warn("Interrupted unexpectedly when waiting for registry notification during shutdown process!");
        }

        ExtensionLoader<Protocol> loader = ExtensionLoader.getExtensionLoader(Protocol.class);        for (String protocolName : loader.getLoadedExtensions()) {            try {
                Protocol protocol = loader.getLoadedExtension(protocolName);                if (protocol != null) {
                    protocol.destroy(); //3.
                }
            } catch (Throwable t) {
                logger.warn(t.getMessage(), t);
            }
        }
    }

ProtocolConfig.destroyAll() 有三個比較重要的操作:

  1. 在1這個點呼叫AbstractRegistryFactory.destroyAll(),其內部會對每個註冊中心進行 destroy 操作,進而把註冊到註冊中心的服務取消註冊。

  2. 2這個點是最近 Dubbo 版本新增的操作,用來增強 Dubbo 的優雅停機,在老版本的 Dubbo 其邏輯是直接摘除服務列表,關閉暴露的連線,因為服務取消註冊,註冊中心是非同步的通知消費者變更其存放在自己記憶體中的提供者列表。因為是非同步操作,當呼叫量比較大的應用時消費者會拿到已經關閉連線點的提供者進行呼叫,這時候就會產生大量的錯誤,而2這個點就是透過Sleep 來延遲關閉協議暴露的連線。

  3. 因為 Dubbo 的擴充套件機制 ,loader.getLoadedExtensions() 會獲取到已使用的所有協議,遍歷呼叫 destroy 方法來關閉其開啟的埠和連線。

而在第3步會在 Exchange 層 對所有開啟的連線進行判斷其有沒有正在執行的請求,如果有會自旋 Sleep 直到設定的 ServerShutdownTimeout 時間或者已經沒有正在執行的請求了才會關閉連線,原始碼如下:

  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)) {
               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); //正在的關閉連線
   }

## 在 SpringBoot 應用中存在的問題

簡單的描述一下問題:就是在應用停機時,瞬間會產生大量的報錯,比如拿到的資料庫連線已經關閉等問題。 其實一看就知道是在停機時還存在正在處理的請求,而這些請求所需要的資源被 Spring 容器所關閉導致的。原來在SpringBoot 啟動時會在 refreshContext 操作也註冊一個 ShotdownHook 來關閉Spring容器。

    private void refreshContext(ConfigurableApplicationContext context) {       this.refresh(context);       if (this.registerShutdownHook) {           try {
               context.registerShutdownHook();
           } catch (AccessControlException var3) {
           }
       }
   }

而要解決這個問題就需要取消掉這個 ShutDownHook ,然後再 Dubbo 優雅停機執行後關閉 Spring 容器。具體的修改如下:

  1. 在啟動Main方法中,修改SpringBoot 啟動程式碼,取消註冊ShutDownHook。

    public static void main(String[] args) {
       SpringApplication app = new SpringApplication(XxxApplication.class);
       app.setRegisterShutdownHook(false);
       app.run(args);
   }
  1. 註冊一個Bean 來讓 Dubbo 關閉後關閉Spring容器。

public class SpringShutdownHook {   private static final Logger logger = LoggerFactory.getLogger(SpringShutdownHook.class);   @Autowired
   private ConfigurableApplicationContext configurableApplicationContext;   public SpringShutdownHook() {
   }   @PostConstruct
   public void registerShutdownHook() {
       logger.info("[SpringShutdownHook] Register ShutdownHook....");
       Thread shutdownHook = new Thread() {           public void run() {               try {                   int timeOut = ConfigUtils.getServerShutdownTimeout();
                   SpringShutdownHook.logger.info("[SpringShutdownHook] Application need sleep {} seconds to wait Dubbo shutdown", (double)timeOut / 1000.0D);
                   Thread.sleep((long)timeOut);
                   SpringShutdownHook.this.configurableApplicationContext.close();
                   SpringShutdownHook.logger.info("[SpringShutdownHook] ApplicationContext closed, Application shutdown");
               } catch (InterruptedException var2) {
                   SpringShutdownHook.logger.error(var2.getMessage(), var2);
               }

           }
       };
       Runtime.getRuntime().addShutdownHook(shutdownHook);
   }
}

作者:

出處:https://www.cnblogs.com/javanoob/p/dubbo_graceful_shutdown.html

 


來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/4479/viewspace-2818000/,如需轉載,請註明出處,否則將追究法律責任。

相關文章