SOFAJRaft原始碼閱讀-ShutdownHook如何優雅的停機

2023-01-25 15:00:24

Java程式經常會遇到程序掛掉的情況,一些狀態沒有正確的儲存下來,這時候就需要在JVM關掉的時候執行一些清理現場的程式碼。JAVA中的ShutdownHook提供了比較好的方案。而在SOFAJRaft-example模組的CounterServer-main方法中就使用了shutdownHook實現優雅停機。
@Author:Akai-yuan
@更新時間:2023/1/25

1.觸發場景與失效場景

JDK提供了Java.Runtime.addShutdownHook(Thread hook)方法,可以註冊一個JVM關閉的勾點這個勾點可以在以下幾種場景中被呼叫:

  1. 程式正常退出
  2. 執行了System.exit()方法
  3. 終端使用Ctrl+C觸發的中斷
  4. 系統關閉
  5. OutOfMemory宕機
  6. 使用Kill pid命令幹掉程序(使用 **kill -9 pid **是不會被呼叫的)

以下幾種情況中是無法被呼叫的:

  1. 通過kill -9命令殺死程序——所以kill -9一定要慎用;
  2. 程式中執行了Runtime.getRuntime().halt()方法;
  3. 作業系統突然崩潰,或機器掉電(用電裝置因斷電、失電、或電的質量達不到要求而不能正常工作)。

2.addShutdownHook方法簡述

Runtime.getRuntime().addShutdownHook(shutdownHook);

該方法指,在JVM中增加一個關閉的勾點,當JVM關閉的時候,會執行系統中已經設定的所有通過方法addShutdownHook新增的勾點,當系統執行完這些勾點後,JVM才會關閉。所以這些勾點可以在JVM關閉的時候進行記憶體清理、物件銷燬、關閉連線等操作。

3.SOFAJRaft中勾點函數的實現

通過反射獲取到grpcServer範例的shutdown方法和awaitTerminationLimit方法,並新增到勾點函數當中

public static void blockUntilShutdown() {
        if (rpcServer == null) {
            return;
        }
        //當RpcFactoryHelper中維護的工廠型別是GrpcRaftRpcFactory時進入if條件內部
        if ("com.alipay.sofa.jraft.rpc.impl.GrpcRaftRpcFactory".equals(RpcFactoryHelper.rpcFactory().getClass()
            .getName())) {
            try {
                //反射獲取grpcServer中維護的(io.grpc包下的)server範例
                Method getServer = rpcServer.getClass().getMethod("getServer");
                Object grpcServer = getServer.invoke(rpcServer);
                //反射獲取server範例的shutdown方法和awaitTerminationLimit方法
                Method shutdown = grpcServer.getClass().getMethod("shutdown");
                Method awaitTerminationLimit = grpcServer.getClass().getMethod("awaitTermination", long.class,
                    TimeUnit.class);
            	//新增一個shutdownHook執行緒執行方法
                Runtime.getRuntime().addShutdownHook(new Thread() {
                    @Override
                    public void run() {
                        try {
                            shutdown.invoke(grpcServer);
                            awaitTerminationLimit.invoke(grpcServer, 30, TimeUnit.SECONDS);
                        } catch (Exception e) {
                            // Use stderr here since the logger may have been reset by its JVM shutdown hook.
                            e.printStackTrace(System.err);
                        }
                    }
                });
                //執行awaitTermination方法
                Method awaitTermination = grpcServer.getClass().getMethod("awaitTermination");
                awaitTermination.invoke(grpcServer);
            } catch (Exception e) {
                LOG.error("Failed to block grpc server", e);
            }
        }
    }

4.grpc中的shutdown方法

GrpcServer下的shutdown方法與本文的勾點函數無關,此處再對比分析一下GrpcServer的shutdown方法。

    public void shutdown() {
        //CAS
        //當且僅當期待值為true時(與當前AtomicBoolean型別的started一致),設定為false關閉
        if (!this.started.compareAndSet(true, false)) {
            return;
        }
        ExecutorServiceHelper.shutdownAndAwaitTermination(this.defaultExecutor);
        GrpcServerHelper.shutdownAndAwaitTermination(this.server);
    }

