【Java】优雅停机时的一点思考

ここに画像の説明を挿入

1.概述

转载:http://cxytiandi.com/blog/detail/15386

转载自:徐靖峰 Kirito的技术分享

最近瞥了一眼项目的重启脚本,发现运维一直在使用 kill -9 的方式重启 springboot embedded tomcat,其实大家几乎一致认为: kill -9 的方式比较暴力,但究竟会带来什么问题却很少有人能分析出个头绪。这篇文章主要记录下自己的思考过程。

kill -9 和 kill -15 有什么区别?

在以前,我们发布 WEB 应用通常的步骤是将代码打成 war 包,然后丢到一个配置好了应用容器(如 Tomcat,Weblogic)的 Linux 机器上,这时候我们想要启动/关闭应用,方式很简单,运行其中的启动/关闭脚本即可。而 springboot 提供了另一种方式,将整个应用连同内置的 tomcat 服务器一起打包,这无疑给发布应用带来了很大的便捷性,与之而来也产生了一个问题:如何关闭 springboot 应用呢?一个显而易见的做法便是,根据应用名找到进程 id,杀死进程 id 即可达到关闭应用的效果。

上述的场景描述引出了我的疑问:怎么优雅地杀死一个 springboot 应用进程呢?这里仅仅以最常用的 Linux 操作系统为例,在 Linux 中 kill 指令负责杀死进程,其后可以紧跟一个数字,代表信号编号(Signal),执行 kill -l 指令,可以一览所有的信号编号。

xu@ntzyz-qcloud ~ % kill -l                                                                     
HUP INT QUIT ILL TRAP ABRT BUS FPE KILL USR1 SEGV USR2 PIPE ALRM TERM STKFLT CHLD CONT STOP TSTP TTIN TTOU URG XCPU XFSZ VTALRM PROF WINCH POLL PWR SYS

本文主要介绍下第 9 个信号编码 KILL,以及第 15 个信号编号 TERM 。

先简单理解下这两者的区别:

  1. kill -9 pid 可以理解为操作系统从内核级别强行杀死某个进程
  2. kill -15 pid 则可以理解为发送一个通知,告知应用主动关闭。

这么对比还是有点抽象,那我们就从应用的表现来看看,这两个命令杀死应用到底有啥区别。

代码准备

由于笔者 springboot 接触较多,所以以一个简易的 springboot 应用为例展开讨论,添加如下代码。

1 增加一个实现了 DisposableBean 接口的类

@Component
public class TestDisposableBean implements DisposableBean{
    
    
    @Override
    public void destroy() throws Exception {
    
    
        System.out.println("测试 Bean 已销毁 ...");
    }
}

2 增加 JVM 关闭时的钩子

@SpringBootApplication
@RestController
public class TestShutdownApplication implements DisposableBean {
    
    
    public static void main(String[] args) {
    
    
        SpringApplication.run(TestShutdownApplication.class, args);
        Runtime.getRuntime().addShutdownHook(new Thread(new Runnable() {
    
    
            @Override
            public void run() {
    
    
                System.out.println("执行 ShutdownHook ...");
            }
        }));
    }
}

测试步骤

执行 java -jar test-shutdown-1.0.jar 将应用运行起来
测试 kill -9 pid,kill -15 pid,ctrl + c 后输出日志内容
测试结果

kill -15 pid & ctrl + c,效果一样,输出结果如下

2018-01-14 16:55:32.424  INFO 8762 --- [       Thread-3] ationConfigEmbeddedWebApplicationContext : Closing org.springframework.boot.context.embedded.AnnotationConfigEmbeddedWebApplicationContext@2cdf8d8a: startup date [Sun Jan 14 16:55:24 UTC 2018]; root of context hierarchy
2018-01-14 16:55:32.432  INFO 8762 --- [       Thread-3] o.s.j.e.a.AnnotationMBeanExporter        : Unregistering JMX-exposed beans on shutdown
执行 ShutdownHook ...
测试 Bean 已销毁 ...
java -jar test-shutdown-1.0.jar  7.46s user 0.30s system 80% cpu 9.674 total

kill -9 pid,没有输出任何应用日志

