求求你們別再用 kill -9 了,這才是 Spring Boot 停機的正確方式!!!

来源:https://www.cnblogs.com/javastack/archive/2022/10/21/16814120.html
-Advertisement-
Play Games

再談為了提醒明知故犯(在一坑裡迭倒兩次不是不多見),由於業務系統中大量使用了spring Boot embedded tomcat的模式運行,在一些運維腳本中經常看到Linux 中 kill 指令,然而它的使用也有些講究,要思考如何能做到優雅停機。 何為優雅關機 就是為確保應用關閉時,通知應用進程釋 ...


再談為了提醒明知故犯(在一坑裡迭倒兩次不是不多見),由於業務系統中大量使用了spring Boot embedded tomcat的模式運行,在一些運維腳本中經常看到Linux 中 kill 指令,然而它的使用也有些講究,要思考如何能做到優雅停機。


何為優雅關機

就是為確保應用關閉時,通知應用進程釋放所占用的資源

  • 線程池,shutdown(不接受新任務等待處理完)還是shutdownNow(調用 Thread.interrupt進行中斷)
  • socket 鏈接,比如:netty、mq
  • 告知註冊中心快速下線(靠心跳機制客服早都跳起來了),比如:eureka
  • 清理臨時文件,比如:poi
  • 各種堆內堆外記憶體釋放

總之,進程強行終止會帶來數據丟失或者終端無法恢復到正常狀態,在分散式環境下還可能導致數據不一致的情況。

kill指令

kill -9 pid 可以模擬了一次系統宕機,系統斷電等極端情況,而kill -15 pid 則是等待應用關閉,執行阻塞操作,有時候也會出現無法關閉應用的情況(線上理想情況下,是bug就該尋根溯源)

#查看jvm進程pid
jps
#列出所有信號名稱
kill -l
 
# Windows下信號常量值
# 簡稱  全稱    數值 
# INT   SIGINT     2       Ctrl+C中斷
# ILL   SIGILL     4       非法指令
# FPE   SIGFPE     8       floating point exception(浮點異常)
# SEGV  SIGSEGV    11      segment violation(段錯誤)
# TERM  SIGTERM    5       Software termination signal from kill(Kill發出的軟體終止)
# BREAK SIGBREAK   21      Ctrl-Break sequence(Ctrl+Break中斷)
# ABRT  SIGABRT    22      abnormal termination triggered by abort call(Abort)
 
#linux信號常量值
# 簡稱  全稱  數值  
# HUP   SIGHUP      1    終端斷線  
# INT   SIGINT      2    中斷(同 Ctrl + C)        
# QUIT  SIGQUIT     3    退出(同 Ctrl + \)         
# KILL  SIGKILL     9    強制終止         
# TERM  SIGTERM     15    終止         
# CONT  SIGCONT     18    繼續(與STOP相反, fg/bg命令)         
# STOP  SIGSTOP     19    暫停(同 Ctrl + Z)        
#....
 
#可以理解為操作系統從內核級別強行殺死某個進程
kill -9 pid 
#理解為發送一個通知,等待應用主動關閉
kill -15 pid
#也支持信號常量值全稱或簡寫(就是去掉SIG後)
kill -l KILL

思考:jvm是如何接受處理linux信號量的?

當然是在jvm啟動時就載入了自定義SignalHandler,關閉jvm時觸發對應的handle。

public interface SignalHandler {
    SignalHandler SIG_DFL = new NativeSignalHandler(0L);
    SignalHandler SIG_IGN = new NativeSignalHandler(1L);
 
    void handle(Signal var1);
}
class Terminator {
    private static SignalHandler handler = null;
 
    Terminator() {
    }
    //jvm設置SignalHandler,在System.initializeSystemClass中觸發
    static void setup() {
        if (handler == null) {
            SignalHandler var0 = new SignalHandler() {
                public void handle(Signal var1) {
                    Shutdown.exit(var1.getNumber() + 128);//調用Shutdown.exit
                }
            };
            handler = var0;
 
            try {
                Signal.handle(new Signal("INT"), var0);//中斷時
            } catch (IllegalArgumentException var3) {
                ;
            }
 
            try {
                Signal.handle(new Signal("TERM"), var0);//終止時
            } catch (IllegalArgumentException var2) {
                ;
            }
 
        }
    }
}

Runtime.addShutdownHook

在瞭解Shutdown.exit之前,先看Runtime.getRuntime().addShutdownHook(shutdownHook);則是為jvm中增加一個關閉的鉤子,當jvm關閉的時候調用。

