springBoot小程序開發的(de)項目,後台如何優雅的(de)停止進程
目錄
何爲優雅關機 kill 指令 Runtime.addShutdownHook Spring 3.2.12 SpringBoot
再談爲了(le)提醒明(míng)知故犯(在一坑裏叠倒兩次不是不多(duō)見),在小程序開發的(de)業務,由于業務系統中大(dà)量使用(yòng)了(le) SpringBoot embedded tomcat 的(de)模式運行,在一些運維腳本中經常看到 Linux 中 kill 指令,然而它的(de)使用(yòng)也(yě)有些講究,要思考如何能做(zuò)到優雅停機。
何爲優雅關機
就是爲确保應用(yòng)關閉時(shí),通(tōng)知應用(yòng)進程釋放所占用(yòng)的(de)資源:
線程池,shutdown(不接受新任務等待處理(lǐ)完)還(hái)是 shutdownNow(調用(yòng) Thread.interrupt 進行中斷) Socket 鏈接,比如:Netty、MQ 告知注冊中心快(kuài)速下(xià)線(靠心跳機制客服早都跳起來(lái)了(le)),比如:Eureka 清理(lǐ)臨時(shí)文件,比如:POI 各種堆内堆外内存釋放
總之,小程序開發的(de)程序進程強行終止會帶來(lái)數據丢失或者終端無法恢複到正常狀态,在分(fēn)布式環境下(xià)還(hái)可(kě)能導緻數據不一緻的(de)情況。
kill 指令
kill -9 pid 可(kě)以模拟了(le)一次系統宕機,系統斷電等極端情況,而 kill -15 pid 則是等待應用(yòng)關閉,執行阻塞操作,有時(shí)候也(yě)會出現無法關閉應用(yòng)的(de)情況(線上理(lǐ)想情況下(xià),是 bug 就該尋根溯源)
#查看jvm進程pid
jps
#列出所有信号名稱
kill -l
# Windows下(xià)信号常量值
# 簡稱 全稱 數值
# 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發出的(de)軟件終止)
# 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)
#....
#可(kě)以理(lǐ)解爲操作系統從内核級别強行殺死某個(gè)進程
kill -9 pid
#理(lǐ)解爲發送一個(gè)通(tōng)知,等待應用(yòng)主動關閉
kill -15 pid
#也(yě)支持信号常量值全稱或簡寫(就是去掉SIG後)
kill -l KILL
思考:JVM 是如何接受處理(lǐ) Linux 信号量的(de)?
當然是在 JVM 啓動時(shí)就加載了(le)自定義 SignalHandler,關閉 JVM 時(shí)觸發對(duì)應的(de) 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);//調用(yòng)Shutdown.exit
}
};
handler = var0;
try {
Signal.handle(new Signal("INT"), var0);//中斷時(shí)
} catch (IllegalArgumentException var3) {
;
}
try {
Signal.handle(new Signal("TERM"), var0);//終止時(shí)
} catch (IllegalArgumentException var2) {
;
}
}
}
}
Runtime.addShutdownHook
在了(le)解 Shutdown.exit 之前,先看:
Runtime.getRuntime().addShutdownHook(shutdownHook);
則是爲 JVM 中增加一個(gè)關閉的(de)鈎子,當 JVM 關閉的(de)時(shí)候調用(yòng)。
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);
}
}
//它含數據結構和(hé)邏輯管理(lǐ)虛拟機關閉序列
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來(lái)exit? */
private static boolean runFinalizersOnExit = false;
// 系統關閉鈎子注冊一個(gè)預定義的(de)插槽.
// 關閉鈎子的(de)列表如下(xià):
// (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];
// 當前運行關閉鈎子的(de)鈎子的(de)索引
private static int currentRunningHook = 0;
/* 前面的(de)靜态字段由這(zhè)個(gè)鎖保護 */
private static class Lock { };
private static Object lock = new Lock();
/* 爲native halt方法提供鎖對(duì)象 */
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時(shí),添加的(de)槽點小于當前執行的(de)槽點位置也(yě)不能添加
if (state > HOOKS || (state == HOOKS && slot <= currentRunningHook))
throw new IllegalStateException("Shutdown in progress");
}
hooks[slot] = hook;
}
}
/* 執行所有注冊的(de)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的(de)操作
*/
static void halt(int status) {
synchronized (haltLock) {
halt0(status);
}
}
//JNI方法
static native void halt0(int status);
// shutdown的(de)執行順序: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時(shí)執行,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不同的(de)是不做(zuò)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 中通(tōng)過 ContextClosedEvent 事件來(lái)觸發一些動作(可(kě)以拓展),主要通(tōng)過 LifecycleProcessor.onClose 來(lái)做(zuò) stopBeans。
由此可(kě)見 Spring 也(yě)基于 JVM 做(zuò)了(le)拓展。
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 {
//發布應用(yòng)内的(de)關閉事件
publishEvent(new ContextClosedEvent(this));
}
catch (Throwable ex) {
logger.warn("Exception thrown from ApplicationListener handling ContextClosedEvent", ex);
}
// 停止所有的(de)Lifecycle beans.
try {
getLifecycleProcessor().onClose();
}
catch (Throwable ex) {
logger.warn("Exception thrown from LifecycleProcessor on context close", ex);
}
// 銷毀spring 的(de) BeanFactory可(kě)能會緩存單例的(de) Bean.
destroyBeans();
// 關閉當前應用(yòng)上下(xià)文(BeanFactory)
closeBeanFactory();
// 執行子類的(de)關閉邏輯
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();
}
SpringBoot
到這(zhè)裏就進入重點了(le),SpringBoot 中有 spring-boot-starter-actuator 模塊提供了(le)一個(gè) restful 接口,用(yòng)于優雅停機。
執行請求 curl -X POST http://127.0.0.1:8088/shutdown ,待關閉成功則返回提示。
注:線上環境該 url 需要設置權限,可(kě)配合 spring-security 使用(yòng)或在 nginx 中限制内網訪問。
#啓用(yòng)shutdown
endpoints.shutdown.enabled=true
#禁用(yòng)密碼驗證
endpoints.shutdown.sensitive=false
#可(kě)統一指定所有endpoints的(de)路徑
management.context-path=/manage
#指定管理(lǐ)端口和(hé)IP
management.port=8088
management.address=127.0.0.1
#開啓shutdown的(de)安全驗證(spring-security)
endpoints.shutdown.sensitive=true
#驗證用(yòng)戶名
security.user.name=admin
#驗證密碼
security.user.password=secret
#角色
management.security.role=SUPERUSER
SpringBoot 的(de) shutdown 原理(lǐ)也(yě)不複雜(zá),其實還(hái)是通(tōng)過調用(yòng) AbstractApplicationContext.close 實現的(de)。
@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();
}
//這(zhè)個(gè)調用(yòng)的(de)就是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;
}
}
}
經過以上操作,可(kě)以優雅的(de)排除使用(yòng)springBoot小程序開發的(de)程序在運行過程中,導緻數據丢失或者意想不到的(de)BUG問題,小夥伴們,有沒有GET到知識點呢(ne)。
掃一掃,關注我們
相關新聞
- JAVA開發之hashMap時(shí)間複雜(zá)度分(fēn)析
- 微信小程序開發偶發性獲取手機号失敗解決方案
- 使用(yòng)JAVA開發小程序時(shí),如何防止接口被頻(pín)繁請求
- app開發制作過程中,使用(yòng)JAVA注解方式,實現權限功能開發
- springBoot小程序開發的(de)項目,後台如何優雅的(de)停止進程
- JAVA知識十連問
- JAVA語言小程序開發之hashMap原理(lǐ)詳解
- mysql常見錯誤詳解
- 小程序open-data組件将于2022年2月(yuè)21日24時(shí)起···
- 爲什(shén)麽重寫了(le)equals方法,就必須重寫hashCode