再談為了提醒明知故犯(在一坑里迭倒兩次不是不多見),由于業務系統中大量使用了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;itry{
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瀏覽量
14343 -
Boot
+關注
關注
0文章
149瀏覽量
35839 -
kill
+關注
關注
0文章
9瀏覽量
2105
原文標題:求求你們別再用 kill -9 了,這才是 Spring Boot 停機的正確方式!!!
文章出處:【微信號:芋道源碼,微信公眾號:芋道源碼】歡迎添加關注!文章轉載請注明出處。
發布評論請先 登錄
相關推薦
評論