👉 这是一个或许对你有用的社群
🐱 一对一交流/面试小册/简历优化/求职解惑,欢迎加入芋道快速开发平台知识星球。下面是星球提供的部分资料:
👉这是一个或许对你有用的开源项目
国产 Star 破 10w+ 的开源项目,前端包括管理后台 + 微信小程序,后端支持单体和微服务架构。
功能涵盖 RBAC 权限、SaaS 多租户、数据权限、商城、支付、工作流、大屏报表、微信公众号、CRM 等等功能:
  • Boot 仓库:https://gitee.com/zhijiantianya/ruoyi-vue-pro
  • Cloud 仓库:https://gitee.com/zhijiantianya/yudao-cloud
  • 视频教程:https://doc.iocoder.cn
【国内首批】支持 JDK 21 + SpringBoot 3.2.0、JDK 8 + Spring Boot 2.7.18 双版本 
来源:blog.csdn.net/alex_xfboy/
article/details/90404691/

再谈为了提醒明知故犯(在一坑里迭倒两次不是不多见),由于业务系统中大量使用了spring Boot embedded tomcat的模式运行,在一些运维脚本中经常看到Linux 中 kill 指令,然而它的使用也有些讲究,要思考如何能做到优雅停机。

何为优雅关机

就是为确保应用关闭时,通知应用进程释放所占用的资源
  • 线程池,shutdown(不接受新任务等待处理完)还是shutdownNow(调用 Thread.interrupt进行中断)
  • socket 链接,比如:netty、mq
  • 告知注册中心快速下线(靠心跳机制客服早都跳起来了),比如:eureka
  • 清理临时文件,比如:poi
  • 各种堆内堆外内存释放
总之,进程强行终止会带来数据丢失或者终端无法恢复到正常状态,在分布式环境下还可能导致数据不一致的情况。
基于 Spring Boot + MyBatis Plus + Vue & Element 实现的后台管理系统 + 用户小程序,支持 RBAC 动态权限、多租户、数据权限、工作流、三方登录、支付、短信、商城等功能
  • 项目地址:https://github.com/YunaiV/ruoyi-vue-pro
  • 视频教程:https://doc.iocoder.cn/video/

kill指令

kill -9 pid 可以模拟了一次系统宕机,系统断电等极端情况,而kill -15 pid 则是等待应用关闭,执行阻塞操作,有时候也会出现无法关闭应用的情况(线上理想情况下,是bug就该寻根溯源)
#查看jvm进程pid

jps

#列出所有信号名称

kill -l



> 基于 Spring Cloud Alibaba + Gateway + Nacos + RocketMQ + Vue & Element 实现的后台管理系统 + 用户小程序,支持 RBAC 动态权限、多租户、数据权限、工作流、三方登录、支付、短信、商城等功能

>

> * 项目地址:<https:
//github.com/YunaiV/yudao-cloud>
> * 视频教程:<https:
//doc.iocoder.cn/video/>

# 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。
publicinterfaceSignalHandler
{

    SignalHandler SIG_DFL = 
new
 NativeSignalHandler(
0L
);

    SignalHandler SIG_IGN = 
new
 NativeSignalHandler(
1L
);


voidhandle(Signal var1)
;

}