ExecutorServiceHelper#shutdownAndAwaitTermination:
我們可以發現實際上就是在執行ExecutorService 中 的shutdown()、shutdownNow()、awaitTermination() 方法,那麼我們來區別以下這幾個方法

public static boolean shutdownAndAwaitTermination(final ExecutorService pool, final long timeoutMillis) {
        if (pool == null) {
            return true;
        }
        // 禁止提交新任務
        pool.shutdown();
        final TimeUnit unit = TimeUnit.MILLISECONDS;
        final long phaseOne = timeoutMillis / 5;
        try {
            // 等待一段時間以終止現有任務
            if (pool.awaitTermination(phaseOne, unit)) {
                return true;
            }
            pool.shutdownNow();
            // 等待一段時間,等待任務響應被取消
            if (pool.awaitTermination(timeoutMillis - phaseOne, unit)) {
                return true;
            }
            LOG.warn("Fail to shutdown pool: {}.", pool);
        } catch (final InterruptedException e) {
            // (Re-)cancel if current thread also interrupted
            pool.shutdownNow();
            // preserve interrupt status
            Thread.currentThread().interrupt();
        }
        return false;
    }

  1. shutdown():停止接收新任務,原來的任務繼續執行

1、停止接收新的submit的任務;
2、已經提交的任務(包括正在跑的和佇列中等待的),會繼續執行完成;
3、等到第2步完成後,才真正停止;


  1. shutdownNow():停止接收新任務,原來的任務停止執行

1、跟 shutdown() 一樣,先停止接收新submit的任務;
2、忽略佇列裡等待的任務;
3、嘗試將正在執行的任務interrupt中斷;
4、返回未執行的任務列表;
說明:
它試圖終止執行緒的方法是通過呼叫 Thread.interrupt() 方法來實現的,這種方法的作用有限,如果執行緒中沒有sleep 、wait、Condition、定時鎖等應用, interrupt() 方法是無法中斷當前的執行緒的。
所以,shutdownNow() 並不代表執行緒池就一定立即就能退出,它也可能必須要等待所有正在執行的任務都執行完成了才能退出。但是大多數時候是能立即退出的。


  1. awaitTermination(long timeOut, TimeUnit unit):當前執行緒阻塞

當前執行緒阻塞,直到:

  • 等所有已提交的任務(包括正在跑的和佇列中等待的)執行完;
  • 或者 等超時時間到了(timeout 和 TimeUnit設定的時間);
  • 或者 執行緒被中斷,丟擲InterruptedException

然後會監測 ExecutorService 是否已經關閉,返回true(shutdown請求後所有任務執行完畢)或false(已超時)

GrpcServerHelper#shutdownAndAwaitTermination
與ExecutorServiceHelper類中的shutdownAndAwaitTermination方法類似的,該方法將優雅的關閉grpcServer.

public static boolean shutdownAndAwaitTermination(final Server server, final long timeoutMillis) {
        if (server == null) {
            return true;
        }
        // disable new tasks from being submitted
        server.shutdown();
        final TimeUnit unit = TimeUnit.MILLISECONDS;
        final long phaseOne = timeoutMillis / 5;
        try {
            // wait a while for existing tasks to terminate
            if (server.awaitTermination(phaseOne, unit)) {
                return true;
            }
            server.shutdownNow();
            // wait a while for tasks to respond to being cancelled
            if (server.awaitTermination(timeoutMillis - phaseOne, unit)) {
                return true;
            }
            LOG.warn("Fail to shutdown grpc server: {}.", server);
        } catch (final InterruptedException e) {
            // (Re-)cancel if current thread also interrupted
            server.shutdownNow();
            // 保持中斷狀態
            Thread.currentThread().interrupt();
        }
        return false;
    }