原来的系统是个单体服务,导致逻辑越来越复杂,牵一发而动全身。为了提高系统的可扩展性,我们把原来的单体系统,按照功能拆分成不同的微服务。
我们所有的微服务都是部署在弹性云上的,希望在部署服务时能够做到无损发布。要做到这一点,以下几个步骤是需要实现的:
下面看下如何实现上面的需求。
有以下几种eureka注册中心服务下线的方式:
直接kill服务
这种方式简单粗暴,但是在这种情况下,虽然客户端已经停止服务了,但是仍然存在于注册中心列表中,会造成部分模块调用时出错,所以这个方案pass。
向Eureka service发送delete请求
http://{eureka-server:port}/eureka/apps/{application.name}/{instance.name}
这种方案只是取消注册服务,但是当eureka服务再一次接收到心跳请求时,会重新把这个实例注册到eureka上,所以这个方案也pass了。
客户端通知Eureka service下线
DiscoveryManager.getInstance().shutdownComponent();
eureka客户端可以通过上面一行代码主动通知注册中心下线,下线后也不会再注册到eureka上,这个方案符合我们的要求,但是我们需要确认这行代码需要在什么时候被调用?
在这里我们首先需要确定从eureka注册中心删除实例的时机,有以下几种想法:
@GetMapping("/shutdown") public void shutdown() { DiscoveryManager.getInstance().shutdownComponent(); }
在容器部署之前,先调用此接口下线,然后再执行部署操作。但是这样做有很大的弊端:1. 该接口不能暴露出去,同时为了避免其他人恶意调用,还需要加一些鉴权操作;2. 无法集成到部署脚本中,因为和弹性云团队的同学了解到,容器销毁前并不会执行control.sh里的stop方法,而是发送一个SIGTERM信号,所以没办法将该接口调用写到部署脚本中。因此如果采用这种方式,只能每个容器上线前手动调用该接口,风险太大,因为此方案不合适。
Runtime.getRuntime().addShutdownHook(new Thread(() -> { // 从eureka注册列表中删除实例 DiscoveryManager.getInstance().shutdownComponent(); // 休眠120S try { Thread.sleep(120 * 1000); } catch (Exception ignore) { } }));
JVM在接收到系统的SIGTERM信号后,会调用Shutdown Hook里的方法,这样注册一个这样的Shutdown Hook是不是就可以了呢?
经过测试发现并不完美,虽然下线时能够及时通知eureka服务下线改服务,但是同时Tomcat也会拒绝接收接下来的请求,druid线程池也会close;这样其他微服务由于缓存了改实例,还会有请求打到这个实例上,导致请求报错。
是什么原因导致上述情况的呢?翻阅Spring源码可以发现,SpringBoot在服务启动过程中,会自动注册一个Shutdown Hook,源码如下:
// org.springframework.boot.SpringApplication#refreshContext private void refreshContext(ConfigurableApplicationContext context) { this.refresh((ApplicationContext)context); if (this.registerShutdownHook) { try { // 注册shutdownHook context.registerShutdownHook(); } catch (AccessControlException var3) { } } }
SpringBoot在启动过程中,刷新Context之后,如果没有手动关闭registerShutdownHook(默认开启),则会注册一个Shutdown Hook。
// org.springframework.context.support.AbstractApplicationContext#registerShutdownHook @Override public void registerShutdownHook() { if (this.shutdownHook == null) { // No shutdown hook registered yet. this.shutdownHook = new Thread(SHUTDOWN_HOOK_THREAD_NAME) { @Override public void run() { synchronized (startupShutdownMonitor) { // shutdownHook真正需要执行的逻辑 doClose(); } } }; // 注册shutdownHook Runtime.getRuntime().addShutdownHook(this.shutdownHook); } }
Spring Shutdown Hook的具体执行逻辑,我们稍后分析;现在来看下如果JVM注册了多个Shutdown Hook,那么它们的执行顺序是怎么样的?
// java.lang.Runtime#addShutdownHook public void addShutdownHook(Thread hook) { SecurityManager sm = System.getSecurityManager(); if (sm != null) { sm.checkPermission(new RuntimePermission("shutdownHooks")); } ApplicationShutdownHooks.add(hook); }
// java.lang.ApplicationShutdownHooks /* The set of registered hooks */ private static IdentityHashMap<Thread, Thread> hooks; static synchronized void add(Thread hook) { if(hooks == null) throw new IllegalStateException("Shutdown in progress"); if (hook.isAlive()) throw new IllegalArgumentException("Hook already running"); if (hooks.containsKey(hook)) throw new IllegalArgumentException("Hook previously registered"); hooks.put(hook, hook); }
可以看到,当我们添加一个Shutdown Hook时,会调用ApplicationShutdownHooks.add(hook),向ApplicationShutdownHooks类下的静态变量private static IdentityHashMap<Thread, Thread> hooks里添加一个hook,hook本身是一个thread对象。
// java.lang.ApplicationShutdownHooks#runHooks /* Iterates over all application hooks creating a new thread for each * to run in. Hooks are run concurrently and this method waits for * them to finish. */ static void runHooks() { Collection<Thread> threads; synchronized(ApplicationShutdownHooks.class) { threads = hooks.keySet(); hooks = null; } for (Thread hook : threads) { hook.start(); } for (Thread hook : threads) { while (true) { try { hook.join(); break; } catch (InterruptedException ignored) { } } } }
上述源码是应用级hooks的执行逻辑,hook执行时调用的是tread类的start方法,所以多个hook是异步执行的,但是会等到所有hook全部执行完才会退出。
到这里,我们就可以确定方案2有问题的原因:虽然我们在自定义Shutdown Hook里自作聪明的sleep 120s,但是由于它和Spring Shutdown Hook执行并不是同步的,所以在自定义hook的睡眠过程中,spring同时也在做一些收尾工作,导致此时打到改实例上的请求报错。
既然自定义Shutdown Hook的方案行不通,那么是不是可以在Spring Shutdown Hook这里搞一些操作呢?接下来看下Spring Shutdown Hook的具体实现逻辑:
// org.springframework.context.support.AbstractApplicationContext#doClose protected void doClose() { if (this.active.get() && this.closed.compareAndSet(false, true)) { LiveBeansView.unregisterApplicationContext(this); // 1. Publish shutdown event. publishEvent(new ContextClosedEvent(this)); // 2. Stop all Lifecycle beans, to avoid delays during individual destruction. if (this.lifecycleProcessor != null) { this.lifecycleProcessor.onClose(); } // 3. Destroy all cached singletons in the context's BeanFactory. destroyBeans(); // 4. Close the state of this context itself. closeBeanFactory(); // 5. Let subclasses do some final clean-up if they wish... onClose(); // 6. Reset local application listeners to pre-refresh state. if (this.earlyApplicationListeners != null) { this.applicationListeners.clear(); this.applicationListeners.addAll(this.earlyApplicationListeners); } this.active.set(false); } }
上面源码只保留了关键代码,可以看到,Spring Shutdown Hook一共做了这些事情:
既然Spring Shutdown Hook执行逻辑的第一步是发布Context Close事件,那我们就可以创建一个listener监听此事件,然后在监听回调里执行从eureka注册列表中删除实例的逻辑。实现如下:
@Component public class EurekaShutdownConfig implements ApplicationListener<ContextClosedEvent>, PriorityOrdered { private static final Logger log = LoggerFactory.getLogger(EurekaShutdownConfig.class); @Override public void onApplicationEvent(ContextClosedEvent event) { try { log.info(LogUtil.logMsg("_shutdown", "msg", "eureka instance offline begin!")); DiscoveryManager.getInstance().shutdownComponent(); log.info(LogUtil.logMsg("_shutdown", "msg", "eureka instance offline end!")); log.info(LogUtil.logMsg("_shutdown", "msg", "start sleep 120S for cache!")); Thread.sleep(120 * 1000); log.info(LogUtil.logMsg("_shutdown", "msg", "stop sleep 120S for cache!")); } catch (Throwable ignore) { } } @Override public int getOrder() { return 0; } }
至此主动从eureka注册中心删除实例的时机就已经确定了。
application.yml
server: # 优雅关机策略 shutdown: graceful # 其他配置 ...
tomcat执行优雅关机的时机是在lifecycleProcessor.onClose(),在这里不详细展开说明了,可自行翻阅源码。
自定义线程池
@Configuration public class MyThreadTaskExecutor { @Bean public Executor taskExecutor() { ThreadPoolTaskExecutor taskExecutor = new ThreadPoolTaskExecutor(); // 线程池参数 taskExecutor.setCorePoolSize(8); taskExecutor.setMaxPoolSize(32); taskExecutor.setQueueCapacity(9999); taskExecutor.setKeepAliveSeconds(60); taskExecutor.setThreadNamePrefix("async-"); taskExecutor.setTaskDecorator(new TraceIdTaskDecorator()); // 服务停用前等待异步线程执行完成 taskExecutor.setWaitForTasksToCompleteOnShutdown(true); // 60S后强制关闭 taskExecutor.setAwaitTerminationSeconds(60); taskExecutor.initialize(); return taskExecutor; } }
自定义线程池和数据库连接池的关闭是在销毁bean时执行的。
至此,我们可以总结下当服务接收到SIGTERM信号后的处理逻辑:
如有谬误,欢迎指正。
本文来源:博客园,转载请注明出处!
来源地址:https://www.cnblogs.com/mervyn-hao/p/15840378.html
作为经典的软件需求工程畅销书,经由需求社区两大知名领袖结对全面修订和更新,覆盖新的主题、实例和指南,全方位讨论软件项目所涉及的所有需求开发和管理活动,介绍当下的所有实践。书中描述实用性强的、高效的、经过实际检验的端到端需求工程管理技术,通过丰富的实例来演示如何利用实践来减少订单变更,提高客户满意度,减少开发成本。
最新内容
© 2016 - 2024 chengxuzhixin.com All Rights Reserved.