別再用 kill -9 了,這纔是微服務上下線的正確姿勢!

對於微服務來說,服務的優雅上下線是必要的。

就上線來說,如果組件或者容器沒有啓動成功,就不應該對外暴露服務,對於下線來說,如果機器已經停機了,就應該保證服務已下線,如此可避免上游流量進入不健康的機器。

優雅下線

基礎下線(Spring/SpringBoot / 內置容器)

首先 JVM 本身是支持通過 shutdownHook 的方式優雅停機的。

Runtime.getRuntime().addShutdownHook(new Thread() {
    @Override
    public void run() {
        close();
    }
});

此方式支持在以下幾種場景優雅停機:

  1. 程序正常退出

  2. 使用 System.exit()

  3. 終端使用 Ctrl+C

  4. 使用 Kill pid 幹掉進程

那麼如果你偏偏要kill -9 程序肯定是不知所措的。

而在 Springboot 中,其實已經幫你實現好了一個 shutdownHook,支持響應 Ctrl+c 或者 kill -15 TERM 信號。

隨便啓動一個應用,然後 Ctrl+c 一下,觀察日誌就可知, 它在 AnnotationConfigEmbeddedWebApplicationContext 這個類中打印出了疑似 Closing... 的日誌,真正的實現邏輯在其父類 AbstractApplicationContext 中 (這個其實是 spring 中的類,意味着什麼呢,在 spring 中就支持了對優雅停機的擴展)。

public void registerShutdownHook() {
    if (this.shutdownHook == null) {
        this.shutdownHook = new Thread() {
            public void run() {
                synchronized(AbstractApplicationContext.this.startupShutdownMonitor) {
                    AbstractApplicationContext.this.doClose();
                }
            }
        };
        Runtime.getRuntime().addShutdownHook(this.shutdownHook);
    }
 
}
 
public void destroy() {
    this.close();
}
 
public void close() {
    Object var1 = this.startupShutdownMonitor;
    synchronized(this.startupShutdownMonitor) {
        this.doClose();
        if (this.shutdownHook != null) {
            try {
                Runtime.getRuntime().removeShutdownHook(this.shutdownHook);
            } catch (IllegalStateException var4) {
                ;
            }
        }
 
    }
}
 
protected void doClose() {
    if (this.active.get() && this.closed.compareAndSet(false, true)) {
        if (this.logger.isInfoEnabled()) {
            this.logger.info("Closing " + this);
        }
 
        LiveBeansView.unregisterApplicationContext(this);
 
        try {
            this.publishEvent((ApplicationEvent)(new ContextClosedEvent(this)));
        } catch (Throwable var3) {
            this.logger.warn("Exception thrown from ApplicationListener handling ContextClosedEvent", var3);
        }
 
        if (this.lifecycleProcessor != null) {
            try {
                this.lifecycleProcessor.onClose();
            } catch (Throwable var2) {
                this.logger.warn("Exception thrown from LifecycleProcessor on context close", var2);
            }
        }
 
        this.destroyBeans();
        this.closeBeanFactory();
        this.onClose();
        this.active.set(false);
    }
 
}

我們能對它做些什麼呢,其實很明顯,在 doClose 方法中它發佈了一個 ContextClosedEvent 的方法,不就是給我們擴展用的麼。

於是我們可以寫個監聽器監聽 ContextClosedEvent,在發生事件的時候做下線邏輯,對微服務來說即是從註冊中心中註銷掉服務。

@Component
public class GracefulShutdownListener implements ApplicationListener<ContextClosedEvent> {
    
    @Override
    public void onApplicationEvent(ContextClosedEvent contextClosedEvent){
       //註銷邏輯
       zookeeperRegistry.unregister(mCurrentServiceURL);
       ...
    }
}

可能會有疑問的是,微服務中一般來說,註銷服務往往是優雅下線的第一步,接着纔會執行停機操作,那麼這個時候流量進來怎麼辦呢?

個人會建議是,在註銷服務之後就可開啓請求擋板拒絕流量了,通過微服務框架本身的故障轉移功能去處理被拒絕的流量即可。

Docker 中的下線

好有人說了,我用 docker 部署服務,支不支持優雅下線。

那來看看 docker 的一些停止命令都會幹些啥:

一般來說,正常人可能會用 docker stop 或者 docker kill 命令去關閉容器(當然如果上一步註冊了 USR2 自定義信息,可能會通過 docker exec kill -12 去關閉)。

對於 docker stop 來說,它會發一個 SIGTERM(kill -15 term 信息) 給容器的 PID1 進程,並且默認會等待 10s,再發送一個 SIGKILL(kill -9 信息) 給 PID1。