可以发现,kill -9 pid 是给应用杀了个措手不及,没有留给应用任何反应的机会。而反观 kill -15 pid,则比较优雅,先是由AnnotationConfigEmbeddedWebApplicationContext (一个 ApplicationContext 的实现类)收到了通知,紧接着执行了测试代码中的 Shutdown Hook,最后执行了 DisposableBean#destory() 方法。孰优孰劣,立判高下。

一般我们会在应用关闭时处理一下“善后”的逻辑,比如

  1. 关闭 socket 链接
  2. 清理临时文件
  3. 发送消息通知给订阅方,告知自己下线
  4. 将自己将要被销毁的消息通知给子进程
  5. 各种资源的释放
  6. 等等

kill -9 pid 则是直接模拟了一次系统宕机,系统断电,这对于应用来说太不友好了,不要用收割机来修剪花盆里的花。取而代之,便是使用 kill -15 pid 来代替。如果在某次实际操作中发现:kill -15 pid 无法关闭应用,则可以考虑使用内核级别的 kill -9 pid ,但请事后务必排查出是什么原因导致 kill -15 pid 无法关闭。

springboot 如何处理 -15 TERM Signal

上面解释过了,使用 kill -15 pid 的方式可以比较优雅的关闭 springboot 应用,我们可能有以下的疑惑: springboot/spring 是如何响应这一关闭行为的呢?是先关闭了 tomcat,紧接着退出 JVM,还是相反的次序?它们又是如何互相关联的?

尝试从日志开始着手分析,AnnotationConfigEmbeddedWebApplicationContext 打印出了 Closing 的行为,直接去源码中一探究竟,最终在其父类 AbstractApplicationContext 中找到了关键的代码:

@Override
public void registerShutdownHook() {
    
    
  if (this.shutdownHook == null) {
    
    
    this.shutdownHook = new Thread() {
    
    
      @Override
      public void run() {
    
    
        synchronized (startupShutdownMonitor) {
    
    
          doClose();
        }
      }
    };
    Runtime.getRuntime().addShutdownHook(this.shutdownHook);
  }
}
@Override
public void close() {
    
    
   synchronized (this.startupShutdownMonitor) {
    
    
      doClose();
      if (this.shutdownHook != null) {
    
    
         Runtime.getRuntime().removeShutdownHook(this.shutdownHook);
      }
   }
}
protected void doClose() {
    
    
   if (this.active.get() && this.closed.compareAndSet(false, true)) {
    
    
      LiveBeansView.unregisterApplicationContext(this);
      // 发布应用内的关闭事件
      publishEvent(new ContextClosedEvent(this));
      // Stop all Lifecycle beans, to avoid delays during individual destruction.
      if (this.lifecycleProcessor != null) {
    
    
         this.lifecycleProcessor.onClose();
      }
      // spring 的 BeanFactory 可能会缓存单例的 Bean 
      destroyBeans();
      // 关闭应用上下文&BeanFactory
      closeBeanFactory();
      // 执行子类的关闭逻辑
      onClose();
      this.active.set(false);
   }
}

为了方便排版以及便于理解,我去除了源码中的部分异常处理代码,并添加了相关的注释。在容器初始化时,ApplicationContext 便已经注册了一个 Shutdown Hook,这个钩子调用了 Close() 方法,于是当我们执行 kill -15 pid 时,JVM 接收到关闭指令,触发了这个 Shutdown Hook,进而由 Close() 方法去处理一些善后手段。具体的善后手段有哪些,则完全依赖于 ApplicationContext 的 doClose() 逻辑,包括了注释中提及的销毁缓存单例对象,发布 close 事件,关闭应用上下文等等,特别的,当 ApplicationContext 的实现类是 AnnotationConfigEmbeddedWebApplicationContext 时,还会处理一些 tomcat/jetty 一类内置应用服务器关闭的逻辑。

窥见了 springboot 内部的这些细节,更加应该了解到优雅关闭应用的必要性。JAVA 和 C 都提供了对 Signal 的封装,我们也可以手动捕获操作系统的这些 Signal,在此不做过多介绍,有兴趣的朋友可以自己尝试捕获下。

