再谈为了提醒明知故犯(在一坑里迭倒两次不是不多见),由于业务系统中大量使用了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
>基于SpringCloudAlibaba+Gateway+Nacos+RocketMQ+Vue&Element实现的后台管理系统+用户小程序,支持RBAC动态权限、多租户、数据权限、工作流、三方登录、支付、短信、商城等功能
>
>*项目地址://github.com/YunaiV/yudao-cloud>
>*视频教程://doc.iocoder.cn/video/>
#Windows下信号常量值
#简称全称数值
#INTSIGINT2Ctrl+C中断
#ILLSIGILL4非法指令
#FPESIGFPE8floatingpointexception(浮点异常)
#SEGVSIGSEGV11segmentviolation(段错误)
#TERMSIGTERM5Softwareterminationsignalfromkill(Kill发出的软件终止)
#BREAKSIGBREAK21Ctrl-Breaksequence(Ctrl+Break中断)
#ABRTSIGABRT22abnormalterminationtriggeredbyabortcall(Abort)
#linux信号常量值
#简称全称数值
#HUPSIGHUP1终端断线
#INTSIGINT2中断(同Ctrl+C)
#QUITSIGQUIT3退出(同Ctrl+)
#KILLSIGKILL9强制终止
#TERMSIGTERM15终止
#CONTSIGCONT18继续(与STOP相反,fg/bg命令)
#STOPSIGSTOP19暂停(同Ctrl+Z)
#....
#可以理解为操作系统从内核级别强行杀死某个进程
kill-9pid
#理解为发送一个通知,等待应用主动关闭
kill-15pid
#也支持信号常量值全称或简写(就是去掉SIG后)
kill-lKILL
思考:jvm是如何接受处理linux信号量的?
当然是在jvm启动时就加载了自定义SignalHandler
,关闭jvm时触发对应的handle。
publicinterfaceSignalHandler{
SignalHandlerSIG_DFL=newNativeSignalHandler(0L);
SignalHandlerSIG_IGN=newNativeSignalHandler(1L);
voidhandle(Signalvar1);
}
classTerminator{
privatestaticSignalHandlerhandler=null;
Terminator(){
}
//jvm设置SignalHandler,在System.initializeSystemClass中触发
staticvoidsetup(){
if(handler==null){
SignalHandlervar0=newSignalHandler(){
publicvoidhandle(Signalvar1){
Shutdown.exit(var1.getNumber()+128);//调用Shutdown.exit
}
};
handler=var0;
try{
Signal.handle(newSignal("INT"),var0);//中断时
}catch(IllegalArgumentExceptionvar3){
;
}
try{
Signal.handle(newSignal("TERM"),var0);//终止时
}catch(IllegalArgumentExceptionvar2){
;
}
}
}
}
Runtime.addShutdownHook
在了解Shutdown.exit
之前,先看Runtime.getRuntime().addShutdownHook(shutdownHook);
则是为jvm中增加一个关闭的钩子,当jvm关闭的时候调用。
publicclassRuntime{
publicvoidaddShutdownHook(Threadhook){
SecurityManagersm=System.getSecurityManager();
if(sm!=null){
sm.checkPermission(newRuntimePermission("shutdownHooks"));
}
ApplicationShutdownHooks.add(hook);
}
}
classApplicationShutdownHooks{
/*Thesetofregisteredhooks*/
privatestaticIdentityHashMaphooks;
staticsynchronizedvoidadd(Threadhook){
if(hooks==null)
thrownewIllegalStateException("Shutdowninprogress");
if(hook.isAlive())
thrownewIllegalArgumentException("Hookalreadyrunning");
if(hooks.containsKey(hook))
thrownewIllegalArgumentException("Hookpreviouslyregistered");
hooks.put(hook,hook);
}
}
//它含数据结构和逻辑管理虚拟机关闭序列
classShutdown{
/*Shutdown系列状态*/
privatestaticfinalintRUNNING=0;
privatestaticfinalintHOOKS=1;
privatestaticfinalintFINALIZERS=2;
privatestaticintstate=RUNNING;
/*是否应该运行所以finalizers来exit?*/
privatestaticbooleanrunFinalizersOnExit=false;
//系统关闭钩子注册一个预定义的插槽.
//关闭钩子的列表如下:
//(0)Consolerestorehook
//(1)Applicationhooks
//(2)DeleteOnExithook
privatestaticfinalintMAX_SYSTEM_HOOKS=10;
privatestaticfinalRunnable[]hooks=newRunnable[MAX_SYSTEM_HOOKS];
//当前运行关闭钩子的钩子的索引
privatestaticintcurrentRunningHook=0;
/*前面的静态字段由这个锁保护*/
privatestaticclassLock{};
privatestaticObjectlock=newLock();
/*为nativehalt方法提供锁对象*/
privatestaticObjecthaltLock=newLock();
staticvoidadd(intslot,booleanregisterShutdownInProgress,Runnablehook){
synchronized(lock){
if(hooks[slot]!=null)
thrownewInternalError("Shutdownhookatslot"+slot+"alreadyregistered");
if(!registerShutdownInProgress){//执行shutdown过程中不添加hook
if(state>RUNNING)//如果已经在执行shutdown操作不能添加hook
thrownewIllegalStateException("Shutdowninprogress");
}else{//如果hooks已经执行完毕不能再添加hook。如果正在执行hooks时,添加的槽点小于当前执行的槽点位置也不能添加
if(state>HOOKS||(state==HOOKS&&slot<= currentRunningHook))
thrownewIllegalStateException("Shutdowninprogress");
}
hooks[slot]=hook;
}
}
/*执行所有注册的hooks
*/
privatestaticvoidrunHooks(){
for(inti=0;i< MAX_SYSTEM_HOOKS; i++) {
try{
Runnablehook;
synchronized(lock){
//acquirethelocktomakesurethehookregisteredduring
//shutdownisvisiblehere.
currentRunningHook=i;
hook=hooks[i];
}
if(hook!=null)hook.run();
}catch(Throwablet){
if(tinstanceofThreadDeath){
ThreadDeathtd=(ThreadDeath)t;
throwtd;
}
}
}
}
/*关闭JVM的操作
*/
staticvoidhalt(intstatus){
synchronized(haltLock){
halt0(status);
}
}
//JNI方法
staticnativevoidhalt0(intstatus);
//shutdown的执行顺序:runHooks>runFinalizersOnExit
privatestaticvoidsequence(){
synchronized(lock){
/*Guardagainstthepossibilityofadaemonthreadinvokingexit
*afterDestroyJavaVMinitiatestheshutdownsequence
*/
if(state!=HOOKS)return;
}
runHooks();
booleanrfoe;
synchronized(lock){
state=FINALIZERS;
rfoe=runFinalizersOnExit;
}
if(rfoe)runAllFinalizers();
}
//Runtime.exit时执行,runHooks>runFinalizersOnExit>halt
staticvoidexit(intstatus){
booleanrunMoreFinalizers=false;
synchronized(lock){
if(status!=0)runFinalizersOnExit=false;
switch(state){
caseRUNNING:/*Initiateshutdown*/
state=HOOKS;
break;
caseHOOKS:/*Stallandhalt*/
break;
caseFINALIZERS:
if(status!=0){
/*Haltimmediatelyonnonzerostatus*/
halt(status);
}else{
/*Compatibilitywitholdbehavior:
*Runmorefinalizersandthenhalt
*/
runMoreFinalizers=runFinalizersOnExit;
}
break;
}
}
if(runMoreFinalizers){
runAllFinalizers();
halt(status);
}
synchronized(Shutdown.class){
/*Synchronizeontheclassobject,causinganyotherthread
*thatattemptstoinitiateshutdowntostallindefinitely
*/
sequence();
halt(status);
}
}
//shutdown操作,与exit不同的是不做halt操作(关闭JVM)
staticvoidshutdown(){
synchronized(lock){
switch(state){
caseRUNNING:/*Initiateshutdown*/
state=HOOKS;
break;
caseHOOKS:/*Stallandthenreturn*/
caseFINALIZERS:
break;
}
}
synchronized(Shutdown.class){
sequence();
}
}
}
spring 3.2.12
在spring中通过ContextClosedEvent
事件来触发一些动作(可以拓展),主要通过LifecycleProcessor.onClose
来做stopBeans
。由此可见spring也基于jvm做了拓展。
publicabstractclassAbstractApplicationContextextendsDefaultResourceLoader{
publicvoidregisterShutdownHook(){
if(this.shutdownHook==null){
//Noshutdownhookregisteredyet.
this.shutdownHook=newThread(){
@Override
publicvoidrun(){
doClose();
}
};
Runtime.getRuntime().addShutdownHook(this.shutdownHook);
}
}
protectedvoiddoClose(){
booleanactuallyClose;
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(newContextClosedEvent(this));
}
catch(Throwableex){
logger.warn("ExceptionthrownfromApplicationListenerhandlingContextClosedEvent",ex);
}
//停止所有的Lifecyclebeans.
try{
getLifecycleProcessor().onClose();
}
catch(Throwableex){
logger.warn("ExceptionthrownfromLifecycleProcessoroncontextclose",ex);
}
//销毁spring的BeanFactory可能会缓存单例的Bean.
destroyBeans();
//关闭当前应用上下文(BeanFactory)
closeBeanFactory();
//执行子类的关闭逻辑
onClose();
synchronized(this.activeMonitor){
this.active=false;
}
}
}
}
publicinterfaceLifecycleProcessorextendsLifecycle{
/**
*Notificationofcontextrefresh,e.g.forauto-startingcomponents.
*/
voidonRefresh();
/**
*Notificationofcontextclosephase,e.g.forauto-stoppingcomponents.
*/
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(ShutdownEndpointdelegate){
super(delegate);
}
//post请求
@PostMapping(
produces={"application/vnd.spring-boot.actuator.v1+json","application/json"}
)
@ResponseBody
publicObjectinvoke(){
return!this.getDelegate().isEnabled()?newResponseEntity(Collections.singletonMap("message","Thisendpointisdisabled"),HttpStatus.NOT_FOUND):super.invoke();
}
}
@ConfigurationProperties(
prefix="endpoints.shutdown"
)
publicclassShutdownEndpointextendsAbstractEndpoint<Map<String,Object>>implementsApplicationContextAware{
privatestaticfinalMapNO_CONTEXT_MESSAGE=Collections.unmodifiableMap(Collections.singletonMap("message","Nocontexttoshutdown."));
privatestaticfinalMapSHUTDOWN_MESSAGE=Collections.unmodifiableMap(Collections.singletonMap("message","Shuttingdown,bye..."));
privateConfigurableApplicationContextcontext;
publicShutdownEndpoint(){
super("shutdown",true,false);
}
//执行关闭
publicMapinvoke() {
if(this.context==null){
returnNO_CONTEXT_MESSAGE;
}else{
booleanvar6=false;
Mapvar1;
classNamelessClass_1implementsRunnable{
NamelessClass_1(){
}
publicvoidrun(){
try{
Thread.sleep(500L);
}catch(InterruptedExceptionvar2){
Thread.currentThread().interrupt();
}
//这个调用的就是AbstractApplicationContext.close
ShutdownEndpoint.this.context.close();
}
}
try{
var6=true;
var1=SHUTDOWN_MESSAGE;
var6=false;
}finally{
if(var6){
Threadthread=newThread(newNamelessClass_1());
thread.setContextClassLoader(this.getClass().getClassLoader());
thread.start();
}
}
Threadthread=newThread(newNamelessClass_1());
thread.setContextClassLoader(this.getClass().getClassLoader());
thread.start();
returnvar1;
}
}
}
审核编辑 :李倩
-
spring
+关注
关注
0文章
340浏览量
14353 -
Boot
+关注
关注
0文章
150浏览量
35851 -
kill
+关注
关注
0文章
9浏览量
2111
原文标题:求求你们别再用 kill -9 了,这才是 Spring Boot 停机的正确方式!!!
文章出处:【微信号:芋道源码,微信公众号:芋道源码】欢迎添加关注!文章转载请注明出处。
发布评论请先 登录
相关推荐
评论