背景

以往的单机应用会采用kill方式关闭应用服务,但是这种关闭应用的方式在springboot中会让当前应用将所有处理中的请求丢弃,返回失败响应。我们在处理重要业务逻辑要极力避免的这种响应失败在,所以我们需要一种更加好的的方式关闭springBoot应用。本文讲述了一种基于SpringBoot Actuator和tomcat回调的方式平滑关闭应用

基本思路

我们关闭一个微服务应用基本可以分为两大步骤

  • 关闭web应用服务器
  • 关闭spring容器

由于web应用服务器有多种所以下文描述的是如何平滑的关闭springboot中的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: :::::::,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) { String displayName = "Web";
if (event.getSource() instanceof AnnotationConfigApplicationContext ){
displayName = ((AnnotationConfigApplicationContext) event.getSource()).getDisplayName();
}/* Suspend all external requests*/
this.connector.pause();
/* Get ThreadPool For current connector */
Executor executor = this.connector.getProtocolHandler().getExecutor();
if (executor instanceof ThreadPoolExecutor) {
log.warn("当前{}应用准备关闭",displayName);
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("当前{}应用等待超过最大时长{}秒,将强制关闭", displayName,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)
if [ $count != ];then
curl -X POST "http://localhost:$SEVER_PORT/actuator/shutdown"
sleep
while(($count != ))
do
kill $boot_id
sleep
count=$(pgrep -f "$START_JAR" | wc -l)
done
echo "服务已停止: "
else
echo "服务未在运行"
fi
}

总结

本文主探究了如何真实生产环境中关闭基于Spring Boot 应用的实现,文中采用的是内嵌式的tomcat,如果采用其他 Web 容器也类似方式,希望这边文章有所帮助,若有错误或者不当之处,还请大家批评指正,一起学习交流。

参考链接

https://spring.io/guides/gs/actuator-service/

如何关闭Springboot应用服务的更多相关文章

  1. SpringBoot_01_正确、安全地停止SpringBoot应用服务

    二.参考资料 1.正确.安全地停止SpringBoot应用服务

  2. 关闭SpringBoot logo图标

    public static void main(String[] args) {// SpringApplication.run(LicenseApp.class, args); //关闭Spring ...

  3. 正确、安全地停止SpringBoot应用服务

    引言 Spring Boot,作为Spring框架对"约定优先于配置(Convention Over Configuration)"理念的最佳实践的产物,它能帮助我们很快捷的创建出 ...

  4. 【springboot】之利用shell脚本优雅启动,关闭springboot服务

    springbot开发api接口服务,生产环境中一般都是运行独立的jar,在部署过程中涉及到服务的优雅启动,关闭, springboot官方文档给出的有两种方式, 1.使用http shutdown ...

  5. 【积累】如何优雅关闭SpringBoot Web服务进程

    1.使用ps ef查出进程对应的pid. 2.使用kill -15 pid结束进程. 为什么不使用kill -9 pid,个人理解kill -15 pid更优雅,能在结束进程前执行spring容器清理 ...

  6. 优雅关闭springboot应用

    1.添加钩子函数,钩子函数中指定要调用的方法 @PostConstruct public void run() { this.zkClient.start(this); this.schedulerS ...

  7. SpringBoot实现优雅的关机

    最近在公司使用了 Springboot 项目, 发现在   linux  上 通过 java -jar 命令可以十分安全的运行, 但是 当我们需要关闭它的时候呢? 难道  登陆服务器 kill 线程? ...

  8. springboot 知识点

    ---恢复内容开始--- 1springBoot项目引入方式, 1,继承自父 project (需要没有付项目才能用,一般我们的项目都会有 父 项目 所以 这种方式不推荐 ,记住有这种方式 就可以了) ...

  9. SpringBoot+Vue前后端分离,使用SpringSecurity完美处理权限问题(一)

    当前后端分离时,权限问题的处理也和我们传统的处理方式有一点差异. 笔者前几天刚好在负责一个项目的权限管理模块,现在权限管理模块已经做完了,我想通过5-6篇文章,来介绍一下项目中遇到的问题以及我的解决方 ...

随机推荐

  1. niginx:duplicate MIME type "text/html" in nginx.conf 错误(转载)

    把nginx升级到最新以后,发现用原来的配置启动的时候会提示: duplicate MIME type "text/html" in /usr/local/nginx/conf/n ...

  2. connection closed by foreign host / Permissions 0620 for '/etc/ssh/ssh_host_ed25519_key' are too open 解决方案

    发生此次故障的原因: 在文件夹授权时 错误的执行了 chmod -R 755 / 本来只想授权当前文件夹的 结果... 然后就导致xshell连不上了 懵逼... 解决方案 将权限收回: 执行: ch ...

  3. Mycat的简介及安装

    Mycat简介: 1.1Mycat含义 简单的说,MyCAT就是: 一个彻底开源的,面向企业应用开发的“大数据库集群” 支持事务.ACID.可以替代Mysql的加强版数据库 一个可以视为“Mysql” ...

  4. 记一次Xmrig挖矿木马排查过程

    问题现象 Linux 服务器收到报警信息,主机 CPU 跑满. 自动创建运行 Docker 容器 xmrig, 导致其他运行中容器被迫停止. 问题原因 通过 top 命令可以看到有一个 xmrig 进 ...

  5. 初识ASP.NET CORE

    首先创建一个asp.net core web应用程序 第二步 目前官方预置了7种模板项目供我们选择.从中我们可以看出,既有我们熟悉的MVC.WebAPI,又新添加了Razor Page,以及结合比较流 ...

  6. vue cli3配置开发环境、测试环境、生产(线上)环境

    cli3创建vue项目是精简版的少了build和config这2个文件,所以配置开发环境.测试环境.生产环境的话需要自己创建env文件. 需要注意2点: 1.cli2创建项目生成的config文件里的 ...

  7. 万字综述,核心开发者全面解读PyTorch内部机制

    斯坦福大学博士生与 Facebook 人工智能研究所研究工程师 Edward Z. Yang 是 PyTorch 开源项目的核心开发者之一.他在 5 月 14 日的 PyTorch 纽约聚会上做了一个 ...

  8. WeChat-SmallProgram:组件 scroll-view 横向和纵向 案例

    scroll-view为滚动视图,分为水平滚动和垂直滚动.注意滚动视图垂直滚动时一定要设置高度否则的话scroll-view不会生效. 滚动视图常用的地方一般都是Item项比较多的界面,比如我的模块 ...

  9. Python python 函数参数:必选参数,默认参数

    import math # 函数的必选参数 '''函数的必选参数,指的是函数调用的时候必须传入的参数 ''' def cal (n): return n * n var = cal(2) '''上面的 ...

  10. Go深入学习之select

    select的用法 1)select只能用于channel的操作(写入.读出),而switch则更通用一些 2)select的case是随机的,而switch里的case是顺序执行 3)select要 ...