还有其他优雅关闭应用的方式吗?
spring-boot-starter-actuator 模块提供了一个 restful 接口,用于优雅停机。

添加依赖

<dependency>
   <groupId>org.springframework.boot</groupId>
   <artifactId>spring-boot-starter-actuator</artifactId>
</dependency>

添加配置

#启用shutdown
endpoints.shutdown.enabled=true
#禁用密码验证
endpoints.shutdown.sensitive=false

生产中请注意该端口需要设置权限,如配合 spring-security 使用。

执行 curl -X POST host:port/shutdown 指令,关闭成功便可以获得如下的返回:

{
    
    "message":"Shutting down, bye..."}

虽然 springboot 提供了这样的方式,但按我目前的了解,没见到有人用这种方式停机,kill -15 pid 的方式达到的效果与此相同,将其列于此处只是为了方案的完整性。

如何销毁作为成员变量的线程池?

JVMはシャットダウン時に特定のリソースを再利用するのに役立ちますが、一部のサービスが非同期のコールバックとタイミングタスクを使用する場合、不適切な処理がビジネス上の問題を引き起こす可能性があります。その中でも、スレッドプールのシャットダウン方法が一般的な問題です。

@Service
public class SomeService {
    
    
    ExecutorService executorService = Executors.newFixedThreadPool(10);
    public void concurrentExecute() {
    
    
        executorService.execute(new Runnable() {
    
    
            @Override
            public void run() {
    
    
                System.out.println("executed...");
            }
        });
    }
}

アプリケーションがシャットダウンしたときにスレッドプールを閉じる方法を見つける必要があります(JVMがシャットダウンしてコンテナーが実行を停止します)。

初期計画:何もしない。通常の状況では、JVMが閉じてリリースされるため、これは大きな問題にはなりませんが、明らかに、この記事で強調している2つの言葉を達成していません。

方法1の欠点は、スレッドプールで送信されたタスクとブロッキングキューで実行されていないタスクが非常に制御不能になることです。シャットダウンの指示を受け取った後、すぐに終了しますか?または、タスクが完了するのを待ちますか?それとも、タスクが完了する前に、タスクが完了する前に一定の時間待機することですか?

スキームの改善:

最初のソリューションの欠点を発見した後、私はすぐに次のようにDisposableBeanインターフェースを使用することを考えました:

@Service
public class SomeService implements DisposableBean{
    
    
    ExecutorService executorService = Executors.newFixedThreadPool(10);
    public void concurrentExecute() {
    
    
        executorService.execute(new Runnable() {
    
    
            @Override
            public void run() {
    
    
                System.out.println("executed...");
            }
        });
    }
    @Override
    public void destroy() throws Exception {
    
    
        executorService.shutdownNow();
        //executorService.shutdown();
    }
}

ThreadPoolExecutorはシャットダウン後にSHUTDOWNになり、新しいタスクを受け入れることができなくなり、実行中のタスクの実行が完了するのを待ちます。つまり、シャットダウンは単なるコマンドであり、シャットダウンされるかどうかはスレッド自体に依存します。

ThreadPoolExecutorは異なる方法でshutdownNowを処理します。メソッドが実行されると、メソッドはSTOP状態になり、実行中のスレッドでThread.interrupt()メソッドを呼び出します(ただし、スレッドが割り込みを処理しない場合、何も起こりません)。 「すぐに閉じる」という意味ではありません。

shutdownとshutdownNowのJavaドキュメントを確認してください。次のヒントがあります。

