关于 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()
即可.
来源: http://www.jianshu.com/p/c57d186eb454