关于java:咱们从头到尾说一次优雅关闭

37次阅读

共计 7455 个字符,预计需要花费 19 分钟才能阅读完成。

优雅敞开 (Graceful Shutdown/Graceful Exit),这个词如同并没有什么官网的定义,也没找到权威的起源,不过在 Bing 里搜寻 Graceful Exit,呈现的第二条却是个专门为女性解决离婚的网站……
好家伙,女性离婚一站式解决方案,这也太业余了。看来不光是程序须要优雅敞开,就连离婚也得 Graceful!

在计算机里呢,优雅敞开指的其实就是程序的一种敞开计划。那既然有优雅敞开,必定也有不优雅的敞开了。

Windows 的优雅敞开

就拿 Windows 电脑开关机这事来说,长按电源键强制关机,或者间接断电关机,这种就属于硬敞开(hard shutdown),操作系统接管不到任何信号就间接没了,多不优雅!

此时零碎内,或者一些软件还没有进行敞开前的解决,比方你加班写了 4 个小时的 PPT 来没来得及保留……

但个别除了死机之外,很少会有人强制关机,大多数人的操作还是通过电源选项 -> 关机操作,让操作系统本人解决关机。比方 Windows 在关机前,会被动的敞开所有应用程序,可是很多利用会捕捉过程的敞开事件,导致本人无奈失常敞开,从而导致系统无奈失常关机。比方 office 套件里,在敞开之前如果没保留会弹框让你保留,这个机制就会烦扰操作系统的失常关机。

或者你用的是 Win10,动不动就本人更新零碎的那种,如果你在更新零碎的时候断电强制关机,再次开机的时候可能就会有惊喜了……更新文件写了一半,你猜猜会呈现什么问题?

网络中的优雅敞开

网络是不牢靠的!

TCP 的八股文置信大家都背过,四次挥手后能力断开连接,但四次挥手也是建设在失常敞开的前提下。如果你强行拔网线,或者强制断电,对端不可能及时的检测到你的断开,此时对端如果持续发送报文,就会收到谬误了。

你看除了优雅的四次挥手,还有 TCP KeepAlive 做心跳,光有这个还不够,应用层还得再做一层心跳,同时还得正确优雅的解决连贯断开,Connection Reset 之类的谬误。

所以,如果咱们在写一个网络程序时,肯定要提供敞开机制,在敞开事件中失常敞开 socket/server,从而缩小因为敞开导致的更多异样问题。

怎么监听敞开事件?

各种语言都会提供这个敞开事件的监听机制,只是用法不同。借助这个敞开监听,实现优雅敞开就很轻松了。

JAVA 监听敞开

JAVA 提供了一个简略的敞开事件的监听机制,能够接管到 失常敞开 信号的事件,比方命令行程序下的 Ctrl+C 退出信号。

Runtime.getRuntime().addShutdownHook(new Thread(new Runnable() {
    @Override
    public void run() {System.out.println("Before shutdown...");
    }
}));

在这段配置实现后,失常敞开前,ShutdownHook 的线程就会被启动执行,输入 Before shutdown。当然你要是间接强制敞开,比方 Windows 下的完结过程,Linux 下的 Kill -9……神仙都监听不到

C++ 里监听敞开

C++ 里也有相似的实现,只有将函数注册到 atexit 函数中,在程序失常敞开前就能够执行注册的 fnExit 函数。

void fnExit1 (void)
{puts ("Exit function 1.");
}

void fnExit2 (void)
{puts ("Exit function 2.");
}

int main ()
{atexit (fnExit1);
  atexit (fnExit2);
  puts ("Main function.");
  return 0;
}

敞开过程中可能会遇到的问题

构想这么一个场景,一个音讯生产逻辑,事务提交胜利后推送周边零碎。早就收到了敞开信号,然而因为有大量音讯沉积,一部分曾经沉积在内存队列了,可是并行生产解决的逻辑始终没执行完。

