浅谈Springboot web应用优雅关闭
背景
当Springboot Web(2.3.0前) 应用通过nginx负载均衡, 应用节点进行滚动重启时, 发现nginx会将请求转发到处于shutdown阶段的节点. 此时由于应用内资源已释放(e.g singletion destruction), http请求将无法得到适当的处理, 从而影响系统运行/用户体验.
本文将会分析springboot web 2.3.0 前后的迭代, 比较gracefully shutdown 引入前后springboot web 应用对SIGTERM kill信号量处理区别.
web应用的优雅关闭
简单来讲, 当一个应用接收到SIGTERM 信号时(kill/kill -15)时, 会拒绝后续来自客户端的请求(此时, 客户端指一个负载组件 : nginx/网关/SLB). 随后主动等待一个grace period, 等待在途请求和线程池内任务的处理完毕, 随后正式开始释放.
需要指出的是, 如果要做到用户层面无感知, 不但web应用需要拥有优雅关闭能力, web应用前面的负载组件也要能够平滑将请求转发到未关闭的web节点上 —
e.g nginx proxy_next_upstream能够将请求重路由到未处于connection refused的server.
Springboot web 2.3.0前的关闭
整合spring-boot-starter-web的springboot应用在关闭时会经历:
核心函数:
org.springframework.context.support.AbstractApplicationContext#doClose
// 只保留核心
LiveBeansView.unregisterApplicationContext(this);
publishEvent(new ContextClosedEvent(this));
this.lifecycleProcessor.onClose();
destroyBeans();
closeBeanFactory();
onClose();
需要注意的是, 整合整合spring-boot-starter-web 模块的springboot应用, ServletWebServerApplicationContext 重写了 onClose() 函数, 并在函数中增加了关闭web server能力(模版方法设计模式中的勾子函数).
protected void onClose() {
super.onClose();
stopAndReleaseWebServer();
}
问题
上述资源释放流程中, web容器的关闭在最后一步, 既会存在短暂的spring bean & bean factory 释放但web容器依旧能拦截http请求的阶段, 此时http请求的处理需要已释放的bean资源, 容易引发 BeanCreationNotAllowedException
Springboot web 优雅关闭方案
优雅关闭在springboot web 2.3.0引入, 通过将embeded web容器适配优雅关闭函数, 在springboot 容器关闭早期(destroy bean前) 将web容器设置为拒绝连接状态. 并且在收到SIGTERM信号量时应用会暂停一段时间用于处理在途请求 & 线程池任务
2.3.0后springboot 关闭顺序
需要注意的是, 嵌入式web容器接口org.springframework.boot.web.server.WebServer新增优雅关闭相关函数定义
default void shutDownGracefully(GracefulShutdownCallback callback) {
callback.shutdownComplete(GracefulShutdownResult.IMMEDIATE);
}
Springboot web模块四大嵌入式web容器(tomcat undertow jetty reactor-netty)都定义了各自的优雅关闭实现, 需要指出的是除了undertow在优雅关闭阶段立即返回http_code 503(service unavailable)外, 其他web容器优雅关闭阶段都是通过拒绝请求(connection refuse)
Tomcat为例
Tomcat对应抽象接口org.springframework.boot.web.server.WebServer的shutDownGracefully函数由 org.springframework.boot.web.servlet.context.WebServerGracefulShutdownLifecycle调用, 该类实现了SmartLifecycle接口因此能在application context关闭的早期被调用.
Tomcat优雅关闭实现
private void doShutdown(GracefulShutdownCallback callback) {
List<Connector> connectors = getConnectors();
connectors.forEach(this::close);
try {
for (Container host : this.tomcat.getEngine().findChildren()) {
for (Container context : host.findChildren()) {
while (isActive(context)) {
if (this.aborted) {
logger.info("Graceful shutdown aborted with one or more requests still active");
callback.shutdownComplete(GracefulShutdownResult.REQUESTS_ACTIVE);
return;
}
Thread.sleep(50);
}
}
}
}
catch (InterruptedException ex) {
Thread.currentThread().interrupt();
}
logger.info("Graceful shutdown complete");
callback.shutdownComplete(GracefulShutdownResult.IDLE);
}
PS:写了一份简单的spring-boot-starter 适用于springboot 2.3.0前优雅关闭web应用(收到SIGTERM后立刻返回503状态码) 项目地址