shutdown() :Initiates an orderly shutdown in which previously submitted tasks are executed, but no new tasks will be accepted.Invocation has no additional effect if already shut down.This method does not wait for previously submitted tasks to complete execution.Use {
    
    @link #awaitTermination awaitTermination} to do that.
shutdownNow():Attempts to stop all actively executing tasks, halts the processing of waiting tasks, and returns a list of the tasks that were awaiting execution. These tasks are drained (removed) from the task queue upon return from this method.This method does not wait for actively executing tasks to terminate. Use {
    
    @link #awaitTermination awaitTermination} to do that.There are no guarantees beyond best-effort attempts to stop processing actively executing tasks. This implementation cancels tasks via {
    
    @link Thread#interrupt}, so any task that fails to respond to interrupts may never terminate.

どちらもawaitTermination追加の実行方法が必要であることを示唆していますshutdown/shutdownNow実行だけでは不十分です。

最終的な解決策:春のスレッドプールのリサイクル戦略を参照して、最終的な解決策を得ました。



public abstract class ExecutorConfigurationSupport extends CustomizableThreadFactory
      implements DisposableBean{
    
    
    @Override
    public void destroy() {
    
    
        shutdown();
    }
    /**
     * Perform a shutdown on the underlying ExecutorService.
     * @see java.util.concurrent.ExecutorService#shutdown()
     * @see java.util.concurrent.ExecutorService#shutdownNow()
     * @see #awaitTerminationIfNecessary()
     */
    public void shutdown() {
    
    
        if (this.waitForTasksToCompleteOnShutdown) {
    
    
            this.executor.shutdown();
        }
        else {
    
    
            this.executor.shutdownNow();
        }
        awaitTerminationIfNecessary();
    }
    /**
     * Wait for the executor to terminate, according to the value of the
     * {@link #setAwaitTerminationSeconds "awaitTerminationSeconds"} property.
     */
    private void awaitTerminationIfNecessary() {
    
    
        if (this.awaitTerminationSeconds > 0) {
    
    
            try {
    
    
                this.executor.awaitTermination(this.awaitTerminationSeconds, TimeUnit.SECONDS));
            }
            catch (InterruptedException ex) {
    
    
                Thread.currentThread().interrupt();
            }
        }
    }
}

コメントは保持され、一部のログコードは削除され、スレッドプールを適切に閉じるための解決策が目の前に表示されます。

  1. waitForTasksToCompleteOnShutdownフラグを使用して、すべてのタスクをすぐに終了するか、タスクが完了するのを待って終了するかを制御します。

  2. executor.awaitTermination(this.awaitTerminationSeconds, TimeUnit.SECONDS)); タスクが無期限に実行されないように待機時間を制御します(すでに前に強調したように、shutdownNowでもスレッドが実行を停止することは保証されていません)。

私たちの思考を必要とするよりエレガントなシャットダウン戦略

RPCの原則を分析する一連の記事で述べたように、服务治理框架通常のシャットダウンの問題は一般的に考慮されます。通常の練習は事先隔断流量継続すること关闭应用です。

一般的なアプローチは、サービスノード从注册中心摘除とサブスクライバーを送信して通知を受信し、ノードを削除して、正常にシャットダウンすることです。

データベース操作に関しては、トランザクションのACID機能を使用して、通常のオフラインは言うまでもなく、クラッシュが停止しても異常なデータが発生しないようにすることができます。

別の例は、メッセージキューがACKメカニズム+メッセージの永続性、またはトランザクションメッセージの保証に依存する可能性があることです。タイミングタスクの多いサービスでは、他のサービスと比較して長時間実行されるサービスであるため、オフラインを処理するときの正常なシャットダウンの問題に特別な注意を払う必要があります。この状況は、ダウンタイムの問題の影響を受けやすくなります。べき等性とフラグを使用して、時間指定タスクを設計できます...

トランザクションやACKなどの機能のサポートにより、ダウンタイムや停電kill -9 pidなどの状況で、サービスの信頼性を可能な限り高めることができます。また、kill -15 pid通常のオフライン状態でのシャットダウン戦略についても考慮する必要があります。最後に、この問題を解決する際に、jvmシャットダウンフックについての理解を追加します。

シャットダウンフックは、フックが終了するまでJVMが確実に実行されるようにします。また、kill -15 pidコマンドを受け取ったときにブロッキング操作を実行すると、JVMをシャットダウンする前にタスクの実行が完了するのを待つことができることもわかります。同時に、kill -15 pidを実行すると一部のアプリケーションが終了できないという問題についても説明しています。はい、割り込みがブロックされています。

その他の優れた記事:https : //blog.csdn.net/u18256007842/article/details/85330504

おすすめ

転載: blog.csdn.net/qq_21383435/article/details/108523917