spring_boot关闭与kill

kill

kill 可将指定的信息送至程序。

预设的信息为 SIGTERM(15),可将指定程序终止。就是默认 kill 传送的信息。

1
kill [-s <信息名称或编号>][程序] 或 kill [-l <信息编号>]
  • -l <信息编号>  若不加<信息编号>选项,则 -l 参数会列出全部的信息名称。
  • -s <信息名称或编号>  指定要送出的信息。
  • [程序]  [程序]可以是程序的PID或是PGID,也可以是工作编号。

最常用的信号是:

  • 1 (HUP):重新加载进程。
  • 9 (KILL):杀死一个进程。
  • 15 (TERM):正常停止一个进程。

有的文章说:kill -9 进程ID

是从内核级别把进程杀死。会使你的程序里执行到哪处于一种不知的状态。所以除非必要时刻不要使用kill -9 进程ID。

kill 进程ID

正常停止一个进程。是发送一个信号给你的程序告诉他,他要停止,他会走关闭流程。

什么情况会引发kill不掉要使用Kill -9

例如进程陷入无限循环、资源泄漏、线程死锁等。在这种情况下,标准的kill命令可能无法将进程正确地终止。

判断一个进程是否进入无限循环、资源泄漏、线程死锁等问题是一个复杂的任务,通常需要运行时监控和诊断工具来帮助我们进行分析。下面是一些常见的工具和方法:

  1. 监控进程的系统资源使用情况:使用操作系统的监控工具(如top、ps等)来观察进程的CPU使用率、内存占用情况等。如果发现一个进程占用过高的CPU或内存资源而无法恢复正常状态,那可能是进入了无限循环或资源泄漏的情况。
  2. 分析进程的日志文件:进程的日志文件可能包含关于异常、错误信息、死锁等的记录。通过仔细分析日志文件,可以定位到具体的代码段或操作可能导致进程陷入无限循环或死锁。
  3. 使用诊断工具:Java提供了一些用于诊断的工具,如jps、jstack、jmap、jconsole等。这些工具可以提供线程堆栈信息、内存快照、垃圾收集信息等,有助于分析进程中的问题。
  4. 执行代码审查:检查进程中的代码,特别是涉及到循环、锁、资源关闭等的地方。查看是否存在潜在的无限循环、不正确的锁使用、未及时释放资源等问题。

spring boot 优雅关闭

什么叫优雅结束?

  • 第一步:停止接收请求和内部线程。
  • 第二步:判断是否有线程正在执行。
  • 第三步:等待正在执行的线程执行完毕。
  • 第四步:停止容器。

优雅关闭

测试代码:

1
2
3
4
5
6
7
8
9
10
11
@GetMapping(value = "/test") 
public String test(){
log.info("test --- start");
try {
Thread.sleep(100000);
} catch (InterruptedException e) {
e.printStackTrace();
}
log.info("test --- end");
return "test";
}

注意这里是补获取异常没有抛出异常。

kill -15 pid

启动spring boot项目

访问测试接口

使用 kill -15 pid(kill pid) 来结束这个进程

这时观察日志(console): 发现test — start 和 test — end 还有打断异常输出。

所以在生产环境中,使用kill -15 是基本安全的,程序正常执行至完毕。

ConfigurableApplicationContext colse

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
import lombok.extern.slf4j.Slf4j; 
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@Slf4j
public class TestController implements ApplicationContextAware {

private ApplicationContext context;

@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
this.context = applicationContext;
}


@GetMapping(value = "/test")
public String test(){
log.info("test --- start");
try {
Thread.sleep(100000);
} catch (InterruptedException e) {
e.printStackTrace();
}
log.info("test --- end");
return "test";
}

/**
* 停机
*/
@PostMapping(value = "shutdown")
public void shutdown(){
ConfigurableApplicationContext cyx = (ConfigurableApplicationContext) context;
cyx.close();
}
}

程序在启动的时候向 JVM 注册了一个关闭钩子,我们在执行 colse 方法的时候会删除这个关闭钩子,JVM 就会知道这是需要停止服务。

访问shutdown接口。观察同kill -15。

actuator

