关于Spring Boot、Spring Cloud应用的优雅停机,平时经常会被问到,这也是实际应用过程中,必须要掌握的点。
在40岁老架构师 尼恩的读者社区(50+)中,最近有小伙伴拿到了一线互联网企业如美团、拼多多、极兔、有赞、希音的面试资格,遇到一几个很重要的面试题:
与之类似的、其他小伙伴遇到过的问题还有:
这里尼恩给大家做一下系统化、体系化的梳理,使得大家可以充分展示一下大家雄厚的 “技术肌肉”,让面试官爱到 “不能自已、口水直流”。
也一并把这个题目以及参考答案,收入咱们的 《尼恩Java面试宝典》V77版本,供后面的小伙伴参考,提升大家的 3高 架构、设计、开发水平。
最新《尼恩 架构笔记》、《尼恩高并发三部曲 》、《尼恩Java面试宝典》 的PDF文件,请到文末公号【技术自由圈】取。
java程序运行在JVM上,有很多情况可能会突然崩溃掉,比如OOM、用户强制退出、业务其他报错。。。等一系列的问题可能导致我们的进程挂掉。
这时候,我们就有服务改进、新版本升级的需求。
如果我们要升级某个服务,前提就是要进行服务的优雅下线。
什么才是SpringCloud 优雅下线?
那么,咱们常用的 kill PID,它是优雅的下线策略吗?
当然不是。
首先,优雅下线是目标,而不是手段,它是一个相对的概念。
虽然kill PID 不是优雅下线,并且kill PID和kill -9 PID都是暴力杀死服务,相对于kill -9 PID来说,kill PID就是优雅的。
那么,到底什么才是SpringCloud 优雅下线呢?
包括以下内容:
那么SpringCloud 优雅下线该如何实现呢?
要介绍清楚 SpringCloud 优雅下线实现机制,必须首先从JVM的优雅退出的基础知识开始。
JVM的优雅退出机制,主要是通过 Hook实现的。
JVM有shutdwonHook机制,中文习惯叫优雅退出。
VM的优雅退出Hook,和linux系统中执行SIGTERM(kill -15 或者 svc -d)时,退出前执行的一些操作。
首先看看,JVM 退出的钩子函数 的应用场景。
我们的java程序运行在JVM上,有很多情况可能会突然崩溃掉,比如:
一系列的问题,可能导致我们的JVM 进程挂掉。
JVM 退出的钩子函数是指在 JVM 进程即将退出时,自动执行用户指定的代码段。
这个功能的应用场景比较广泛,例如:
总之,钩子函数可以在 JVM 退出时执行一些自定义的操作,以便更好地管理和控制程序的运行。
40岁老架构师尼恩的提示:
应用的优雅关闭非常重要,可以在关闭之前做一些记录操作、补救操作。
至少,能知道咱们的JVM进程,什么时间,什么原因发生过异常退出。
在java程序中,可以通过添加关闭钩子函数,实现在程序退出时关闭资源、优雅退出的功能。
如何做呢?
主要就是通过Runtime.addShutDownHook(Thread hook)
来实现的。
Runtime.addShutdownHook(Thread hook)
是 Java 中的一个方法,用于在 JVM 关闭时注册一个线程来执行清理操作。
Runtime.addShutdownHook(Thread hook)
每一次调用,就是注册一个线程,参考代码如下:
// 添加hook thread,重写其run方法
Runtime.getRuntime().addShutdownHook(new Thread(){
@Override
public void run() {
System.out.println("this is hook demo...");
// jvm 退出的钩子逻辑
}
});
Runtime.addShutdownHook(Thread hook)
可以调用多次,从而注册多个线程。
当 JVM 即将关闭时,会按照注册的顺序依次执行这些线程,以便进行一些资源释放、日志记录或其他清理操作。
这个方法可以在应用程序中用来确保在程序退出前执行一些必要的清理工作,例如关闭数据库连接或释放文件句柄等。
下面我们来简单看一个Runtime.addShutDownHook(Thread hook)
使用示例
// 创建HookTest,我们通过main方法来模拟应用程序
public class HookTest {
public static void main(String[] args) {
// 添加hook thread,重写其run方法
Runtime.getRuntime().addShutdownHook(new Thread(){
@Override
public void run() {
System.out.println("this is hook demo...");
// jvm 退出的钩子逻辑
}
});
int i = 0;
// 这里会报错,jvm回去执行hook thread
int j = 10/i;
System.out.println("j" + j);
}
}
执行之后,结果如下:
Exception in thread "main" java.lang.ArithmeticException: / by zero
at hook.HookTest.main(HookTest.java:23)
this is hook demo...
Process finished with exit code 1
总结:我们主动写了一个报错程序,在程序报错之后,钩子函数还是被执行了。
经验证,我们是可以通过对Runtime添加钩子函数,来完成退出时的善后工作。
既然JDK提供的这个方法可以注册一个JVM关闭的钩子函数,那么这个函数在什么情况下会被调用呢?
上述我们展示了在程序异常情况下会被调用,还有没有其他场景呢?
Runtime.addShutDownHook(Thread hook)
触发场景 ,详见下图
40岁老架构师尼恩提示:强制关闭 不能 进行 Runtime.addShutDownHook(Thread hook)的触发,
上图的第三个分支,展示得清清楚楚
Spring/SpringBoot应用,如何手动添加钩子函数呢?
Spring/SpringBoot 提供了一个方法:
// 通过这种方式来添加钩子函数
ApplicationContext.registerShutdownHook();
ApplicationContext.registerShutdownHook()
方法是 Spring 框架中的一个方法,用于注册一个 JVM 关闭的钩子(shutdown hook)。
当 JVM 关闭时,Spring 容器可以优雅地关闭并释放资源,从而避免了可能的资源泄漏或其他问题。
ApplicationContext.registerShutdownHook()
方法应该在 Spring 应用程序的 main
方法中调用,以确保在 JVM 关闭时 Spring 容器能够正确地关闭。
下面是 Spring 官方的原文介绍:
spring通过JVM实现注册退出钩子的源码如下:
// 通过源码可以看到,
@Override
public void registerShutdownHook() {
if (this.shutdownHook == null) {
// No shutdown hook registered yet.
this.shutdownHook = new Thread() {
@Override
public void run() {
synchronized (startupShutdownMonitor) {
doClose();
}
}
};
// 也是通过这种方式来添加
Runtime.getRuntime().addShutdownHook(this.shutdownHook);
}
}
// 重点是这个doClose()方法
protected void doClose() {
// Check whether an actual close attempt is necessary...
if (this.active.get() && this.closed.compareAndSet(false, true)) {
if (logger.isInfoEnabled()) {
logger.info("Closing " + this);
}
LiveBeansView.unregisterApplicationContext(this);
try {
// Publish shutdown event.
publishEvent(new ContextClosedEvent(this));
}
catch (Throwable ex) {
logger.warn("Exception thrown from ApplicationListener handling ContextClosedEvent", ex);
}
// Stop all Lifecycle beans, to avoid delays during individual destruction.
if (this.lifecycleProcessor != null) {
try {
this.lifecycleProcessor.onClose();
}
catch (Throwable ex) {
logger.warn("Exception thrown from LifecycleProcessor on context close", ex);
}
}
// Destroy all cached singletons in the context's BeanFactory.
destroyBeans();
// Close the state of this context itself.
closeBeanFactory();
// Let subclasses do some final clean-up if they wish...
onClose();
// Switch to inactive.
this.active.set(false);
}
}
spring里registerShutdownHook
的源码所示,就是注册一个jvm的shutdownHook
钩子函数。jvm退出前会执行这个钩子函数。
通过源码,可以看到:doClose()
方法会执行bean的destroy()
,也会执行SmartLifeCycle
的stop()
方法,我们就可以通过重写这些方法来实现对象的关闭,生命周期的管理,实现平滑shutdown、优雅关闭。
spring 为何在容器销毁时自动 调用destroy()等方法? 就是这里的 destroyBeans()
方法的执行。 所以,这里特别关注的是 destroyBeans()
方法。
destroyBeans()
是 Spring 框架中的一个方法,它是在 Spring 容器关闭时调用的方法,用于销毁所有的单例 bean。
在 Spring 容器关闭时,会依次调用所有单例 bean 的 destroy()
方法,而 destroyBeans()
方法就是用于触发这个过程的。
在 destroy()
方法中,我们可以释放资源、关闭连接等操作,以确保应用程序正确地关闭。
40岁老架构师尼恩的提示:
destroy()
方法只在单例 bean 中才会被调用,而对于原型 bean,Spring 容器不会调用其
destroy()
方法。
两个方式:
DisposableBean
接口,并重写了其中的 destroy()
方法@PreDestroy
注解来指定销毁方法destroy()
方法是在 Spring 容器销毁时调用的方法,用于释放资源或执行清理操作。
方式一: 实现了 DisposableBean
接口,并重写了其中的 destroy()
方法
下面是destroy()
方法如何使用的一个示例:
public class MyBean implements DisposableBean {
private Resource resource;
public void setResource(Resource resource) {
this.resource = resource;
}
// 实现 DisposableBean 接口中的 destroy() 方法
@Override
public void destroy() throws Exception {
// 释放资源
if (this.resource != null) {
this.resource.release();
}
}
}
在这个示例中,MyBean
实现了 DisposableBean
接口,并重写了其中的 destroy()
方法。
在 destroy()
方法中,我们释放了 resource
资源。
当 Spring 容器销毁时,会自动调用 MyBean
的 destroy()
方法,从而释放资源。
方式二:使用 @PreDestroy
注解来指定销毁方法
除了实现 DisposableBean
接口,还可以使用 @PreDestroy
注解来指定销毁方法。例如:
public class MyBean {
private Resource resource;
public void setResource(Resource resource) {
this.resource = resource;
}
// 使用 @PreDestroy 注解指定销毁方法
@PreDestroy
public void releaseResource() {
if (this.resource != null) {
this.resource.release();
}
}
}
在这个示例中,MyBean
没有实现 DisposableBean
接口,而是使用 @PreDestroy
注解指定了销毁方法 releaseResource()
。当 Spring 容器销毁时,会自动调用这个方法。
实际上, registerShutdownHook()
钩子方法,在新的Springboot 版本中,不需要手动调用,已经被自动的执行了。
看一个简单的应用, 尼恩带大家,一步一步的翻翻源码:
顺着run的调用链路,继续往里翻看,会看到refreshContext
方法的执行
关键就在于这个 refreshContext
方法
继续往里翻看,发现一个秘密: 自动调用了registerShutdownHook()
方法
好吧,终于回到了 registerShutdownHook()源码 ,这个上面详细介绍了的
细心的小伙伴,可能发现那儿有个条件,如果条件不满足,基本上就跳过去了。
不着急。
40岁老架构师尼恩崇尚动手,咱们单步执行一下,发现一个秘密:条件默认就是true
在分布式微服务场景下, SpringCloud 微服务实例 都是通过 注册中心如 Eureka /nacos 进行实例管理的。
但如果的务发现组件使用的是 Eureka,那么默认最长会有 90 秒的延迟,其他应用才会感知到该服务下线,
这意味着:
该实例下线后的 90 秒内,其他服务仍然可能调用到这个已下线的实例。
Spring Boot 应用 退出的时候,如何在 Eureka 进行实例的主动删除呢?
可以借助 Spring Boot 应用的 Shutdown hook,结合 Eureka 的Client API,达到微服务实例优雅下线的目标。
Eureka 的两个核心的 Client API 如下:
eurekaAutoServiceRegistration.start()
方法时,当前服务向 Eureka 注册中心注册服务;eurekaAutoServiceRegistration.stop()
方法时,当前服务会向 Eureka 注册中心进行反注册,注册中心收到请求后,会将此服务从注册列表中删除。借助 Spring Boot 应用的 Shutdown hook ,微服务实例优雅下线的目标, 源码如下:
执行的结果,如下:
40岁老架构师在自己的 技术自由圈( Future Super Architect Community 未来 超级 架构师社区)做个调查,大部分的线上项目,现在都使用了Nacos 作为注册中心。
那么,Nacos 是否支持微服务实例的 上线和下线呢?
理论上,Nacos 不仅仅支持优雅的上线和下线,而且可以通过控制台、API 或 SDK 进行操作。
在控制台上线服务时,可以选择“上线方式”,有“快速上线”和“灰度发布”两种方式。当然,这个是需要运维人员配合的。其中,“快速上线”会直接将服务实例上线,而“灰度发布”则会先将服务实例发布到灰度环境,等待一段时间后再逐步将其发布到生产环境。
这里不关注运维人员的活,咱们专注的是开发、架构师的活儿。
在 API 或 SDK 中,可以使用以下方法进行上线和下线操作:
registerInstance
接口,指定服务名、IP、端口等信息即可。deRegisterInstance
接口,指定实例 ID 即可。可以借助 Spring Boot 应用的 Shutdown hook,结合 Eureka 的nacos API,达到微服务实例优雅下线的目标。
实现的方式和 前面的Eureka 类似, 仅仅是API的调用上的区别。
这里不做展开,有兴趣的,可以来 尼恩的 技术自由圈( Future Super Architect Community 未来 超级 架构师社区)沟通。
一般来说,咱们的线上应用,都是在 Kubernetes部署的。
关于 K8S+ SpringCloud 云原生微服务架构,具体请看尼恩的 PDF电子书 《K8S学习圣经》
问题来了,云原生场景下, SpringCloud 微服务实例,如何优雅退出?
还是使用 JVM关闭的钩子函数吗?
首先,尼恩带大家来看一下JVM关闭的钩子函数的问题。
前面的用到的SpringShutDownHook,最终还是调用的 JVM关闭的钩子函数。
既然JDK提供的这个方法可以注册一个JVM关闭的钩子函数,那么这个函数在什么情况下会被调用呢?
一般来说,咱们的线上应用,都是在 Kubernetes部署的。
关于 K8S+ SpringCloud 云原生微服务架构,具体请看尼恩的 PDF电子书 《K8S学习圣经》
Kubernetes 是如何优雅的停止 Pod 的?
当 kill 掉一个 Pod 的时候, Pod 的状态为 Terminating,开启终止流程。
JVM的优雅退出,发生在最后一步。
但是实际情况中,我们可能会遇到以下情况,导致最后一步发生了意外,比如:
Kubernetes 怎么进行超时处理的呢?
Kubernetes 还有一个 terminationGracePeriodSeconds
的硬停止时间,默认是 30s,如果 30s 内还是无法完成上述的过程,那就就会发送 SIGKILL,强制干掉 Pod。
在POSIX兼容的平台上,SIGKILL是发送给一个进程来导致它立即终止的信号。
SIGKILL的符号常量在头文件signal.h中定义。因为在不同平台上,信号数字可能变化,因此符号信号名被使用,然而在大量主要的系统上,SIGKILL是信号#9
Runtime.addShutDownHook(Thread hook)
触发场景 ,详见下图
40岁老架构师尼恩提示:强制关闭 不能 进行 Runtime.addShutDownHook(Thread hook)的触发,上图的第三个分支,展示得清清楚楚
所以,JVM的优雅退出的问题就昭然若揭了。
在实际情况中,如果会遇到以下情况,导致最后一步发生了意外,比如:
JVM的优雅退出就失效了。
怎么办呢?
比较直接、有效的策略是:
利用优雅的停止 Pod 的前置钩子, 定义preStop钩子接口优先执行核心的下线逻辑
定义preStop钩子接口优先执行核心的下线逻辑,比如这里的 微服务实例下线
具体如何操作呢?实操上有个步骤:
step1: 在SpringBoot应用中,定义WEB接口,实现核心线下逻辑
先看第一步:
step2:在 pod的preStop钩子接口的设置上,设置为SpringBoot应用下线钩子链接
具体的做法是:为容器加上PreStop配置项,设置为SpringBoot应用的下线钩子链接
/demo-provider/graceful/service/offline
当 kill 掉一个 Pod 的时候, Pod 的状态为 Terminating,开启终止流程。
preStop钩子 执行完成后, 最后一步, Pod 发 SIGTERM 信号让 Pod 中的所有容器优雅,SIGTERM 信号发给 jvm后,Jvm会执行优雅退出逻辑,主要是:
这些,咱们就先用放在前面介绍的 JVM 处理钩子方法里边去了。
我们在应用服务下线前,通过HTTP钩子调用,主动通知注册中心下线该实例
不论是Dubbo还是Cloud 的分布式服务框架,都关注的是怎么能在服务停止前,先将提供者在注册中心进行下线,然后在停止服务提供者,
唯有这样,才能保证业务微服务之间RPC远程调用,不会产生各种503、timeout等现象。
除了 微服务的无损下线,作为 SpringBoot应用, 还有 单体服务优雅停机的需求:
这些前面介绍到 ,咱们就先用放在 JVM 处理钩子方法里边去了。
SpringBoot应用的优雅停机,实际上指的是内嵌WEB服务器的优雅停机。
目前Spring Boot已经发展到了2.3.4.RELEASE,伴随着2.3版本的到来,优雅停机机制也更加完善了。
Web 容器优雅停机行为指的是在关闭容器时,让当前正在处理的请求处理完成或者等待一段时间,让正在处理的请求完成后再关闭容器,而不是直接强制终止正在处理的请求。这样可以避免正在处理的请求被中断,从而提高系统的可用性和稳定性。
一般来说,Web 容器的优雅停机行为需要满足以下几个条件:
如果没有优雅停机,服务器此时直接直接关闭(kill -9),那么就会导致当前正在容器内运行的业务直接失败,在某些特殊的场景下产生脏数据。
在服务器执行关闭(kill -2)时,会预留一点时间使容器内部业务线程执行完毕,
增加了优雅停机配置后, 此时容器也不允许新的请求进入。
目前版本的Spring Boot 优雅停机支持Jetty, Reactor Netty, Tomcat和 Undertow 以及反应式和基于 Servlet 的 web 应用程序都支持优雅停机功能。
新请求的处理方式跟web服务器有关,Reactor Netty、 Tomcat将停止接入请求,Undertow的处理方式是返回503。
具体行为,如下表所示:
web 容器名称 | 行为说明 |
---|---|
tomcat 9.0.33+ | 停止接收请求,客户端新请求等待超时。 |
Reactor Netty | 停止接收请求,客户端新请求等待超时。 |
Undertow | 停止接收请求,客户端新请求直接返回 503。 |
不同的 Web 容器实现优雅停机的方式可能会有所不同,但是一般都会提供相关的配置选项或者 API 接口来实现这个功能。
另外,和SpringBoot内嵌的WEB服务器类似,其他的非SpringBoot内嵌WEB服务器,也可以进行设置。
下面是 Nginx 和 Apache 的优雅停机配置:
worker_shutdown_timeout
选项来设置等待时间graceful-stop
命令来实现优雅停机。新版本配置非常简单,server.shutdown=graceful
就搞定了(注意,优雅停机配置需要配合Tomcat 9.0.33(含)以上版本)
server:
port: 6080
shutdown: graceful #开启优雅停机
spring:
lifecycle:
timeout-per-shutdown-phase: 20s #设置缓冲时间 默认30s
在设置了缓冲参数timeout-per-shutdown-phase
后,在规定时间内如果线程无法执行完毕则会被强制停机。
下面我们来看下停机时,加了优雅停日志和不加的区别:
//未加优雅停机配置
Disconnected from the target VM, address: '127.0.0.1:49754', transport: 'socket'
Process finished with exit code 130 (interrupted by signal 2: SIGINT)
加了优雅停机配置后,
日志可明显发现的 Waiting for active requests to cpmplete, 此时容器将在ShutdownHook执行完毕后停止。
前面讲到,SpringBoot 的优雅退出,最终在 Java 程序中可以通过添加钩子,在程序退出时会执行钩子方法,从而实现关闭资源、平滑退出、优雅退出等功能。
SpringBoot 在启动过程中,则会默认注册一个JVM Shutdown Hook,在应用被关闭的时候,会触发钩子调用 doClose()方法,去关闭容器。
这部分代码,前面介绍了,具体在 org.springframework.boot.SpringApplication#refreshContext
方法中
在创建 webserver 的时候,会创建一个实现smartLifecycle
的 bean,用来支撑 server 的优雅关闭。
// 注册webServerGracefulShutdown用来实现server优雅关闭
this.getBeanFactory().registerSingleton("webServerGracefulShutdown", new WebServerGracefulShutdownLifecycle(this.webServer));
}
可以看到 WebServerGracefulShutdownLifecycle 类实现SmartLifecycle
接口,重写了 stop 方法,stop 方法会触发 webserver 的优雅关闭方法(取决于具体使用的 webserver 如 tomcatWebServer)。
// 优雅关闭server
this.webServer.shutDownGracefully((result) -> callback.run());
@Override
public void shutDownGracefully(GracefulShutdownCallback callback) {
if (this.gracefulShutdown == null) {
// 如果没有开启优雅停机,会立即关闭tomcat服务器
callback.shutdownComplete(GracefulShutdownResult.IMMEDIATE);
return;
}
// 优雅关闭服务器
this.gracefulShutdown.shutDownGracefully(callback);
}
至此,优雅退出的代码,通过smartLifecycle的Bean 的stop 方法实现退出的回调注册。
smartLifecycle的Bean 的stop 方法什么时候被执行呢?
上文提到JVM钩子方法被调用后,会执行 doColse()方法,
而这个 doColse()方法, 在关闭容器之前,会通过 lifecycleProcessor 调用 lifecycle 的方法。
protected void doClose() {
if (this.active.get() && this.closed.compareAndSet(false, true)) {
LiveBeansView.unregisterApplicationContext(this);
// 发布 ContextClosedEvent 事件
publishEvent(new ContextClosedEvent(this));
// 回调所有实现Lifecycle 接口的Bean的stop方法
if (this.lifecycleProcessor != null) {
this.lifecycleProcessor.onClose();
}
// 销毁bean, 关闭容器
destroyBeans();
closeBeanFactory();
onClose();
if (this.earlyApplicationListeners != null) {
this.applicationListeners.clear();
this.applicationListeners.addAll(this.earlyApplicationListeners);
}
// Switch to inactive.
this.active.set(false);
}
}
关闭 Lifecycle Bean 的入口:
org.springframework.context.support.DefaultLifecycleProcessor
具体的代码如下:
public class DefaultLifecycleProcessor implements LifecycleProcessor, BeanFactoryAware {
@Override
public void onClose() {
stopBeans();
this.running = false;
}
private void stopBeans() {
//获取所有的 Lifecycle bean
Map<String, Lifecycle> lifecycleBeans = getLifecycleBeans();
//按Phase值对bean分组, 如果没有实现 Phased 接口则认为 Phase 是 0
Map<Integer, LifecycleGroup> phases = new HashMap<>();
lifecycleBeans.forEach((beanName, bean) -> {
int shutdownPhase = getPhase(bean);
LifecycleGroup group = phases.get(shutdownPhase);
if (group == null) {
group = new LifecycleGroup(shutdownPhase, this.timeoutPerShutdownPhase, lifecycleBeans, false);
phases.put(shutdownPhase, group);
}
group.add(beanName, bean);
});
if (!phases.isEmpty()) {
List<Integer> keys = new ArrayList<>(phases.keySet());
//按照 Phase 值倒序
keys.sort(Collections.reverseOrder());
// Phase值越大优先级越高,先执行
for (Integer key : keys) {
phases.get(key).stop();
}
}
}
DefaultLifecycleProcessor
的 stop 方法执行流程:
stop 方法的实现,在这里就终于被执行了。
完成 tomcat的优雅退出行为,执行完之前接收到的请求,实现优雅退出。
WebServerGracefulShutdownLifecycle
,Phase=Inter.MAX_VALUE
,处于最优先执行序列,所以会先触发优雅关闭 tomcat ,并且tomcat 关闭方法是异步执行的,主线会继续调用执行本组其他 bean 的关闭方法,然后等待所有 bean 关闭完毕,超过等待时间,会执行下一组 Lifecycle bean 的关闭。通过源码分析,大家也发现了,SpringBoot应用的优雅停机,是注册了 JVM 优雅退出的钩子方法
JVM 优雅退出的钩子方法如何触发的呢?
常见的触发方式有:
使用方式:kill java进程ID
kill命令的格式是 kill -Signal pid
,其中 pid 就是进程的编号,signal是发送给进程的信号,默认参数下,kill 发送 SIGTERM(15)信号给进程,告诉进程,你需要被关闭,请自行停止运行并退出。
kill、kill -9、kill -3的区别
kill 会默认传15代表的信号为SIGTERM,这是告诉进程你需要被关闭,请自行停止运行并退出,进程可以清理缓存自行结束,也可以拒绝结束。
kill -9
代表的信号是SIGKILL,表示进程被终止,需要立即退出,强制杀死该进程,这个信号不能被捕获也不能被忽略。
kill -3
可以打印进程各个线程的堆栈信息,kill -3 pid
后文件的保存路径为:/proc/${pid}/cwd
,文件名为:antBuilderOutput.log
其他的Kill信号清单如下:
信号 | 取值 | 默认动作 | 含义(发出信号的原因) |
---|---|---|---|
SIGHUP | 1 | Term | 终端的挂断或进程死亡 |
SIGINT | 2 | Term | 来自键盘的中断信号 |
SIGQUIT | 3 | Core | 来自键盘的离开信号 |
SIGILL | 4 | Core | 非法指令 |
SIGABRT | 6 | Core | 来自abort的异常信号 |
SIGFPE | 8 | Core | 浮点例外 |
SIGKILL | 9 | Term | 杀死 |
SIGSEGV | 11 | Core | 段非法错误(内存引用无效) |
SIGPIPE | 13 | Term | 管道损坏:向一个没有读进程的管道写数据 |
SIGALRM | 14 | Term | 来自alarm的计时器到时信号 |
SIGTERM | 15 | Term | 终止 |
SIGUSR1 | 30,10,16 | Term | 用户自定义信号1 |
SIGUSR2 | 31,12,17 | Term | 用户自定义信号2 |
SIGCHLD | 20,17,18 | Ign | 子进程停止或终止 |
SIGCONT | 19,18,25 | Cont | 如果停止,继续执行 |
SIGSTOP | 17,19,23 | Stop | 非来自终端的停止信号 |
SIGTSTP | 18,20,24 | Stop | 来自终端的停止信号 |
SIGTTIN | 21,21,26 | Stop | 后台进程读终端 |
SIGTTOU | 22,22,27 | Stop | 后台进程写终端 |
SIGBUS | 10,7,10 | Core | 总线错误(内存访问错误) |
SIGPOLL | Term | Pollable事件发生(Sys V),与SIGIO同义 | |
SIGPROF | 27,27,29 | Term | 统计分布图用计时器到时 |
SIGSYS | 12,-,12 | Core | 非法系统调用(SVr4) |
SIGTRAP | 5 | Core | 跟踪/断点自陷 |
SIGURG | 16,23,21 | Ign | socket紧急信号(4.2BSD) |
SIGVTALRM | 26,26,28 | Term | 虚拟计时器到时(4.2BSD) |
SIGXCPU | 24,24,30 | Core | 超过CPU时限(4.2BSD) |
SIGXFSZ | 25,25,31 | Core | 超过文件长度限制(4.2BSD) |
SIGIOT | 6 | Core | IOT自陷,与SIGABRT同义 |
SIGEMT | 7,-,7 | Term | |
SIGSTKFLT | -,16,- | Term | 协处理器堆栈错误(不使用) |
SIGIO | 23,29,22 | Term | 描述符上可以进行I/O操作 |
SIGCLD | -,-,18 | Ign | 与SIGCHLD同义 |
SIGPWR | 29,30,19 | Term | 电力故障(System V) |
SIGINFO | 29,-,- | 与SIGPWR同义 | |
SIGLOST | -,-,- | Term | 文件锁丢失 |
SIGWINCH | 28,28,20 | Ign | 窗口大小改变(4.3BSD, Sun) |
SIGUNUSED | -,31,- | Term | 未使用信号(will be SIGSYS) |
Spring Boot 提供了/shutdown端点,可以借助它实现优雅停机。
使用方式:在想下线应用的application.yml中添加如下配置,从而启用并暴露/shutdown端点:
management:
endpoint:
shutdown:
enabled: true
endpoints:
web:
exposure:
include: shutdown
发送 POST 请求到/shutdown端点
curl -X http://ip:port/actuator/shutdown
该方式本质和方式一是一样的,也是借助 Spring Boot 应用的 Shutdown hook 去实现的。
actuator 都使用了SPI的扩展方式,
先看下AutoConfiguration,可以看到关键点就是ShutdownEndpoint
@Configuration(
proxyBeanMethods = false
)
@ConditionalOnAvailableEndpoint(
endpoint = ShutdownEndpoint.class
)
public class ShutdownEndpointAutoConfiguration {
public ShutdownEndpointAutoConfiguration() {
}
@Bean(
destroyMethod = ""
)
@ConditionalOnMissingBean
public ShutdownEndpoint shutdownEndpoint() {
return new ShutdownEndpoint();
}
}
ShutdownEndpoint,的核心代码如下:
@Endpoint(
id = "shutdown",
enableByDefault = false
)
public class ShutdownEndpoint implements ApplicationContextAware {
@WriteOperation
public Map<String, String> shutdown() {
if (this.context == null) {
return NO_CONTEXT_MESSAGE;
} else {
boolean var6 = false;
Map var1;
try {
var6 = true;
var1 = SHUTDOWN_MESSAGE;
var6 = false;
} finally {
if (var6) {
Thread thread = new Thread(this::performShutdown);
thread.setContextClassLoader(this.getClass().getClassLoader());
thread.start();
}
}
Thread thread = new Thread(this::performShutdown);
thread.setContextClassLoader(this.getClass().getClassLoader());
thread.start();
return var1;
}
}
private void performShutdown() {
try {
Thread.sleep(500L);
} catch (InterruptedException var2) {
Thread.currentThread().interrupt();
}
this.context.close(); //这里才是核心
}
}
在调用了 this.context.close()
,其实就是AbstractApplicationContext
的close()
方法 (重点是其中的doClose()
)
/**
* Close this application context, destroying all beans in its bean factory.
* Delegates to {@code doClose()} for the actual closing procedure.
* Also removes a JVM shutdown hook, if registered, as it's not needed anymore.
* @see #doClose()
* @see #registerShutdownHook()
*/
@Override
public void close() {
synchronized (this.startupShutdownMonitor) {
doClose(); //重点:销毁bean 并执行jvm shutdown hook
// If we registered a JVM shutdown hook, we don't need it anymore now:
// We've already explicitly closed the context.
if (this.shutdownHook != null) {
try {
Runtime.getRuntime().removeShutdownHook(this.shutdownHook);
}
catch (IllegalStateException ex) {
// ignore - VM is already shutting down
}
}
}
}
doClose() 方法,又回到了前面的 Spring 的核心关闭方法。
doClose()在销毁 bean, 关闭容器之前会执行所有实现 Lifecycel 接口 bean 的 stop方法,并且会按 Phase 值分组, phase 大的优先执行。
Spring Boot、Spring Cloud应用的优雅停机,平时经常会被问到,这也是实际应用过程中,必须要掌握的点,
这里简单总结下以前我们一般在实现的时候要把握的几个要点:
SpringCloud + SpringBoot 面试题,是非常常见的面试题。
以上的5大方案,如果大家能对答如流,如数家珍,基本上 面试官会被你 震惊到、吸引到。
最终,让面试官爱到 “不能自已、口水直流”。 offer, 也就来了。
学习过程中,如果有啥问题,大家可以来 找 40岁老架构师尼恩沟通。
《吃透8图1模板,人人可以做架构》
《10Wqps评论中台,如何架构?B站是这么做的!!!》
《阿里二面:千万级、亿级数据,如何性能优化? 教科书级 答案来了》
《峰值21WQps、亿级DAU,小游戏《羊了个羊》是怎么架构的?》
《100亿级订单怎么调度,来一个大厂的极品方案》
《2个大厂 100亿级 超大流量 红包 架构方案》
… 更多架构文章,正在添加中
尼恩 架构笔记、面试题 的PDF文件更新,▼请到下面【技术自由圈】公号取 ▼