classTerminator
{

privatestatic
 SignalHandler handler = 
null
;


    Terminator() {

    }

//jvm设置SignalHandler,在System.initializeSystemClass中触发
staticvoidsetup()
{

if
 (handler == 
null
) {

            SignalHandler var0 = 
new
 SignalHandler() {

publicvoidhandle(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关闭的时候调用。
publicclassRuntime
{

publicvoidaddShutdownHook(Thread hook)
{

        SecurityManager sm = System.getSecurityManager();

if
 (sm != 
null
) {

            sm.checkPermission(
new
 RuntimePermission(
"shutdownHooks"
));

        }

        ApplicationShutdownHooks.add(hook);

    }

}

classApplicationShutdownHooks
{

/* The set of registered hooks */
privatestatic
 IdentityHashMap<Thread, Thread> hooks;

staticsynchronizedvoidadd(Thread hook)
{

if
(hooks == 
null
)

thrownew
 IllegalStateException(
"Shutdown in progress"
);


if
 (hook.isAlive())

thrownew
 IllegalArgumentException(
"Hook already running"
);


if
 (hooks.containsKey(hook))

thrownew
 IllegalArgumentException(
"Hook previously registered"
);


        hooks.put(hook, hook);

    }

}

//它含数据结构和逻辑管理虚拟机关闭序列
classShutdown
{

/* Shutdown 系列状态*/
privatestaticfinalint
 RUNNING = 
0
;

privatestaticfinalint
 HOOKS = 
1
;

privatestaticfinalint
 FINALIZERS = 
2
;

privatestaticint
 state = RUNNING;

/* 是否应该运行所以finalizers来exit? */
privatestaticboolean
 runFinalizersOnExit = 
false
;

// 系统关闭钩子注册一个预定义的插槽.
// 关闭钩子的列表如下:
// (0) Console restore hook
// (1) Application hooks
// (2) DeleteOnExit hook
privatestaticfinalint
 MAX_SYSTEM_HOOKS = 
10
;

privatestaticfinal
 Runnable[] hooks = 
new
 Runnable[MAX_SYSTEM_HOOKS];

// 当前运行关闭钩子的钩子的索引
privatestaticint
 currentRunningHook = 
0
;

/* 前面的静态字段由这个锁保护 */
privatestaticclassLock
{ };

privatestatic
 Object lock = 
new
 Lock();


/* 为native halt方法提供锁对象 */
privatestatic
 Object haltLock = 
new
 Lock();


staticvoidadd(int slot, boolean registerShutdownInProgress, Runnable hook)
{

synchronized
 (lock) {

if
 (hooks[slot] != 
null
)

thrownew
 InternalError(
"Shutdown hook at slot "
 + slot + 
" already registered"
);


if
 (!registerShutdownInProgress) {
//执行shutdown过程中不添加hook
if
 (state > RUNNING)
//如果已经在执行shutdown操作不能添加hook
thrownew
 IllegalStateException(
"Shutdown in progress"
);

            } 
else
 {
//如果hooks已经执行完毕不能再添加hook。如果正在执行hooks时,添加的槽点小于当前执行的槽点位置也不能添加
if
 (state > HOOKS || (state == HOOKS && slot <= currentRunningHook))

thrownew
 IllegalStateException(
"Shutdown in progress"
);

            }


            hooks[slot] = hook;

        }

    }

/* 执行所有注册的hooks

     */

privatestaticvoidrunHooks()
{

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的操作

     */

staticvoidhalt(int status)
{

synchronized
 (haltLock) {

            halt0(status);

        }

    }

//JNI方法
staticnativevoidhalt0(int status)
;

// shutdown的执行顺序:runHooks > runFinalizersOnExit
privatestaticvoidsequence()
{

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
staticvoidexit(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)
staticvoidshutdown()
{

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做了拓展。
publicabstractclassAbstractApplicationContextextendsDefaultResourceLoader
{

publicvoidregisterShutdownHook()
{

if
 (
this
.shutdownHook == 
null
) {

// No shutdown hook registered yet.
this
.shutdownHook = 
new
 Thread() {

@Override
publicvoidrun()
{

     doClose();

    }

   };

   Runtime.getRuntime().addShutdownHook(
this
.shutdownHook);

  }

 }

protectedvoiddoClose()
{

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
;

   }

  }

 } 

}

publicinterfaceLifecycleProcessorextendsLifecycle
{

/**

  * Notification of context refresh, e.g. for auto-starting components.

  */

voidonRefresh()
;


/**

  * Notification of context close phase, e.g. for auto-stopping components.

  */

voidonClose()
;

}

spring boot

到这里就进入重点了,spring boot中有spring-boot-starter-actuator 模块提供了一个 restful 接口,用于优雅停机。执行请求 curl -X POST http://127.0.0.1:8088/shutdown ,待关闭成功则返回提示。
注:线上环境该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"
)

publicclassShutdownMvcEndpointextendsEndpointMvcAdapter
{

publicShutdownMvcEndpoint(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"
)

publicclassShutdownEndpointextendsAbstractEndpoint<Map<StringObject>> implementsApplicationContextAware
{

privatestaticfinal
 Map<String, Object> NO_CONTEXT_MESSAGE = Collections.unmodifiableMap(Collections.singletonMap(
"message"
"No context to shutdown."
));

privatestaticfinal
 Map<String, Object> SHUTDOWN_MESSAGE = Collections.unmodifiableMap(Collections.singletonMap(
"message"
"Shutting down, bye..."
));

private
 ConfigurableApplicationContext context;


publicShutdownEndpoint()
{

super
(
"shutdown"
true
false
);

    }

//执行关闭
public Map<String, Object> invoke()
{

if
 (
this
.context == 
null
) {

return
 NO_CONTEXT_MESSAGE;

        } 
else
 {

boolean
 var6 = 
false
;


            Map var1;


classNamelessClass_1implementsRunnable
{

                NamelessClass_1() {

                }


publicvoidrun()
{

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;

        }

    }

}

欢迎加入我的知识星球,全面提升技术能力。
👉 加入方式,长按”或“扫描”下方二维码噢
星球的内容包括:项目实战、面试招聘、源码解析、学习路线。
文章有帮助的话,在看,转发吧。
谢谢支持哟 (*^__^*)
继续阅读
阅读原文