这种方式是通过引入依赖的方式停止服务,actuator 提供了很多接口,比如健康检查,基本信息等等,我们也可以使用他来优雅的停机。

引入依赖:

1
2
3
4
<dependency> 
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>

application.yml:

1
2
3
4
5
6
7
8
9
10
management: 
endpoints:
web:
exposure:
include: shutdown
endpoint:
shutdown:
enabled: true
server:
port: 8888

访问:http://127.0.0.1:8888/actuator/shutdown

10 秒以后再停止服务

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
import org.apache.catalina.connector.Connector; 
import org.springframework.boot.web.embedded.tomcat.TomcatConnectorCustomizer;
import org.springframework.context.ApplicationListener;
import org.springframework.context.event.ContextClosedEvent;

import java.util.concurrent.Executor;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

public class ElegantShutdownConfig implements TomcatConnectorCustomizer, ApplicationListener<ContextClosedEvent> {

private volatile Connector connector;
private final int waitTime = 10;

@Override
public void customize(Connector connector) {
this.connector = connector;
}

@Override
public void onApplicationEvent(ContextClosedEvent event) {
connector.pause();
Executor executor = connector.getProtocolHandler().getExecutor();
if (executor instanceof ThreadPoolExecutor) {
try {
ThreadPoolExecutor threadPoolExecutor = (ThreadPoolExecutor) executor;
threadPoolExecutor.shutdown();
if (!threadPoolExecutor.awaitTermination(waitTime, TimeUnit.SECONDS)) {
System.out.println("请尝试暴力关闭");
}
} catch (InterruptedException ex) {
System.out.println("异常了");
Thread.currentThread().interrupt();
}
}

}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
import com.ymy.config.ElegantShutdownConfig; 
import lombok.extern.slf4j.Slf4j;
import org.apache.catalina.connector.Connector;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
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.ConfigurableApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.event.ContextClosedEvent;

import java.util.concurrent.Executor;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

@SpringBootApplication
public class ShutdownServerApplication {

public static void main(String[] args) {
ConfigurableApplicationContext run = SpringApplication.run(ShutdownServerApplication.class, args);
run.registerShutdownHook();
}


@Bean
public ElegantShutdownConfig elegantShutdownConfig() {
return new ElegantShutdownConfig();
}

@Bean
public ServletWebServerFactory servletContainer() {
TomcatServletWebServerFactory tomcat = new TomcatServletWebServerFactory();
tomcat.addConnectorCustomizers(elegantShutdownConfig());
return tomcat;
}
}

启动项目,再访问接口test接口,因为test也是10秒,10秒后再停止服务,操作需要一定的时间,所以test不会被打断,不会出现打断日志。

销毁前数据备份操作

如果我想在服务停止的时候做点备份操作啥的,应该怎么做呢?其实很简单在你要执行的方法上添加一个注解即可:@PreDestroy。

  • Destroy:消灭、毁灭。
  • pre:前缀缩写。

所以合在一起的意思就是在容器停止之前执行一次,你可以在这里面做备份操作,也可以做记录停机时间等。

新增服务停止备份工具类:DataBackupConfig.java。

1
2
3
4
5
6
7
8
9
10
11
12
13
import org.springframework.context.annotation.Configuration; 

import javax.annotation.PreDestroy;

@Configuration
public class DataBackupConfig {

@PreDestroy
public void backData(){

System.out.println("正在备份数据。。。。。。。。。。。");
}
}

用上面的优雅关闭,可以看到执行打印日志。

结合nginx已连接的请求去掉负载后是否成功返回文章

可以发现,重启流程为修改ningx负载,重新加载nginx配置文件,等一会儿,使用kill (kill -15)等优雅关闭spring boot 服务可以基本保证程序可控。

其它的风险

正常情况按照上面的操作步骤就可以了。

但是还要看代码编写,比如如果在sleep的地方不是只打印日志或者异常出现在事务中,代码就会被中断,如果没有事务的保护,数据就会出错,代码要有可重入性、强壮性。

还有一些其他情况比如服务器断电等情况,还是未知情况。


spring_boot关闭与kill
http://hanqichuan.com/2023/06/30/spring/spring_boot关闭与kill/
作者
韩启川
发布于
2023年6月30日
许可协议