public class Runtime {
    public void addShutdownHook(Thread hook) {
        SecurityManager sm = System.getSecurityManager();
        if (sm != null) {
            sm.checkPermission(new RuntimePermission("shutdownHooks"));
        }
        ApplicationShutdownHooks.add(hook);
    }
}
class ApplicationShutdownHooks {
    /* The set of registered hooks */
    private static IdentityHashMap<Thread, Thread> hooks;
    static synchronized void add(Thread hook) {
        if(hooks == null)
            throw new IllegalStateException("Shutdown in progress");
 
        if (hook.isAlive())
            throw new IllegalArgumentException("Hook already running");
 
        if (hooks.containsKey(hook))
            throw new IllegalArgumentException("Hook previously registered");
 
        hooks.put(hook, hook);
    }
}
//它含數據結構和邏輯管理虛擬機關閉序列
class Shutdown {
    /* Shutdown 系列狀態*/
    private static final int RUNNING = 0;
    private static final int HOOKS = 1;
    private static final int FINALIZERS = 2;
    private static int state = RUNNING;
    /* 是否應該運行所以finalizers來exit? */
    private static boolean runFinalizersOnExit = false;
    // 系統關閉鉤子註冊一個預定義的插槽.
    // 關閉鉤子的列表如下:
    // (0) Console restore hook
    // (1) Application hooks
    // (2) DeleteOnExit hook
    private static final int MAX_SYSTEM_HOOKS = 10;
    private static final Runnable[] hooks = new Runnable[MAX_SYSTEM_HOOKS];
    // 當前運行關閉鉤子的鉤子的索引
    private static int currentRunningHook = 0;
    /* 前面的靜態欄位由這個鎖保護 */
    private static class Lock { };
    private static Object lock = new Lock();
 
    /* 為native halt方法提供鎖對象 */
    private static Object haltLock = new Lock();
 
    static void add(int slot, boolean registerShutdownInProgress, Runnable hook) {
        synchronized (lock) {
            if (hooks[slot] != null)
                throw new InternalError("Shutdown hook at slot " + slot + " already registered");
 
            if (!registerShutdownInProgress) {//執行shutdown過程中不添加hook
                if (state > RUNNING)//如果已經在執行shutdown操作不能添加hook
                    throw new IllegalStateException("Shutdown in progress");
            } else {//如果hooks已經執行完畢不能再添加hook。如果正在執行hooks時,添加的槽點小於當前執行的槽點位置也不能添加
                if (state > HOOKS || (state == HOOKS && slot <= currentRunningHook))
                    throw new IllegalStateException("Shutdown in progress");
            }
 
            hooks[slot] = hook;
        }
    }
    /* 執行所有註冊的hooks
     */
    private static void runHooks() {
        for (int i=0; i < MAX_SYSTEM_HOOKS; i++) {
            try {
                Runnable hook;
                synchronized (lock) {
                    // acquire the lock to make sure the hook registered during
                    // shutdown is visible here.
                    currentRunningHook = i;
                    hook = hooks[i];
                }
                if (hook != null) hook.run();
            } catch(Throwable t) {
                if (t instanceof ThreadDeath) {
                    ThreadDeath td = (ThreadDeath)t;
                    throw td;
                }
            }
        }
    }
    /* 關閉JVM的操作
     */
    static void halt(int status) {
        synchronized (haltLock) {
            halt0(status);
        }
    }
    //JNI方法
    static native void halt0(int status);
    // shutdown的執行順序:runHooks > runFinalizersOnExit
    private static void sequence() {
        synchronized (lock) {
            /* Guard against the possibility of a daemon thread invoking exit
             * after DestroyJavaVM initiates the shutdown sequence
             */
            if (state != HOOKS) return;
        }
        runHooks();
        boolean rfoe;
        synchronized (lock) {
            state = FINALIZERS;
            rfoe = runFinalizersOnExit;
        }
        if (rfoe) runAllFinalizers();
    }
    //Runtime.exit時執行,runHooks > runFinalizersOnExit > halt
    static void exit(int status) {
        boolean runMoreFinalizers = false;
        synchronized (lock) {
            if (status != 0) runFinalizersOnExit = false;
            switch (state) {
            case RUNNING:       /* Initiate shutdown */
                state = HOOKS;
                break;
            case HOOKS:         /* Stall and halt */
                break;
            case FINALIZERS:
                if (status != 0) {
                    /* Halt immediately on nonzero status */
                    halt(status);
                } else {
                    /* Compatibility with old behavior:
                     * Run more finalizers and then halt
                     */
                    runMoreFinalizers = runFinalizersOnExit;
                }
                break;
            }
        }
        if (runMoreFinalizers) {
            runAllFinalizers();
            halt(status);
        }
        synchronized (Shutdown.class) {
            /* Synchronize on the class object, causing any other thread
             * that attempts to initiate shutdown to stall indefinitely
             */
            sequence();
            halt(status);
        }
    }
    //shutdown操作,與exit不同的是不做halt操作(關閉JVM)
    static void shutdown() {
        synchronized (lock) {
            switch (state) {
            case RUNNING:       /* Initiate shutdown */
                state = HOOKS;
                break;
            case HOOKS:         /* Stall and then return */
            case FINALIZERS:
                break;
            }
        }
        synchronized (Shutdown.class) {
            sequence();
        }
    }
}

