背景
最近在搞云化项目的启动脚本,觉得以往kill方式关闭服务项目太粗暴了,这种kill关闭应用的方式会让当前应用将所有处理中的请求丢弃,响应失败。这种形式的响应失败在处理重要业务逻辑中是要极力避免的,所以我们需要一种更加优雅的方式关闭springBoot应用。
基本思路
首先我们关闭一个微服务应用可以分为两大步骤
- 关闭web应用服务器
- 关闭spring容器
我项目中使用的是内置的tomcat服务器,所以本文描述的是如何平滑的关闭tomcat应用。SpringBoot Actuator中提供了shutdown端点,利用此端点可以http的方式远程关闭spring 容器,下文讲述了如何使用SpringBoot Actuator的shutdown。
开启Shutdown Endpoint
Spring Boot Actuator 是 Spring Boot 的一大特性,它提供了丰富的功能来帮助我们监控和管理生产环境中运行的 Spring Boot 应用。我们可以通过 HTTP 或者 JMX 方式来对我们应用进行管理,除此之外,它为我们的应用提供了审计,健康状态和度量信息收集的功能,能帮助我们更全面地了解运行中的应用。
引入Actuator
本项目基于gradle构建,引入 " spring-boot-starter-actuator
"如下
api('org.springframework.boot:spring-boot-starter-actuator:2.2.5.RELEASE')
开放端口
Spring Boot Actuator 采用向外部暴露 Endpoint (端点)的方式来让我们与应用进行监控和管理,引入 spring-boot-starter-actuator
之后,就需要启用我们需要的 Shutdown Endpoint。在application.yml中添加如下配置。
management: endpoints: web: exposure: include: "httptrace,health,shutdown" ## 健康检查根路径 base-path: "/actuator" endpoint: shutdown: enabled: true health: show-details: always
建议在include中根据自己的需要开放对应的端口,最好不要直接写“*”。这里由于项目中需要健康检查,所以添加了health,。
添加shutdown过滤器
一般来说使用shutdown端口是需要做权限控制的,但是由于这个项目有部署的时候,有对应的网关,所以这里就比较简单的增加了一个白名单功能。根据配置文件,来控制对应的ip是否可以访问此端口。
1. 添加ActuatorFilter
@Slf4j @RefreshScope public class ActuatorFilter implements Filter { public static final String UNKNOWN = "unknown"; @Value("${shutdown.whitelist}") private String[] shutdownIpWhitelist; @Override public void destroy() { } @Override public void doFilter(ServletRequest srequest, ServletResponse sresponse, FilterChain filterChain) throws IOException, ServletException { HttpServletRequest request = (HttpServletRequest) srequest; String ip = this.getIpAddress(request); log.info("访问shutdown的机器的原始IP:{}", ip); if (!isMatchWhiteList(ip)) { sresponse.setContentType("application/json"); sresponse.setCharacterEncoding("UTF-8"); PrintWriter writer = sresponse.getWriter(); writer.write("{\"code\":401,\"error\":\"IP access forbidden\"}"); writer.flush(); writer.close(); log.warn("ip:{}禁止shutdown", ip); return; } filterChain.doFilter(srequest, sresponse); } @Override public void init(FilterConfig arg0) throws ServletException { log.info("Actuator filter is init....."); } /** * 匹配是否是白名单 */ private boolean isMatchWhiteList(String ip) { List<String> list = Arrays.asList(shutdownIpWhitelist); return list.stream().anyMatch(item -> ip.startsWith(item)); } /** * 获取用户真实IP地址,不使用request.getRemoteAddr();的原因是有可能用户使用了代理软件方式避免真实IP地址, * 可是,如果通过了多级反向代理的话,X-Forwarded-For的值并不止一个,而是一串IP值,究竟哪个才是真正的用户端的真实IP呢? * 答案是取X-Forwarded-For中第一个非unknown的有效IP字符串。 * * 如:X-Forwarded-For:192.168.1.110, 192.168.1.120, 192.168.1.130, 192.168.1.100 * * 用户真实IP为: 192.168.1.110 */ private String getIpAddress(HttpServletRequest request) { String ip = request.getHeader("x-forwarded-for"); if (StringUtils.isBlank(ip) || UNKNOWN.equalsIgnoreCase(ip)) { ip = request.getHeader("Proxy-Client-IP"); } if (StringUtils.isBlank(ip) || UNKNOWN.equalsIgnoreCase(ip)) { ip = request.getHeader("WL-Proxy-Client-IP"); } if (StringUtils.isBlank(ip) || UNKNOWN.equalsIgnoreCase(ip)) { ip = request.getHeader("HTTP_CLIENT_IP"); } if (StringUtils.isBlank(ip) || UNKNOWN.equalsIgnoreCase(ip)) { ip = request.getHeader("HTTP_X_FORWARDED_FOR"); } if (StringUtils.isBlank(ip) || UNKNOWN.equalsIgnoreCase(ip)) { ip = request.getRemoteAddr(); } return ip; } }
这里注意不能在类ActuatorFilter 上加注解@Component,加上改过滤器会过滤所有url。
2.添加过滤器Config
@Configuration public class WebFilterConfig extends WebMvcConfigurationSupport { @Bean public ActuatorFilter getActuatorFilter() { return new ActuatorFilter(); } @Bean public FilterRegistrationBean setShutdownFilter(ActuatorFilter actuatorFilter) { FilterRegistrationBean<ActuatorFilter> registrationBean = new FilterRegistrationBean<>(); registrationBean.setFilter(actuatorFilter); registrationBean.setName("actuatorFilter"); registrationBean.addUrlPatterns("/actuator/shutdown"); return registrationBean; } }
3.添加白名单配置
application.yml中添加如下配置
shutdown: whitelist: 0:0:0:0:0:0:0:1,127.0.0.1
到这里我们的shutdown配置工作就算完成了。当启动应用后,只能本地以POST 方式请求对应路径的“ http://host:port/actuator/shutdown
“”来实现springboot容器的关闭。
关闭Tomcat
要平滑关闭 Spring Boot 应用的前提就是首先要关闭其内置的 Web 容器,不再处理外部新进入的请求。为了能让应用接受关闭事件通知的时候,保证当前 Tomcat 处理所有已经进入的请求,我们需要实现 TomcatConnectorCustomizer 接口,此接口是实现自定义 Tomcat Connector 行为的回调接口。
自定义 Connector
Connector 属于 Tomcat 抽象组件,功能就是用来接收外部请求、内部传递,并返回响应内容,是Tomcat 中请求处理和响应的重要组。Connector 具体实现有 HTTP Connector 和 AJP Connector。
通过定制 Connector 的行为,我们就可以允许在请求处理完毕后进行 Tomcat 线程池的关闭,具体实现代码如下:
@Slf4j public class CustomShutdown implements TomcatConnectorCustomizer, ApplicationListener<ContextClosedEvent> { private static final int TIME_OUT = 30; private volatile Connector connector; @Override public void customize(Connector connector) { this.connector = connector; } @Override public void onApplicationEvent(ContextClosedEvent event) { /* Suspend all external requests*/ this.connector.pause(); /* Get ThreadPool For current connector */ Executor executor = this.connector.getProtocolHandler().getExecutor(); if (executor instanceof ThreadPoolExecutor) { log.warn("当前Web应用准备关闭"); try { ThreadPoolExecutor threadPoolExecutor = (ThreadPoolExecutor) executor; /* Initializes a shutdown task after the current one has been processed task*/ threadPoolExecutor.shutdown(); if (!threadPoolExecutor.awaitTermination(TIME_OUT, TimeUnit.SECONDS)) { log.warn("当前应用等待超过最大时长{}秒,将强制关闭", TIME_OUT); /* Try shutDown Now*/ threadPoolExecutor.shutdownNow(); if (!threadPoolExecutor.awaitTermination(TIME_OUT, TimeUnit.SECONDS)) { log.error("强制关闭失败", TIME_OUT); } } } catch (InterruptedException e) { Thread.currentThread().interrupt(); } } } }
上述代码定义的 TIMEOUT 变量为 Tomcat 线程池延时关闭的最大等待时间,一旦超过这个时间就会强制关闭线程池,所以我们可以通过控制 Tomcat 线程池的关闭时间,(当然了这个也可以写成可配的) 来实现优雅关闭 Web 应用的功能。同时 CustomShutdown 实现了 ApplicationListener<ContextClosedEvent> 接口,意味着我们会监听着 Spring 容器关闭的事件,即当前的 ApplicationContext 执行 close 方法。
添加 Connector 回调
在启动过程中将定制的Connetor回调添加到内嵌的 Tomcat 容器中,然后等待执行。
@Configuration public class ShutdownConfig { @Bean public CustomShutdown customShutdown() { return new CustomShutdown(); } @Bean public ConfigurableServletWebServerFactory webServerFactory(final CustomShutdown customShutdown) { TomcatServletWebServerFactory tomcatServletWebServerFactory = new TomcatServletWebServerFactory(); tomcatServletWebServerFactory.addConnectorCustomizers(customShutdown); return tomcatServletWebServerFactory; } }
这里的 TomcatServletWebServerFactory 是 Spring Boot 实现内嵌 Tomcat 的工厂类。其他的 Web 容器,也有对应的工厂类如 JettyServletWebServerFactory,UndertowServletWebServerFactory。他们共都是继承抽象类 AbstractServletWebServerFactory。AbstractServletWebServerFactory提供了 Web 容器默认的公共实现,如应用上下文设置,会话管理等。 到这里我们的Tomcat平滑关闭就ok了
添加启动脚本
实际生产中我都会制作jar 然后发布。通常应用的启动和关闭操作流程是固定且重复的,以避免出现人为的差错,并且方便使用,提高操作效率,一般会配上对应的程序启动脚本来控制程序的启动和关闭。
对应关闭操作的shell脚本部分如下所示。
SEVER_PORT= export START_JAR_NAME="test-*.jar" START_JAR=$(ls $PRG_HOME | grep $START_JAR_NAME) stop() { echo $"Stoping : " boot_id=$(pgrep -f "$START_JAR") count=$(pgrep -f "$START_JAR" | wc -l) ];then curl -X POST "http://localhost:$SEVER_PORT/actuator/shutdown" )) do kill $boot_id count=$(pgrep -f "$START_JAR" | wc -l) done echo "服务已停止: " else echo "服务未在运行" fi }
总结
本文主要探究了如何对优雅关闭基于Spring Boot 内嵌 Tomcat 的 Web 应用的实现,如果采用其他 Web 容器也类似方式,希望这边文章有所帮助,若有错误或者不当之处,还请大家批评指正,一起学习交流。
参考链接
https://www.cnblogs.com/one12138/p/11241274.html