1. 背景问题
- 描述问题
运维同事反馈上线滚动发布,旧实例接收的请求发生中断,没有处理完成。
为保证服务质量,期望发布时,旧实例需要将已有请求处理完成再回收。
本质其实就是微服务优雅下线的问题。 - 为什么要解决这个问题
未优雅下线会产生如下影响:
(1)业务处理中断
导致数据不完整,属于致命危害。
举例:文件写入中断,文件数据不完整;支付完成,本地业务处理失败。
当然程序可以通过一些健壮性的机制保证业务流程,比如业务补偿、幂等设计、状态机严格控制,这需要编程时考虑更多,即使能做到这些,也会带来维护成本,出现问题需要排查,甚至人工补偿数据完整性。
(2)已下线的服务继续提供服务
因微服务一般都有注册中心服务自动发现机制,在一定时间范围内,即使服务下线,注册中心和服务有心跳时间窗口,比如eureka默认是30秒一次心跳,这个时间窗口内,注册中心不会下线服务,会导致上游调用该服务大量报错,引发告警甚至熔断,业务请求大量失败。
2. 优雅下线理论上需要解决的问题
- 处理完已经接受的请求
(1)正常执行完成后台业务逻辑
(2)调用方接收正常处理完成的信号 - 不再接收新的请求
和运维同事了解了目前部署架构和发布方式,发布采用滚动发布的方式,k8s已经实现了停机时将服务从注册列表自动移除的机制。
这给应用层面省去了不少麻烦,不然得下线时挂钩子(shutdownHook)将服务主动从注册列表移除,还没那么简单,容器重启服务时,如果ip未发生变化,可能还得对外暴露接口,执行脚本将服务加入注册列表,需要一整套方案去保证。
DiscoveryManager.getInstance().shutdownComponent();
复制代码
3. 问题验证
- 重现
- Springboot对优雅关闭的支持
根据关闭打印日志可以看到,关闭相关类AnnotationConfigEmbeddedWebApplicationContext,父类AbstractApplicationContext提供了对优雅下线的扩展支持,源码如下:
/**
* Register a shutdown hook with the JVM runtime, closing this context
* on JVM shutdown unless it has already been closed at that time.
* <p>Delegates to {@code doClose()} for the actual closing procedure.
* @see Runtime#addShutdownHook
* @see #close()
* @see #doClose()
*/
@Override
public void registerShutdownHook() {
if (this.shutdownHook == null) {
// No shutdown hook registered yet.
this.shutdownHook = new Thread() {
@Override
public void run() {
synchronized (startupShutdownMonitor) {
doClose();
}
}
};
Runtime.getRuntime().addShutdownHook(this.shutdownHook);
}
}
/**
* Actually performs context closing: publishes a ContextClosedEvent and
* destroys the singletons in the bean factory of this application context.
* <p>Called by both {@code close()} and a JVM shutdown hook, if any.
* @see org.springframework.context.event.ContextClosedEvent
* @see #destroyBeans()
* @see #close()
* @see #registerShutdownHook()
*/
protected void doClose() {
if (this.active.get() && this.closed.compareAndSet(false, true)) {
if (logger.isInfoEnabled()) {
logger.info("Closing " + this);
}
LiveBeansView.unregisterApplicationContext(this);
try {
// Publish shutdown event.
publishEvent(new ContextClosedEvent(this));
}
catch (Throwable ex) {
logger.warn("Exception thrown from ApplicationListener handling ContextClosedEvent", ex);
}
// Stop all Lifecycle beans, to avoid delays during individual destruction.
try {
getLifecycleProcessor().onClose();
}
catch (Throwable ex) {
logger.warn("Exception thrown from LifecycleProcessor on context close", ex);
}
// Destroy all cached singletons in the context's BeanFactory.
destroyBeans();
// Close the state of this context itself.
closeBeanFactory();
// Let subclasses do some final clean-up if they wish...
onClose();
this.active.set(false);
}
}
复制代码
从关闭哦源码可以看出,publishEvent(new ContextClosedEvent(this));
会对注册了
- 为什么tomcat默认不会优雅关闭?
tomcat 未对关闭做默认处理,触发线程中断。
业务处理中断,调用方报错。 - jetty的优雅关闭有什么问题?
接收到的请求能继续处理,但是调用方会报错
Jetty中Server.stop()方法先关闭connections,然后处理队列中未完成的request,request完成后,由于connection已经关闭了,响应不能写回去了。
4. Springboot内嵌容器优雅停机的处理方案
- Spring Boot 2.3+ 版本,开箱即用
## 开启优雅停机, 如果不配置是默认IMMEDIATE, 立即停机
server.shutdown=graceful
## 优雅停机宽限期时间
spring.lifecycle.timeout-per-shutdown-phase=30s
复制代码
- 低版本
相当于手动实现高版本功能,粗暴一点可以写死,也可以做开关和关闭时间可配置化
Jetty 实现:
@Bean
public EmbeddedServletContainerFactory jettyEmbeddedServletContainerFactory() {
JettyEmbeddedServletContainerFactory factory = new JettyEmbeddedServletContainerFactory();
factory.addServerCustomizers(server -> {
server.setStopAtShutdown(false);
StatisticsHandler statisticsHandler = new StatisticsHandler();
statisticsHandler.setHandler(server.getHandler());
server.setHandler(statisticsHandler);
});
return factory;
}
复制代码
Tomcat 实现:
@Bean
public EmbeddedServletContainerCustomizer tomcatCustomizer() {
return container -> {
if (container instanceof TomcatEmbeddedServletContainerFactory) {
((TomcatEmbeddedServletContainerFactory) container).addConnectorCustomizers(new GracefulShutdown());
}
};
}
private static class GracefulShutdown implements TomcatConnectorCustomizer,
ApplicationListener<ContextClosedEvent> {
private static final Logger log = LoggerFactory.getLogger(GracefulShutdown.class);
private volatile Connector connector;
@Override
public void customize(Connector connector) {
this.connector = connector;
}
@Override
public void onApplicationEvent(ContextClosedEvent event) {
this.connector.pause();
Executor executor = this.connector.getProtocolHandler().getExecutor();
if (executor instanceof ThreadPoolExecutor) {
try {
ThreadPoolExecutor threadPoolExecutor = (ThreadPoolExecutor) executor;
threadPoolExecutor.shutdown();
if (!threadPoolExecutor.awaitTermination(30, TimeUnit.SECONDS)) {
log.warn("Tomcat thread pool did not shut down gracefully within 30s. Forceful shutdown...");
}
} catch (InterruptedException ex) {
Thread.currentThread().interrupt();
}
}
}
}
复制代码
5. 开发层面需要注意的事项说明
- 服务不存在绝对的优雅下线,一方面有断电、拔电源外在因素;还有kill -15后应用本身不能在业务正常下线时间范围内处理完成业务,触发 kill -9,发生业务中断。
- 开发需要关注以下几点
(1)评估每个服务停机处理完请求需要的下线时间
(2)保证应用在下线指定时间内的请求都能正常处理并且响应调用方
(3)手动对业务线程资源优雅下线资源回收
业务处理线程资源释放示例:
@Bean
public ExecutorService bizExecutorService() {
ExecutorService executorService = Executors.newFixedThreadPool(10);
// shutdownAndAwaitTermination 可以参考guava线程池优雅关闭
Runtime.getRuntime().addShutdownHook(new Thread(() -> shutdownAndAwaitTermination(executorService, 10l, TimeUnit.SECONDS)));
return executorService;
}
复制代码
(4)对生产发布因未在指定时间内完成的业务请求进行监控,并优化性能
6. 参考资料
© 版权声明
文章版权归作者所有,未经允许请勿转载。
THE END