spring 3.2.12

在spring中通過ContextClosedEvent事件來觸發一些動作(可以拓展),主要通過LifecycleProcessor.onClose來做stopBeans。由此可見spring也基於jvm做了拓展。

推薦一個開源免費的 Spring Boot 最全教程:

https://github.com/javastacks/spring-boot-best-practice

public abstract class AbstractApplicationContext extends DefaultResourceLoader {
 public void registerShutdownHook() {
  if (this.shutdownHook == null) {
   // No shutdown hook registered yet.
   this.shutdownHook = new Thread() {
    @Override
    public void run() {
     doClose();
    }
   };
   Runtime.getRuntime().addShutdownHook(this.shutdownHook);
  }
 }
 protected void doClose() {
  boolean actuallyClose;
  synchronized (this.activeMonitor) {
   actuallyClose = this.active && !this.closed;
   this.closed = true;
  }
 
  if (actuallyClose) {
   if (logger.isInfoEnabled()) {
    logger.info("Closing " + this);
   }
 
   LiveBeansView.unregisterApplicationContext(this);
 
   try {
    //發佈應用內的關閉事件
    publishEvent(new ContextClosedEvent(this));
   }
   catch (Throwable ex) {
    logger.warn("Exception thrown from ApplicationListener handling ContextClosedEvent", ex);
   }
 
   // 停止所有的Lifecycle beans.
   try {
    getLifecycleProcessor().onClose();
   }
   catch (Throwable ex) {
    logger.warn("Exception thrown from LifecycleProcessor on context close", ex);
   }
 
   // 銷毀spring 的 BeanFactory可能會緩存單例的 Bean.
   destroyBeans();
 
   // 關閉當前應用上下文(BeanFactory)
   closeBeanFactory();
 
   // 執行子類的關閉邏輯
   onClose();
 
   synchronized (this.activeMonitor) {
    this.active = false;
   }
  }
 } 
}
public interface LifecycleProcessor extends Lifecycle {
 /**
  * Notification of context refresh, e.g. for auto-starting components.
  */
 void onRefresh();
 
 /**
  * Notification of context close phase, e.g. for auto-stopping components.
  */
 void onClose();
}

spring boot

到這裡就進入重點了,spring boot中有spring-boot-starter-actuator 模塊提供了一個 restful 介面,用於優雅停機。執行請求 curl -X POST http://127.0.0.1:8088/shutdown ,待關閉成功則返回提示。

Spring Boot 基礎就不介紹了,推薦看這個免費教程:

https://github.com/javastacks/spring-boot-best-practice

註:線上環境該url需要設置許可權,可配合 spring-security使用或在nginx中限制內網訪問

#啟用shutdown
endpoints.shutdown.enabled=true
#禁用密碼驗證
endpoints.shutdown.sensitive=false
#可統一指定所有endpoints的路徑
management.context-path=/manage
#指定管理埠和IP
management.port=8088
management.address=127.0.0.1

#開啟shutdown的安全驗證(spring-security)
endpoints.shutdown.sensitive=true
#驗證用戶名
security.user.name=admin
#驗證密碼
security.user.password=secret
#角色
management.security.role=SUPERUSER

spring boot的shutdown原理也不複雜,其實還是通過調用AbstractApplicationContext.close實現的。

@ConfigurationProperties(
    prefix = "endpoints.shutdown"
)
public class ShutdownMvcEndpoint extends EndpointMvcAdapter {
    public ShutdownMvcEndpoint(ShutdownEndpoint delegate) {
        super(delegate);
    }
    //post請求
    @PostMapping(
        produces = {"application/vnd.spring-boot.actuator.v1+json", "application/json"}
    )
    @ResponseBody
    public Object invoke() {
        return !this.getDelegate().isEnabled() ? new ResponseEntity(Collections.singletonMap("message", "This endpoint is disabled"), HttpStatus.NOT_FOUND) : super.invoke();
    }
}
@ConfigurationProperties(
    prefix = "endpoints.shutdown"
)
public class ShutdownEndpoint extends AbstractEndpoint<Map<String, Object>> implements ApplicationContextAware {
    private static final Map<String, Object> NO_CONTEXT_MESSAGE = Collections.unmodifiableMap(Collections.singletonMap("message", "No context to shutdown."));
    private static final Map<String, Object> SHUTDOWN_MESSAGE = Collections.unmodifiableMap(Collections.singletonMap("message", "Shutting down, bye..."));
    private ConfigurableApplicationContext context;
 
