1. 程式人生 > >Spring Cloud: 使用kill命令優雅關閉微服務, 解決退出時Eureka取消註冊操作耗時過長的坑

Spring Cloud: 使用kill命令優雅關閉微服務, 解決退出時Eureka取消註冊操作耗時過長的坑

關於Spring Cloud服務優雅關閉的方案有很多種了,這裡介紹一下使用kill命令優雅關閉的方案,並解決會出現的問題。

所謂的優雅指兩方面,一是程式在退出時要主動向Eureka取消註冊自己,二是完成資源清理工作。比如我的程式裡用到了執行緒池來非同步執行一些任務,如果退出時不做清理,那麼就有非同步任務被異常中斷導致業務資料不一致的風險。首先我們不能使用kill -9。如果加了-9,那麼系統就不會給JVM呼叫 shutdown hook 的機會,也就無法完成資源清理了。

退出取消Euerka註冊隱藏的坑

Spring Cloud預設的EurekaClientAutoConfiguration

這個自動配置類已經為我們做好了相應的工作,但是卻不夠完美。在程式收到kill訊號時,JVM會呼叫 shutdown hook, 雖然在此hook中就有取消註冊的邏輯,但我在實踐中經常會遇到取消註冊耗時特別長,導致 hook 執行緒block, 程序長時間等待而不能退出。這就會有一個致命的問題,因為kill命令並不會等待目標程序退出才會返回,而是立刻返回,這就意味著kill執行完後你的JVM程序還在。如果出現 hook 執行緒卡住的情況,那就極有可能當你再次啟動服務的時候,上一次服務還沒有關閉,從而導致新服務啟動失敗(往往是因為埠被佔用)。

通過追蹤原始碼,我發現取消註冊的邏輯是在EurekaAutoServiceRegistration#onApplicationEvent()

方法中實現的,此方法響應Spring容器的ContextCloseEvent,然後呼叫stop()方法取消註冊,耗時的也是這個stop()方法。因此我們可以自己編寫一個類繼承此類,覆蓋stop()方法,新增超時邏輯:

@Slf4j
    public static class EngineEurekaAutoServiceRegistration extends EurekaAutoServiceRegistration {
        public EngineEurekaAutoServiceRegistration(ApplicationContext context,
                                                   EurekaServiceRegistry serviceRegistry,
                                                   EurekaRegistration registration) {
            super
(context, serviceRegistry, registration); } /** * 上下文關閉時會呼叫此方法, 在另一個執行緒中取消註冊, 防止超時 */ @Override public void stop() { log.info("unregsiter eureka in another thread"); Thread stopThread = new Thread(() -> super.stop()); stopThread.start(); try { stopThread.join(2000); } catch (InterruptedException e) { e.printStackTrace(); } log.info("unregister done"); } }

在這裡我們在新的執行緒中呼叫父類的stop(), 然後通過join()方法控制超時時間,設定為2s。
要想讓自己的類起效,我們還要做一些工作。首先我們要編寫一個配置類註冊自己的bean:

@Configuration
public class EngineEurekaConfig extends EurekaClientAutoConfiguration {
    public EngineEurekaConfig(ConfigurableEnvironment env) {
        super(env);
    }


    @Bean
    @ConditionalOnBean(AutoServiceRegistrationProperties.class)
    @ConditionalOnProperty(value = "spring.cloud.service-registry.auto-registration.enabled", matchIfMissing = true)
    @Override
    public EurekaAutoServiceRegistration eurekaAutoServiceRegistration(ApplicationContext context,
                                                                       EurekaServiceRegistry registry,
                                                                       EurekaRegistration registration) {
        return new EngineEurekaAutoServiceRegistration(context, registry, registration);
    }
}

然後在@SpringBootApplication註解中排除EurekaClientAutoConfiguration.class:

@SpringBootApplication( exclude = {
            EurekaClientAutoConfiguration.class
            })

這樣Spring就使用我們的類來響應關閉事件了,當unregister過程超時後會直接忽略,程序退出。

執行緒池清理

這部分就比較簡單了,寫一個@PreDestroy方法,在裡面依次呼叫執行緒池的pool.shutdown()pool.awaitTermination()即可。