此时有局部生产线程提交事务,还没有推送周边零碎时,就收到了 Force Kill 信号,那么就会呈现数据不统一的问题,本服务数据曾经落库,但没有推送三方……

再举一个数据库的例子,存储引擎有汇集索引和非汇集索引的概念,如果一条 Insert 语句执行后,刚写了汇集索引,还没来得及写非汇集索引,过程就被干掉了,那么这俩索引数据间接就不统一了!

不过作为存储引擎,肯定会解决这个不统一的问题。但如果能够失常敞开,让存储引擎平安的执行完,这种不统一的危险就会大大降低。

过程进行

JAVA 过程进行的机制是,所有非守护线程都曾经进行 后,过程才会退出。那么间接给 JAVA 过程发一个敞开信号,过程就能敞开吗?必定不行!

JAVA 里的线程默认都是非阻塞线程,非守护线程会只有不停,JVM 过程是不会进行的。所以收到敞开信号后,得自行敞开所有的线程,比方线程池……

线程中断

线程怎么被动敞开?道歉,这个真关不了(stop 办法从 JAVA 1.1 就被废除了),只能等线程本人执行实现,或者通过软状态加 interrupt 来实现:

private volatile boolean stopped = false;

@Override
public void run() {while (!stopped && Thread.interrupted()){// do sth...}
}

public void stop(){
    stopped = true;
    interrupt();}

当线程处于 WAITTING 状态时,interrupt 办法会中断这个 WAITTING 的状态,强制返回并抛出 InterruptedException。比方咱们的线程正在卡在 Socket Read 操作上,或者 Object.wait/JUC 下的一些锁期待状态时,调用 interrupt 办法就会中断这个期待状态,间接抛出异样。

但如果线程没卡在 WAITING 状态,而且还是在线程池中创立的,没有软状态,那下面这个敞开策略可就不太实用了。

线程池的敞开策略

ThreadPoolExecutor 提供了两个敞开办法:

  1. shutdown – interrupt 闲暇的 Worker 线程,期待所有工作(线程)执行实现。因为闲暇 Worker 线程会处于 WAITING 状态,所以 interrupt 办法会间接中断 WAITING 状态,进行这些闲暇线程。
  2. shutdownNow – interrupt 所有的 Worker 线程,不论是不是闲暇。对于闲暇线程来说,和 shutdown 办法一样,间接就被进行了,能够对于正在工作中的 Worker 线程,不肯定处于 WAITING 状态,所以 interrupt 就不能保障敞开了。

留神:大多数的线程池,或者调用线程池的框架,他们的默认敞开策略是调用 shutdown,而不是 shutdownNow,所以正在执行的线程并不一定会被 Interrupt

但作为业务线程,肯定要解决 **InterruptedException**。不然万一有 shutdownAll,或者是手动创立线程的中断,业务线程没有及时响应,可能就会导致线程彻底无奈敞开了

三方框架的敞开策略

除了 JDK 的线程池之外,一些三方框架 / 库,也会提供一些失常敞开的办法。

  • Netty 里的 EventLoopGroup.shutdownGracefully/shutdown – 敞开线程池等资源
  • Reddsion 里的 Redisson.shutdown – 敞开连接池的连贯,销毁各种资源
  • Apache HTTPClient 里的 CloseableHttpClient.close – 敞开连接池的连贯,敞开 Evictor 线程等

这些支流的成熟框架,都会给你提供一个优雅敞开的办法,保障你在调用敞开之后,它能够销毁资源,敞开它本人创立的线程 / 池。

尤其是这种波及到创立线程的三方框架,必须要提供失常敞开的办法,不然可能会呈现线程无奈敞开,导致最终 JVM 过程不能失常退出的状况。

Tomcat 里的优雅敞开

Tomcat 的敞开脚本(sh 版本)设计的很不错,间接手摸手的通知你应该怎么关:

commands:
    stop              Stop Catalina, waiting up to 5 seconds for the process to end
    stop n            Stop Catalina, waiting up to n seconds for the process to end
    stop -force       Stop Catalina, wait up to 5 seconds and then use kill -KILL if still running
    stop n -force     Stop Catalina, wait up to n seconds and then use kill -KILL if still running

这个设计很灵便,间接提供 4 种敞开形式,任你轻易抉择。

force 模式下,会给过程发送一个 SIGTERM Signal(kill -15),这个信号是能够被 JVM 捕捉到的,会执行注册的 ShutdownHook 线程。期待 5 秒后如果过程还在,就 Force Kill,流程如下图所示:

接着 Tomcat 里注册的 ShutdownHook 线程会被执行,手动的敞开各种资源,比方 Tomcat 本人的连贯,线程池等等。

当然还有最重要的一步,敞开所有的 APP:

// org.apache.catalina.core.StandardContext#stopInternal

// 敞开所有利用下的所有 Filter - filter.destroy();
filterStop();
// 敞开所有利用下的所有 Listener - listener.contextDestroyed(event);
listenerStop();

借助这俩敞开前的 Hook,应用程序就能够自行处理敞开了,比方在 XML 时代时应用的 Servlet Context Listener:

<listener>
        <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>

Spring 在这个 Listener 内,自行调用 Application Context 的敞开办法:

public void contextDestroyed(ServletContextEvent event) {
    // 敞开 Spring Application Context
    this.closeWebApplicationContext(event.getServletContext());
    ContextCleanupListener.cleanupAttributes(event.getServletContext());
}

Spring 的优雅敞开

在 Spring ApplicationContext 执行 close 后,Spring 会对所有的 Bean 执行销毁动作,只有你的 Bean 配置了 destroy 策略,或者实现了 AutoCloseable 接口,那么 Spring 在销毁 Bean 时就能够调用 destroy 了,比方 Spring 包装的线程池 – ThreadPoolTaskExecutor,它就实现了 DisposableBean 接口:

// ThreadPoolTaskExecutor
public void destroy() {shutdown();
}

在 destroy Bean 时,这个线程池就会执行 shutdown,不须要你手动控制线程池的 shutdown。

这里须要留神一下,Spring 创立 Bean 和销毁 Bean 的程序是相同的:


销毁时应用相同的程序,就能够保障依赖 Bean 能够失常被销毁,而不会提前销毁。比方 A->B->C 这个依赖关系中,咱们肯定会保障 C 先加载;那么在如果先销毁 C 的话,B 可能还在运行,此时 B 可能就报错了。

所以在解决简单依赖关系的 Bean 时,应该让前置 Bean 先加载,线程池等根底 Bean 最初加载,销毁时就会先销毁线程池这种根底 Bean 了。


大多数须要失常敞开的框架 / 库在集成 Spring 时,都会集成 Spring Bean 的销毁入口。

比方 Redis 客户端 – Lettuce,spring-data-redis 里提供了 lettuce 的集成,集成类 LettuceConnectionFactory 是间接实现 DisposableBean 接口的,在 destroy 办法外部进行敞开

// LettuceConnectionFactory 

public void destroy() {this.resetConnection();
    this.dispose(this.connectionProvider);
    this.dispose(this.reactiveConnectionProvider);

    try {Duration quietPeriod = this.clientConfiguration.getShutdownQuietPeriod();
        Duration timeout = this.clientConfiguration.getShutdownTimeout();
        this.client.shutdown(quietPeriod.toMillis(), timeout.toMillis(), TimeUnit.MILLISECONDS);
    } catch (Exception var4) {if (this.log.isWarnEnabled()) {this.log.warn((this.client != null ? ClassUtils.getShortName(this.client.getClass()) : "LettuceClient") + "did not shut down gracefully.", var4);
        }
    }

    if (this.clusterCommandExecutor != null) {
        try {this.clusterCommandExecutor.destroy();
        } catch (Exception var3) {this.log.warn("Cannot properly close cluster command executor", var3);
        }
    }

    this.destroyed = true;
}

其余框架也是一样,集成 Spring 时,都会基于 Spring 的 destroy 机制来进行资源的销毁。

Spring 销毁机制的问题

当初有这样一个场景,咱们创立了某个 MQ 生产的客户端对象,就叫 XMQConsumer 吧。在这个生产客户端中,内置了一个线程池,当 pull 到音讯时会丢到线程池中执行。

在音讯 MQ 生产的代码中,须要数据库连接池 – DataSource,还须要发送 HTTP 申请 – HttpClient,这俩对象都是被 Spring 托管的。不过 DataSource 和 HttpClient 这俩 Bean 的加载程序比拟靠前,在 XMQConsumer 启动时,这俩 Bean 肯定时初始化实现能够应用的。

不过这里没有给这个 XMQConsumer 指定 destroy-method,所以 Spring 容器在敞开时,并不会敞开这个生产客户端,生产客户端会持续 pull 音讯,生产音讯。

此时当 Tomcat 收到敞开信号后,依照下面的敞开流程,Spring 会依照 Bean 的加载程序 逆序 的顺次销毁:

因为 XMQConsumer 没有指定 destroy ,所以 Spring 只会销毁 #2 和 #3 两个 Bean。但 XMQConsumer 线程池里的线程和主线程可是异步的,在销毁前两个对象时,生产线程依然在运行,运行过程里须要操作数据库,还须要通过 HttpClient 发送申请,此时就会呈现:XXX is Closed 之类的谬误。

Spring Boot 优雅敞开

到了 Spring Boot 之后,这个敞开机制产生了一点点变动。因为之前是 Spring 我的项目部署在 Tomcat 里运行,由 Tomcat 来启动 Spring。

可在 Spring Boot(Executeable Jar 形式)中,程序反过来了,因为是间接启动 Spring,而后在 Spring 中来启动 Tomcat(Embedded)。启动形式变了,那么敞开形式必定也变了,shutdownHook 由 Spring 来负责,最初 Spring 去敞开 Tomcat。

如下图所示,这是两种形式的启动 / 进行程序:

K8S 优雅敞开

这里说的是 K8S 优雅敞开 POD 的机制,和后面介绍的 Tomcat 敞开脚本相似,都是先发送 SIGTERM Signal,N 秒后如果过程还在,就 Force Kill。

只是 Kill 的发起者变成了 K8S/Runtime,容器运行时会给 Pod 内所有容器的主过程发送 Kill(TERM) 信号:

同样的,如果在宽限期内(terminationGracePeriodSeconds,默认 30 秒),容器内的过程没有解决实现敞开逻辑,过程会被强制杀死。

当 K8S 遇到 SpringBoot(Executeable Jar)

没什么非凡的,由 K8S 对 Spring Boot 过程发送 TERM 信号,而后执行 Spring Boot 的 ShutdownHook

当 K8S 遇到 Tomcat

和 Tomcat 的 catalina.sh 敞开形式齐全一样,只是这个敞开的发起者变成了 K8S

总结

说了这么多的优雅敞开,到底怎么算优雅呢?这里简略总结 3 点:

  1. 作为框架 / 库,肯定要提供失常敞开的办法,手动的敞开线程 / 线程池,销毁连贯资源,FD 资源等
  2. 作为应用程序,肯定要解决好 InterruptedException,千万不要疏忽这个异样,不然有过程无奈失常退出的危险
  3. 在敞开时,肯定要留神程序,尤其是线程池类的资源,肯定要保障线程池先敞开。最平安的做法是不要 interrupt 线程,期待线程本人执行实现,而后再敞开。

参考

  • https://kubernetes.io/zh/docs/concepts/workloads/pods/pod-lifecycle/
  • https://github.com/apache/tomcat
  • https://whatis.techtarget.com/definition/graceful-shutdown-and-hard-shutdown
  • https://www.wikiwand.com/en/Graceful_exit
  • https://docs.spring.io/spring-boot/docs/2.3.0.RELEASE/reference/html/spring-boot-features.html#boot-features-graceful-shutdown

正文完
 0