    public ShutdownEndpoint() {
        super("shutdown", true, false);
    }
    //執行關閉
    public Map<String, Object> invoke() {
        if (this.context == null) {
            return NO_CONTEXT_MESSAGE;
        } else {
            boolean var6 = false;
 
            Map var1;
 
            class NamelessClass_1 implements Runnable {
                NamelessClass_1() {
                }
 
                public void run() {
                    try {
                        Thread.sleep(500L);
                    } catch (InterruptedException var2) {
                        Thread.currentThread().interrupt();
                    }
                    //這個調用的就是AbstractApplicationContext.close
                    ShutdownEndpoint.this.context.close();
                }
            }
 
            try {
                var6 = true;
                var1 = SHUTDOWN_MESSAGE;
                var6 = false;
            } finally {
                if (var6) {
                    Thread thread = new Thread(new NamelessClass_1());
                    thread.setContextClassLoader(this.getClass().getClassLoader());
                    thread.start();
                }
            }
 
            Thread thread = new Thread(new NamelessClass_1());
            thread.setContextClassLoader(this.getClass().getClassLoader());
            thread.start();
            return var1;
        }
    }
}

版權聲明:本文為CSDN博主「佈道」的原創文章,遵循CC 4.0 BY-SA版權協議,轉載請附上原文出處鏈接及本聲明。原文鏈接:https://blog.csdn.net/alex_xfboy/article/details/90404691

近期熱文推薦:

1.1,000+ 道 Java面試題及答案整理(2022最新版)

2.勁爆!Java 協程要來了。。。

3.Spring Boot 2.x 教程,太全了!

4.別再寫滿屏的爆爆爆炸類了,試試裝飾器模式,這才是優雅的方式!!

5.《Java開發手冊(嵩山版)》最新發佈,速速下載!

覺得不錯,別忘了隨手點贊+轉發哦!


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • Module 模塊例化的兩種方式:按埠位置例化、按埠名例化。 module top_module ( input a, input b, output out ); mod_a instance1 ( .in1(a), .in2(b), .out(out) ); endmodule Module ...
  • Jupyter Notebook 有兩種鍵盤輸入模式。 編輯模式,允許你往單元中鍵入代碼或文本;這時的單元框線是綠色的。 命令模式,鍵盤輸入運行程式命令;這時的單元框線是灰色。 1命令模式 (按鍵 Esc 開啟)快捷鍵: Enter : 轉入編輯模式 Shift-Enter : 運行本單元,選中下個 ...
  • 滿漢樓03 4.功能實現05 4.8查看賬單功能 按照之間搭建起來的框架,在BillService編寫方法 4.8.1代碼實現 1.修改Bill類 重寫Bill類中的toString方法 @Override public String toString() { return id + "\t\t" ...
  • 介面文檔 介面編寫已經寫完了,需要編寫介面文檔,給前端的人使用 -請求地址 -請求方式 -支持的編碼格式 -請求參數(get,post參數) -返回格式示例 在公司的寫法 1)直接使用word或者md寫2)使用介面文檔平臺,在介面文檔平臺錄入(Yapi(百度開源的自己搭建),第三方平臺(收費),自己 ...
  • 前言 大家早好、午好、晚好吖~ 今天我們來採集一下這個小姐姐網~ 環境使用: Python 3.8 解釋器 Pycharm 編輯器 import re import requests >>> pip install requests 如果安裝python第三方模塊: win + R 輸入 cmd 點 ...
  • 數據類型轉換:將自身數據類型轉化成新的數據類型,並擁有新數據類型相關操作的過程; 為方便更好的幫助處理業務,將數據變更為更適合業務場景的類型; a = '1', 此時想使用數字的數學操作,就需要先將字元串轉化為數字類型; 1.數字與字元串間的轉換 # 字元串轉換成整數 a = '34' b = in ...
  • 為什麼要使用分頁 我們數據表中可能會有成千上萬條數據,當我們訪問某張表的所有數據時,我們不太可能需要一次把所有的數據都展示出來,因為數據量很大,對服務端的記憶體壓力比較大還有就是網路傳輸過程中耗時也會比較大。 通常我們會希望一部分一部分去請求數據,也就是我們常說的一頁一頁獲取數據並展示出來。 分頁的三 ...
  • member functions的調用方式 c++支持三種類型的member functions:static、nonstatic、virtual,且每一種調用方式不盡相同 nonstatic member functions nonstatic member function至少和nonmembe ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...