SpringBoot 1.X 优雅停机 ( shutdown gracefully )
来源:互联网 发布:淘宝商城捡漏 编辑:程序博客网 时间:2024/05/16 12:57
1:常规的关闭方式
该方式主要依赖Spring Boot Actuator的endpoint特性,具体步骤如下:
1) 在pom.xml中引入actuator依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
2) 开启shutdown endpoint
Spring Boot Actuator的shutdown endpoint默认是关闭的,因此在application.properties中开启shutdown endpoint:
#启用shutdown
endpoints.shutdown.enabled=true
#禁用密码验证
endpoints.shutdown.sensitive=false
3) 发送shutdown信号
shutdown的默认url为host:port/shutdown,当需要停止服务时,向服务器post该请求即可,如:
curl -X POST host:port/shutdown
将得到形如{"message":"Shutting down, bye..."}的响应
4) 安全设置
可以看出,使用该方法可以非常方便的进行远程操作,但是需要注意的是,正式使用时,必须对该请求进行必要的安全设置,比如借助spring-boot-starter-security进行身份认证:
a)pom.xml添加security依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
b)开启安全验证,在application.properties中变更配置,并
#开启shutdown的安全验证
endpoints.shutdown.sensitive=true
#验证用户名
security.user.name=admin
#验证密码
security.user.password=secret
#角色
management.security.role=SUPERUSER
c)指定路径、IP、端口
#指定shutdown endpoint的路径,这样一般的漏洞扫描就无法扫描到 shutdown 端口了。
#也可以统一指定所有endpoints的路径`management.context-path=/manage`
endpoints.shutdown.path=/your_custom_shutdown_path
#指定管理端口和IP
management.port=8081
management.address=127.0.0.1
5) 安全设置方法2
由于通常服务部属在机器上之后,是通过反向代理对公网提供服务的。而很多服务本身是无状态的,并不需要单独增加权限验证,因此为了停机而增加一个security是没有很大必要的。
那么另一个思路就是,将停机的 endpoint 在反向代理层限制访问,这样只有内网才能停机。这就大大增加了安全性。
以Nginx为例,配置起来也非常简单:
upstream sdkserver {
server 127.0.0.1:8274;
server 127.0.0.1:8277;
keepalive 40;
}
server {
listen 8888;
server_name sdk_back;
location /your_custom_shutdown_path {
deny all;
}
location / {
root /;
proxy_pass http://sdkserver;
}
}
这个配置中,有两个active-active的服务,对外端口为8888
同时这个服务直接拒绝任何对于 your_custom_shutdown_path 的访问,这样就只能在内网上进行停机了。
而一般生产网络是跟办公网络隔离的,所以相当于将安全托管给了生产网络。这个网络的访问都是追溯的,也不会有挖空心思的安全攻击。
2:常规做法中的潜在问题
实时上,利用 shutdown endpoint 直接进行关闭是非常粗暴的做法。为了验证这一点,给出样例程序。直接是最简单的SpringBoot样例。
当前 SpringBoot 版本 1.5.9。
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
</dependencies>
server.port=8080
#启用shutdown
endpoints.shutdown.enabled=true
#是否密码验证
endpoints.shutdown.sensitive=false
#是否启用密码校验
management.security.enabled=false
@ComponentScan(basePackages = {"qi.tech"})
@SpringBootApplication
public class Main {
public static void main(String[] args) {
ApplicationContext context = SpringApplication.run(Main.class, args);
Runtime.getRuntime().addShutdownHook(new Thread(new Runnable() {
@Override
public void run() {
HomeController one = context.getBean(HomeController.class);
System.out.println( one + " -> started: " + one.started.get() + " ended:" + one.ended.get() );
}
}));
}
}
@Controller
public class HomeController {
// 计数器
public AtomicInteger started = new AtomicInteger();
public AtomicInteger ended = new AtomicInteger();
@RequestMapping("/hello")
@ResponseBody
public String index() {
System.out.println( Thread.currentThread().getName() + " -> " + this + " Get one, got: " + started.addAndGet(1) );
try {
Thread.sleep( 1000*10); // 模拟一个执行时间很长的任务
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println( Thread.currentThread().getName() + " -> " + this + " Finish one, finished: " + ended.addAndGet(1) );
return "hello";
}
}
启动上述程序,然后在页面端刷新若干次,这样模拟后端收到多个请求。并且由于每个请求需要十秒钟来完成。然后,执行 POST /shutdown 控制SpringBoot停机。
在控制台看到类似如下输入:
上图表示收到了10个请求,并且在执行过程中收到了停机请求并产生了响应:
Closing org.springframework.boot.context.embedded.AnnotationConfigEmbeddedWebApplicationContext@14ec4505
并且在关机过程中,对执行中的线程进行了中断处理。
假设上述的过程不是暂停,而是实际进行业务操作,那么很可能出现业务异常终止。
这就不满足安全关闭的基础条件了。作为Tomcat应用,应该的关闭方式是:
暂停接受新请求,但是完成对已接收请求的处理,然后再进行关闭。
对Tomcat有一定理解的同学知道,Tomcat接受请求是通过Connector组件,而这个组件本身实现了LifeCycle接口,它是可以被暂停的,暂停过程只停止接受请求,但不会关闭容器本身。
因此,优雅停机,应该是先暂停 Tomcat 的 Connector,然后再等待执行线程执行完成,最后停止。当然,如果执行线程持续不返还,可以有一个Timeout,过了时间进行强杀。
至于为什么 shutdown endpoint 没有实现这个点,参见 GitHub 讨论:https://github.com/spring-projects/spring-boot/issues/4657
从官方的解释来看,当前关闭的流程是先关闭 ApplicationContext 然后触发容器关闭,官方也认同了这个流程是反的。
但是,因为实现翻转的次序很麻烦,有很多配置和适配,所以现在还没有做。
据说 2.0 已经的计划已经增加了这部分,然而2.0还没有正式发布。
3: 优雅停机
虽然默认没有实现真正的优雅关机,然而这个过程实现起来并不困难。直接按照上述讨论中给出来的例子融合到我们的代码里,改动很小:
@ComponentScan(basePackages = {"qi.tech"})
@SpringBootApplication
public class Main {
public static void main(String[] args) {
ApplicationContext context = SpringApplication.run(Main.class, args);
Runtime.getRuntime().addShutdownHook(new Thread(new Runnable() {
@Override
public void run() {
HomeController one = context.getBean(HomeController.class);
System.out.println( one + " -> started: " + one.started.get() + " ended:" + one.ended.get() );
}
}));
}
/**
* 用于接受shutdown事件
* @return
*/
@Bean
public GracefulShutdown gracefulShutdown() {
return new GracefulShutdown();
}
/**
* 用于注入 connector
* @return
*/
@Bean
public EmbeddedServletContainerCustomizer tomcatCustomizer() {
return new EmbeddedServletContainerCustomizer() {
@Override
public void customize(ConfigurableEmbeddedServletContainer container) {
if (container instanceof TomcatEmbeddedServletContainerFactory) {
((TomcatEmbeddedServletContainerFactory) container).addConnectorCustomizers(gracefulShutdown());
}
}
};
}
private static class GracefulShutdown implementsTomcatConnectorCustomizer, ApplicationListener<ContextClosedEvent> {
private static final Logger log = LoggerFactory.getLogger(GracefulShutdown.class);
private volatile Connector connector;
private final int waitTime = 100;
@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(waitTime, TimeUnit.SECONDS)) {
log.warn("Tomcat thread pool did not shut down gracefully within "
+ waitTime + " seconds. Proceeding with forceful shutdown");
}
} catch (InterruptedException ex) {
Thread.currentThread().interrupt();
}
}
}
}
}
这个代码本身是很简单的 GracefulShutdown 实现了两个接口:TomcatConnectorCustomizer 和 ApplicationListener。TomcatConnectorCustomizer 会在 EmbeddedServletContainerCustomizerBeanPostProcessor 中被统一初始化,即注入容器。
private void postProcessBeforeInitialization(
ConfigurableEmbeddedServletContainer bean) {
for (EmbeddedServletContainerCustomizer customizer : getCustomizers()) {
customizer.customize(bean);
}
}
private Collection<EmbeddedServletContainerCustomizer> getCustomizers() {
if (this.customizers == null) {
// Look up does not include the parent context
this.customizers = new ArrayList<EmbeddedServletContainerCustomizer>(
this.beanFactory
.getBeansOfType(EmbeddedServletContainerCustomizer.class,
false, false)
.values());
Collections.sort(this.customizers, AnnotationAwareOrderComparator.INSTANCE);
this.customizers = Collections.unmodifiableList(this.customizers);
}
return this.customizers;
}
AbstractApplicationEventMulticaster 中则会对各种事件系统事件进行传递,GracefulShutdown 则响应 ContextClosedEvent。并执行关闭处理。关闭的流程是先暂停 connector, 然后再停止后续的执行线程池。我们再来看一下实验结果。
所有请求正常执行完,没有任何异常中断发生。
参考:
https://github.com/spring-projects/spring-boot/issues/4657 Shut down embedded servlet container gracefully
https://github.com/corentin59/spring-boot-graceful-shutdown Spring-Boot-Graceful-Shutdown-Starter
https://www.cnblogs.com/lobo/p/5657684.html 正确、安全地停止SpringBoot应用服务
阅读全文
0 0
- SpringBoot 1.X 优雅停机 ( shutdown gracefully )
- SpringBoot-Actuator应用监控以及优雅停机
- java 优雅停机实现
- rocketmq的优雅停机
- (十六)优雅停机
- Failed to shutdown database console gracefully
- dubbo 优雅停机源码分析
- Java优雅停机实现[待编辑]
- Dubbo源代码分析九:优雅停机
- 优雅的断开连接--shutdown()
- 优雅的断开连接--shutdown()
- Linux关机重启停机(shutdown reboot halt)流程分析
- hsf dubbo学习九--服务降级,优雅停机,日志
- netty-epoll-序列化-ChannelBuffer-bootstrap-taskQueue-delayQueue-优雅停机
- 17.dubbo优雅停机、主机绑定、访问日志、服务容器
- 16、优雅的断开连接--shutdown()
- SpringBoot Unregistering JMX-exposed beans on shutdown
- springboot(六):如何优雅的使用mybatis
- KNN算法例子(java,scala,python 代码实现)
- C语言单链表的创建、插入、查找、删除、求长、排序、遍历
- 记一次在Android studio配置多版本出现的坑爹错误
- 高性能队列——Disruptor
- VisualVM使用方法
- SpringBoot 1.X 优雅停机 ( shutdown gracefully )
- Appium-环境安装
- Struts 2拦截器(Intercept)总结
- 使用shell脚本恢复修改文件的时间戳
- 八数码第七境界——A*之曼哈顿+康托展开判重+回溯记录路径+逆序数判无解
- 项目部署到阿里云服务上
- 使用抓包工具时手机连接代理步骤
- 信数金服决策引擎分享(二):灰度发布-冠军/挑战者试验的另一个应用
- [LUOGU1008]三连击