那麼很明顯,docker stop 允許程序有個默認 10s 的反應時間去做一下優雅停機的操作,程序只要能對 kill -15 信號做些反應就好了,如上一步描述。那麼這是比較良好的方式。

搜索公衆號 GitHub 猿後臺回覆 “監控”,獲取一份驚喜禮包。

當然如果 shutdownHook 方法執行了個 50s,那肯定不優雅了。可以通過 docker stop -t 加上等待時間。

外置容器的 shutdown 腳本(Jetty)

如果非要用外置容器方式部署(個人認爲浪費資源並提升複雜度)。那麼能不能優雅停機呢。

可以當然也是可以的,這裏有兩種方式:

首先 RPC 框架本身提供優雅上下線接口,以供調用來結束整個應用的生命週期,並且提供擴展點供開發者自定義服務下線自身的停機邏輯。同時調用該接口的操作會封裝成一個 preStop 操作固化在 jetty 或者其他容器的 shutdown 腳本中,保證在容器停止之前先調用下線接口結束掉整個應用的生命週期。shutdown 腳本中執行類發起下線服務 -> 關閉端口 -> 檢查下線服務直至完成 -> 關閉容器的流程。

而更簡單的另一種方法是直接在腳本中加入 kill -15 命令。

優雅上線


優雅上線相對來說可能會更加困難一些,因爲沒有什麼默認的實現方式,但是總之呢,一個原則就是確保端口存在之後才上線服務。

springboot 內置容器優雅上線

這個就很簡單了,並且業界在應用層面的優雅上線均是在內置容器的前提下實現的,並且還可以配合一些列健康檢查做文章。

參看 sofa-boot 的健康檢查的源碼,它會在程序啓動的時候先對 springboot 的組件做一些健康檢查,然後再對它自己搞得 sofa 的一些中間件做健康檢查,整個健康檢查的流程完畢之後(sofaboot 目前是沒法對自身應用層面做健康檢查的,它有寫相關接口,但是寫死了 port is ready...)纔會暴露服務或者說優雅上線,那麼它健康檢查的時機是什麼時候呢:

@Override
public void onApplicationEvent(ContextRefreshedEvent event) {
    healthCheckerProcessor.init();
    healthIndicatorProcessor.init();
    afterHealthCheckCallbackProcessor.init();
    publishBeforeHealthCheckEvent();
    readinessHealthCheck();
}

可以看到它是監聽了 ContextRefreshedEvent 這個事件。在內置容器模式中,內置容器模式的 start 方法是在 refreshContext 方法中,方法執行完成之後發佈一個 ContextRefreshedEvent 事件,也就是說在監聽到這個事件的時候,內置容器必然是啓動成功了的。

但 ContextRefreshedEvent 這個事件,在一些特定場景中由於種種原因,ContextRefreshedEvent 會被監聽到多次,沒有辦法保證當前是最後一次 event,從而正確執行優雅上線邏輯。

在 springboot 中還有一個更加靠後的事件,叫做 ApplicationReadyEvent,它的發佈藏在了 afterRefresh 還要後面的那一句listeners.finished(context, null)中,完完全全可以保證內置容器 端口已經存在了,所以我們可以監聽這個事件去做優雅上線的邏輯,甚至可以把中間件相關的健康檢查集成在這裏。

@Component
public class GracefulStartupListener implements ApplicationListener<ApplicationReadyEvent> {    
    @Override
    public void onApplicationEvent(ApplicationReadyEvent applicationReadyEvent){
       //註冊邏輯 優雅上線
       apiRegister.register(urls);
       ...
    }
}

外置容器 (Jetty) 優雅上線

目前大多數應用的部署模式不管是 jetty 部署模式還是 docker 部署模式 (同樣使用 jetty 鏡像),本質上用的都是外置容器。那麼這個情況就比較困難了,至少在應用層面無法觀察到外部容器的運行狀態,並且容器本身沒有提供什麼 hook 給你實現。

那麼和優雅上線一樣,需要 RPC 框架提供優雅上線接口來初始化整個應用的生命週期,並且提供擴展點給開發者供執行自定義的上線邏輯 (上報版本探測信息等)。同樣將調用這個接口封裝成一個 postStart 操作,固化在 jetty 等外置容器的 startup 腳本中,保證應用在容器啓動之後在上線。容器執行類似啓動容器 -> 健康檢查 -> 上線服務邏輯 -> 健康上線服務直至完成 的流程。

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/wddAfRDxo8FOCNki_v4kRA