网易首页 > 网易号 > 正文 申请入驻

求求你们别再用 kill -9 了,这才是 Spring Boot 停机的正确方式!

0
分享至

Java精选面试题(微信小程序):5000+道面试题和选择题,真实面经简历模版,包含Java基础、并发、JVM、线程、MQ系列、Redis、Spring系列、Elasticsearch、Docker、K8s、Flink、Spark、架构设计、大厂真题等,在线随时刷题!

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

1. 何为优雅关机

就是为确保应用关闭时,通知应用进程释放所占用的资源

  • 线程池,shutdown(不接受新任务等待处理完)还是shutdownNow(调用 Thread.interrupt进行中断)

  • socket 链接,比如:netty、mq

  • 告知注册中心快速下线(靠心跳机制客服早都跳起来了),比如:eureka

  • 清理临时文件,比如:poi

  • 各种堆内堆外内存释放

总之,进程强行终止会带来数据丢失或者终端无法恢复到正常状态,在分布式环境下还可能导致数据不一致的情况。

2. kill 指令

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

#查看jvm进程pid jps #列出所有信号名称 kill -l > 基于 Spring Cloud Alibaba + Gateway + Nacos + RocketMQ + Vue & Element 实现的后台管理系统 + 用户小程序,支持 RBAC 动态权限、多租户、数据权限、工作流、三方登录、支付、短信、商城等功能 > > * 项目地址: //github.com/YunaiV/yudao-cloud> > * 视频教程: //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。

public interface SignalHandler{     SignalHandler SIG_DFL = new NativeSignalHandler(0L);     SignalHandler SIG_IGN = new NativeSignalHandler(1L);     voidhandle(Signal var1); } class Terminator{     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 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

