优雅停机主要应用在版本更新的时候,为了等待正在工作的线程全部执行完毕,然后再停止。
这就涉及到了springboot的核心机制之一——Actuator。
概念:actuator是制造术语,是指用于移动或控制某些物体的机械设备。 actuators可以通过很小的变化产生大量的运动。
这是官方给出的actuator的定义,正常人一看,根本看不懂嘛,总的来说,actuator就是个监控神器。
actuator的核心,它能够对应用程序进行监控及交互,springboot提供了大量的内置的endpoints(我们也可以自己扩展endpoint)。
下面列举了一些内置的endpoints:
ID | Description | Enabled by default |
---|---|---|
auditevents | 显示当前应用程序的审计事件信息。 | Yes,需要一个AuditEventRepository Bean |
beans | 显示应用程序中所有Spring bean的完整列表。 | Yes |
caches | 显示可用的缓存 | Yes |
conditions | 显示在配置和自动配置类上被评估的条件,以及它们匹配或不匹配的原因。 | Yes |
configprops | 显示所有@ConfigurationProperties的排序列表。 | Yes |
env | 从Spring的ConfigurableEnvironment中公开属性。 | Yes |
flyway | 显示已应用的所有Flyway数据库迁移。 | Yes |
health | 显示应用程序的健康信息。 | Yes |
httptrace | 显示HTTP跟踪信息(默认情况下,最后100个HTTP请求-响应交换)。 | Yes,需要一个AuditEventRepository Bean |
info | 显示任意应用程序信息。 | Yes |
integrationgraph | 显示Spring集成图。 | Yes |
loggers | 显示和修改应用程序中的记录器配置。 | Yes |
liquibase | 显示已应用的所有Liquibase数据库迁移。 | Yes |
metrics | 显示当前应用程序的“度量”信息。 | Yes |
mappings | 显示所有@RequestMapping路径的列表。 | Yes |
scheduledtasks | 显示应用程序中计划的任务。 | Yes |
sessions | 允许从Spring会话支持的会话存储中检索和删除用户会话。当使用Spring Session对反应性web应用程序的支持时不可用。 | Yes |
shutdown | 让应用程序优雅地关闭。 | No |
threaddump | 执行线程转储。 | Yes |
当你的应用是一个web应用的时候,诸如Spring MVC、Spring WebFlux或者Jersey等,可以使用下面的附加endpoint:
ID | Description | Enabled by default |
---|---|---|
heapdump | 返回一个hprof堆转储文件。 | Yes |
jolokia | 通过HTTP公开JMX bean(当Jolokia位于类路径上时,WebFlux不能使用它)。 | Yes |
logfile | 返回日志文件的内容(如果是log .file.name或log .file)。已经设置了路径属性)。支持使用HTTP范围头来检索日志文件的部分内容。 | Yes |
prometheus | 以可以被Prometheus服务器抓取的格式公开度量数据。 | Yes |
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
上面的表格可以看到,只有shutdown endpoint默认是关闭的,所以要先启用它。
application.yml:
management:
endpoint:
shutdown:
enabled: true
application.properties:
#启用shutdown
management.endpoint.shutdown.enabled=true
配置类:
package com.example.demo.Configuration;
import org.apache.catalina.connector.Connector;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.web.embedded.tomcat.TomcatConnectorCustomizer;
import org.springframework.boot.web.embedded.tomcat.TomcatServletWebServerFactory;
import org.springframework.boot.web.servlet.server.ServletWebServerFactory;
import org.springframework.context.ApplicationListener;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.event.ContextClosedEvent;
import java.util.concurrent.Executor;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
/**
* @ClassName ActuratorConfig
* @Description garceful shutdown
* @Author domi
* @Date 2019/11/01
**/
@Configuration
public class ActuratorConfig {
private static final int waitTime = 20; //ms
@Bean
public GracefulShutdown gracefulShutdown() {
return new GracefulShutdown();
}
@Bean
public ServletWebServerFactory servletContainer() {
TomcatServletWebServerFactory tomcatServletWebServerFactory = new TomcatServletWebServerFactory();
tomcatServletWebServerFactory.addConnectorCustomizers(gracefulShutdown());
return tomcatServletWebServerFactory;
}
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 contextClosedEvent) {
this.connector.pause();
Executor executor = this.connector.getProtocolHandler().getExecutor();
if (executor instanceof ThreadPoolExecutor) {
try {
ThreadPoolExecutor threadPoolExecutor = (ThreadPoolExecutor) executor;
log.info("shutdown has started!");
threadPoolExecutor.shutdown();
log.info("shutdown has ended!");
if (!threadPoolExecutor.awaitTermination(waitTime, TimeUnit.SECONDS)) {
log.warn("Tomcat 进程在" + waitTime + "秒内无法结束,尝试强制结束ing...");
}
log.info("shutdown is success!");
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
}
application.yml:
info:
app:
name: spring-boot-actuator
version: V1.0.0
test: test
management:
endpoint:
shutdown:
enabled: true
endpoints:
web:
exposure:
include: "*"
base-path: /monitor
server:
port: 8081
server:
port: 8080
Controller:
package com.example.demo.Contorller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;
import java.util.concurrent.atomic.AtomicInteger;
/**
* @ClassName HomeController
* @Description 模拟正在处理的多个线程
* @Author domi
* @Date 2019/11/01
**/
@RestController
@RequestMapping("/hello")
public class HomeController {
//计数器
public AtomicInteger started = new AtomicInteger();
public AtomicInteger ended = new AtomicInteger();
@RequestMapping(value = "/world", method = RequestMethod.GET)
public String testThread() {
System.out.println("[" + Thread.currentThread().getName() + "]{" + this + "}started:" + started.addAndGet(1));
try {
Thread.sleep(1000 * 30);
} catch (Exception ex) {
ex.printStackTrace();
}
System.out.println("[" + Thread.currentThread().getName() + "]{" + this + "}finished:" + ended.addAndGet(1));
return "hello";
}
}
启动类:
package com.example.demo;
import com.example.demo.Contorller.HomeController;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ApplicationContext;
@SpringBootApplication
public class DemoApplication {
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
}
测试方法:启动项目,浏览器访问http://localhost:8080/hello/world,多次刷新页面,控制台打出如下log:
然后通过在curl输入
curl -X POST http://localhost:8081/monitor/shutdown
curl工具下载链接
关闭项目。控制台打出如下log:
如果在awaitTermination设定的时间内,还是有线程没有关闭的话,将会强制结束线程。
参考链接:
Spring Boot官方文档:https://docs.spring.io/spring-boot/docs/2.2.0.RELEASE/reference/html/production-ready-features.html
Spring Boot 1.X和2.X优雅重启实战:https://www.cnblogs.com/bigbigwood/p/9977069.html
Shut down embedded servlet container gracefully:https://github.com/spring-projects/spring-boot/issues/4657