 > implementsApplicationContextAware{     privatestaticfinal Map NO_CONTEXT_MESSAGE = Collections.unmodifiableMap(Collections.singletonMap( "message", "No context to shutdown."));     privatestaticfinal Map SHUTDOWN_MESSAGE = Collections.unmodifiableMap(Collections.singletonMap( "message", "Shutting down, bye..."));     private ConfigurableApplicationContext context;     publicShutdownEndpoint(){         super("shutdown", true, false);     }     //执行关闭     public Map   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;         }     } }

引用资料:https://linux.die.net/man/1/kill

作者:布道

来源:https://juejin.cn/post/7394073179483947008

公众号“Java精选”所发表内容注明来源的,版权归原出处所有(无法查证版权的或者未注明出处的均来自网络,系转载,转载的目的在于传递更多信息,版权属于原作者。如有侵权,请联系,笔者会第一时间删除处理!

最近有很多人问,有没有读者交流群!加入方式很简单,公众号Java精选,回复“加群”,即可入群!

文章有帮助的话,点在看,转发吧!

特别声明:以上内容(如有图片或视频亦包括在内)为自媒体平台“网易号”用户上传并发布,本平台仅提供信息存储服务。

Notice: The content above (including the pictures and videos if any) is uploaded and posted by a user of NetEase Hao, which is a social media platform and only provides information storage services.

相关推荐
热点推荐
0-2大冷门,世界第4不敌世界第76,无缘ATP250斯海尔托亨博斯站四强

0-2大冷门,世界第4不敌世界第76,无缘ATP250斯海尔托亨博斯站四强

侧身凌空斩
2026-06-12 23:43:21
卸下反舰导弹!22型快艇爆改出山,美军被碾压,中国下了盘大棋

卸下反舰导弹!22型快艇爆改出山,美军被碾压,中国下了盘大棋

李健政观察
2026-06-12 16:07:05
演员陈敏儿去世

演员陈敏儿去世

扬子晚报
2026-06-12 18:49:13
你发现了么,进入2026年,在苏州反向买房的人,越来越多

你发现了么,进入2026年,在苏州反向买房的人,越来越多

林子说事
2026-06-12 18:34:14
立刻停止食用这些粗粮,吃得越多,肠癌风险越高?医生告诉你真相

立刻停止食用这些粗粮,吃得越多,肠癌风险越高?医生告诉你真相

叙说医疗健康
2026-06-12 05:00:08
日本男排把世界第一拉下马!2大主力回归战中国,海宁剔除李咏臻

日本男排把世界第一拉下马!2大主力回归战中国,海宁剔除李咏臻

排球黄金眼
2026-06-12 23:54:31
终于感受到国企降薪有多狠了

终于感受到国企降薪有多狠了

细说职场
2026-06-12 13:50:35
比亚迪一建厂项目暂停!

比亚迪一建厂项目暂停!

电动内参
2026-06-12 18:49:58
紧急叫停?谢娜巡演被大量举报,官方最新回应来了

紧急叫停?谢娜巡演被大量举报,官方最新回应来了

观察鉴娱
2026-06-12 09:51:17
内塔尼亚胡:以色列已准备好抛弃美国独自攻击伊朗

内塔尼亚胡:以色列已准备好抛弃美国独自攻击伊朗

一种观点
2026-06-10 15:53:34
1962年,当36岁梦露当众褪衣亮相那一刻,生命就已经进入了倒计时

1962年,当36岁梦露当众褪衣亮相那一刻,生命就已经进入了倒计时

毒舌小红帽
2026-06-10 18:33:58
疑阿里员工爆料:所在部门50%已离职,N+1到手转行跨境电商

疑阿里员工爆料:所在部门50%已离职,N+1到手转行跨境电商

六子吃凉粉
2026-06-12 11:13:32
马斯克发出终极警报:5年内世界将巨变,人类优势或面对终极危机

马斯克发出终极警报:5年内世界将巨变,人类优势或面对终极危机

流史岁月
2026-06-11 19:10:06
温瑞博打疯了!一日三胜!爆冷淘汰世界第二!林诗栋携手黄友政晋级男双八强!

温瑞博打疯了!一日三胜!爆冷淘汰世界第二!林诗栋携手黄友政晋级男双八强!

好乒乓
2026-06-12 17:35:49
汪小菲公开家庭矛盾:无法与孩子沟通!儿子索要钱,女儿要求房屋

汪小菲公开家庭矛盾:无法与孩子沟通!儿子索要钱,女儿要求房屋

青芳草
2024-07-10 10:05:49
某驻外机构,仅5个月就给10万印度人发签证,遭网暴!

某驻外机构,仅5个月就给10万印度人发签证,遭网暴!

奇思妙想生活家
2026-06-12 15:00:36
金价下跌了,2026年6月12日人民币与国内黄金的最新报价

金价下跌了,2026年6月12日人民币与国内黄金的最新报价

说故事的阿袭
2026-06-12 14:31:46
杰伦·布伦森关键时刻能力强,OG·阿努诺比助尼克斯几乎要夺冠

杰伦·布伦森关键时刻能力强,OG·阿努诺比助尼克斯几乎要夺冠

好火子
2026-06-13 05:45:53
我75岁,存款300多万,血的教训告诫我:再亲的亲人也要留个心眼

我75岁,存款300多万,血的教训告诫我:再亲的亲人也要留个心眼

千秋文化
2026-06-12 20:21:36
又一价格“屠夫”来了!奥迪A6L给出16.49万优惠,你心动吗?

又一价格“屠夫”来了!奥迪A6L给出16.49万优惠,你心动吗?

汽车网评
2026-06-11 22:21:19
2026-06-13 06:08:49
Java精选
Java精选
一场永远也演不完的戏
1794文章数 3859关注度
往期回顾 全部

科技要闻

刚刚,人类历史上首位万亿美元富豪诞生!

头条要闻

SpaceX上市首日收涨19% 总市值报2.1万亿美元

头条要闻

SpaceX上市首日收涨19% 总市值报2.1万亿美元

体育要闻

欧洲恐韩?肉德维德?

娱乐要闻

一天4个瓜,肖战热巴最意外

财经要闻

万亿美元顺差背后,透露这些信号

汽车要闻

标配激光雷达/双动力可选 昊铂S600限时售17.99万起

态度原创

手机
家居
数码
艺术
公开课

手机要闻

vivo X Fold6再预热:天玑9500超能版+OriginOS 6 Fold

家居要闻

空间微调 移形换境

数码要闻

英国监管机构警告:亚马逊、eBay仍在售可能致命的假冒手机充电器

艺术要闻

砸了640亿,再赔160亿!沙特“The Line”项目彻底凉了?

公开课

李玫瑾:为什么性格比能力更重要?

无障碍浏览 进入关怀版