Java SpringBoot

何为优雅关机

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

  • 线程池,shutdown(不接受新任务等待处理完)还是shutdownNow(调用 Thread.interrupt进行中断)
  • socket 链接,比如:netty、mq
  • 告知注册中心快速下线(靠心跳机制客服早都跳起来了),比如:eureka
  • 清理临时文件,比如:poi
  • 各种堆内堆外内存释放

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

kill指令

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

  1. #查看jvm进程pid
  2. jps
  3. #列出所有信号名称
  4. kill -l
  5. # Windows下信号常量值
  6. # 简称 全称 数值
  7. # INT SIGINT 2 Ctrl+C中断
  8. # ILL SIGILL 4 非法指令
  9. # FPE SIGFPE 8 floating point exception(浮点异常)
  10. # SEGV SIGSEGV 11 segment violation(段错误)
  11. # TERM SIGTERM 5 Software termination signal from kill(Kill发出的软件终止)
  12. # BREAK SIGBREAK 21 Ctrl-Break sequence(Ctrl+Break中断)
  13. # ABRT SIGABRT 22 abnormal termination triggered by abort call(Abort)
  14. #linux信号常量值
  15. # 简称 全称数值
  16. # HUP SIGHUP 1 终端断线
  17. # INT SIGINT 2 中断(同 Ctrl + C)
  18. # QUIT SIGQUIT 3 退出(同 Ctrl + \)
  19. # KILL SIGKILL 9 强制终止
  20. # TERM SIGTERM 15 终止
  21. # CONT SIGCONT 18 继续(与STOP相反, fg/bg命令)
  22. # STOP SIGSTOP 19 暂停(同 Ctrl + Z)
  23. #....
  24. #可以理解为操作系统从内核级别强行杀死某个进程
  25. kill -9 pid
  26. #理解为发送一个通知,等待应用主动关闭
  27. kill -15 pid
  28. #也支持信号常量值全称或简写(就是去掉SIG后)
  29. kill -l KILL

思考:jvm是如何接受处理linux信号量的?
当然是在jvm启动时就加载了自定义SignalHandler,关闭jvm时触发对应的handle。

  1. public interface SignalHandler {
  2. SignalHandler SIG_DFL = new NativeSignalHandler(0L);
  3. SignalHandler SIG_IGN = new NativeSignalHandler(1L);
  4. void handle(Signal var1);
  5. }
  6. class Terminator {
  7. private static SignalHandler handler = null;
  8. Terminator() {
  9. }
  10. //jvm设置SignalHandler,在System.initializeSystemClass中触发
  11. static void setup() {
  12. if (handler == null) {
  13. SignalHandler var0 = new SignalHandler() {
  14. public void handle(Signal var1) {
  15. Shutdown.exit(var1.getNumber() + 128);//调用Shutdown.exit
  16. }
  17. };
  18. handler = var0;
  19. try {
  20. Signal.handle(new Signal("INT"), var0);//中断时
  21. } catch (IllegalArgumentException var3) {
  22. ;
  23. }
  24. try {
  25. Signal.handle(new Signal("TERM"), var0);//终止时
  26. } catch (IllegalArgumentException var2) {
  27. ;
  28. }
  29. }
  30. }
  31. }

Runtime.addShutdownHook

在了解Shutdown.exit之前,先看
Runtime.getRuntime().addShutdownHook(shutdownHook);则是为jvm中增加一个关闭的钩子,当jvm关闭的时候调用。

  1. public class Runtime {
  2. public void addShutdownHook(Thread hook) {
  3. SecurityManager sm = System.getSecurityManager();
  4. if (sm != null) {
  5. sm.checkPermission(new RuntimePermission("shutdownHooks"));
  6. }
  7. ApplicationShutdownHooks.add(hook);
  8. }
  9. }
  10. class ApplicationShutdownHooks {
  11. /* The set of registered hooks */
  12. private static IdentityHashMap<Thread, Thread> hooks;
  13. static synchronized void add(Thread hook) {
  14. if(hooks == null)
  15. throw new IllegalStateException("Shutdown in progress");
  16. if (hook.isAlive())
  17. throw new IllegalArgumentException("Hook already running");
  18. if (hooks.containsKey(hook))
  19. throw new IllegalArgumentException("Hook previously registered");
  20. hooks.put(hook, hook);
  21. }
  22. }
  23. //它含数据结构和逻辑管理虚拟机关闭序列
  24. class Shutdown {
  25. /* Shutdown 系列状态*/
  26. private static final int RUNNING = 0;
  27. private static final int HOOKS = 1;
  28. private static final int FINALIZERS = 2;
  29. private static int state = RUNNING;
  30. /* 是否应该运行所以finalizers来exit? */
  31. private static boolean runFinalizersOnExit = false;
  32. // 系统关闭钩子注册一个预定义的插槽.
  33. // 关闭钩子的列表如下:
  34. // (0) Console restore hook
  35. // (1) Application hooks
  36. // (2) DeleteOnExit hook
  37. private static final int MAX_SYSTEM_HOOKS = 10;
  38. private static final Runnable[] hooks = new Runnable[MAX_SYSTEM_HOOKS];
  39. // 当前运行关闭钩子的钩子的索引
  40. private static int currentRunningHook = 0;
  41. /* 前面的静态字段由这个锁保护 */
  42. private static class Lock { };
  43. private static Object lock = new Lock();
  44. /* 为native halt方法提供锁对象 */
  45. private static Object haltLock = new Lock();
  46. static void add(int slot, boolean registerShutdownInProgress, Runnable hook) {
  47. synchronized (lock) {
  48. if (hooks[slot] != null)
  49. throw new InternalError("Shutdown hook at slot " + slot + " already registered");
  50. if (!registerShutdownInProgress) {//执行shutdown过程中不添加hook
  51. if (state > RUNNING)//如果已经在执行shutdown操作不能添加hook
  52. throw new IllegalStateException("Shutdown in progress");
  53. } else {//如果hooks已经执行完毕不能再添加hook。如果正在执行hooks时,添加的槽点小于当前执行的槽点位置也不能添加
  54. if (state > HOOKS || (state == HOOKS && slot <= currentRunningHook))
  55. throw new IllegalStateException("Shutdown in progress");
  56. }
  57. hooks[slot] = hook;
  58. }
  59. }
  60. /* 执行所有注册的hooks
  61. */
  62. private static void runHooks() {
  63. for (int i=0; i < MAX_SYSTEM_HOOKS; i++) {
  64. try {
  65. Runnable hook;
  66. synchronized (lock) {
  67. // acquire the lock to make sure the hook registered during
  68. // shutdown is visible here.
  69. currentRunningHook = i;
  70. hook = hooks[i];
  71. }
  72. if (hook != null) hook.run();
  73. } catch(Throwable t) {
  74. if (t instanceof ThreadDeath) {
  75. ThreadDeath td = (ThreadDeath)t;
  76. throw td;
  77. }
  78. }
  79. }
  80. }
  81. /* 关闭JVM的操作
  82. */
  83. static void halt(int status) {
  84. synchronized (haltLock) {
  85. halt0(status);
  86. }
  87. }
  88. //JNI方法
  89. static native void halt0(int status);
  90. // shutdown的执行顺序:runHooks > runFinalizersOnExit
  91. private static void sequence() {
  92. synchronized (lock) {
  93. /* Guard against the possibility of a daemon thread invoking exit
  94. * after DestroyJavaVM initiates the shutdown sequence
  95. */
  96. if (state != HOOKS) return;
  97. }
  98. runHooks();
  99. boolean rfoe;
  100. synchronized (lock) {
  101. state = FINALIZERS;
  102. rfoe = runFinalizersOnExit;
  103. }
  104. if (rfoe) runAllFinalizers();
  105. }
  106. //Runtime.exit时执行,runHooks > runFinalizersOnExit > halt
  107. static void exit(int status) {
  108. boolean runMoreFinalizers = false;
  109. synchronized (lock) {
  110. if (status != 0) runFinalizersOnExit = false;
  111. switch (state) {
  112. case RUNNING: /* Initiate shutdown */
  113. state = HOOKS;
  114. break;
  115. case HOOKS: /* Stall and halt */
  116. break;
  117. case FINALIZERS:
  118. if (status != 0) {
  119. /* Halt immediately on nonzero status */
  120. halt(status);
  121. } else {
  122. /* Compatibility with old behavior:
  123. * Run more finalizers and then halt
  124. */
  125. runMoreFinalizers = runFinalizersOnExit;
  126. }
  127. break;
  128. }
  129. }
  130. if (runMoreFinalizers) {
  131. runAllFinalizers();
  132. halt(status);
  133. }
  134. synchronized (Shutdown.class) {
  135. /* Synchronize on the class object, causing any other thread
  136. * that attempts to initiate shutdown to stall indefinitely
  137. */
  138. sequence();
  139. halt(status);
  140. }
  141. }
  142. //shutdown操作,与exit不同的是不做halt操作(关闭JVM)
  143. static void shutdown() {
  144. synchronized (lock) {
  145. switch (state) {
  146. case RUNNING: /* Initiate shutdown */
  147. state = HOOKS;
  148. break;
  149. case HOOKS: /* Stall and then return */
  150. case FINALIZERS:
  151. break;
  152. }
  153. }
  154. synchronized (Shutdown.class) {
  155. sequence();
  156. }
  157. }
  158. }

Spring 3.2.12

在Spring中通过ContextClosedEvent事件来触发一些动作(可以拓展),主要通过LifecycleProcessor.onClose来做stopBeans。由此可见Spring也基于jvm做了拓展。

  1. public abstract class AbstractApplicationContext extends DefaultResourceLoader {
  2. public void registerShutdownHook() {
  3. if (this.shutdownHook == null) {
  4. // No shutdown hook registered yet.
  5. this.shutdownHook = new Thread() {
  6. @Override
  7. public void run() {
  8. doClose();
  9. }
  10. };
  11. Runtime.getRuntime().addShutdownHook(this.shutdownHook);
  12. }
  13. }
  14. protected void doClose() {
  15. boolean actuallyClose;
  16. synchronized (this.activeMonitor) {
  17. actuallyClose = this.active && !this.closed;
  18. this.closed = true;
  19. }
  20. if (actuallyClose) {
  21. if (logger.isInfoEnabled()) {
  22. logger.info("Closing " + this);
  23. }
  24. LiveBeansView.unregisterApplicationContext(this);
  25. try {
  26. //发布应用内的关闭事件
  27. publishEvent(new ContextClosedEvent(this));
  28. }
  29. catch (Throwable ex) {
  30. logger.warn("Exception thrown from ApplicationListener handling ContextClosedEvent", ex);
  31. }
  32. // 停止所有的Lifecycle beans.
  33. try {
  34. getLifecycleProcessor().onClose();
  35. }
  36. catch (Throwable ex) {
  37. logger.warn("Exception thrown from LifecycleProcessor on context close", ex);
  38. }
  39. // 销毁spring 的 BeanFactory可能会缓存单例的 Bean.
  40. destroyBeans();
  41. // 关闭当前应用上下文(BeanFactory)
  42. closeBeanFactory();
  43. // 执行子类的关闭逻辑
  44. onClose();
  45. synchronized (this.activeMonitor) {
  46. this.active = false;
  47. }
  48. }
  49. }
  50. }
  51. public interface LifecycleProcessor extends Lifecycle {
  52. /**
  53. * Notification of context refresh, e.g. for auto-starting components.
  54. */
  55. void onRefresh();
  56. /**
  57. * Notification of context close phase, e.g. for auto-stopping components.
  58. */
  59. void onClose();
  60. }

SpringBoot

到这里就进入重点了,SpringBoot中有spring-boot-starter-actuator 模块提供了一个 restful 接口,用于优雅停机。执行请求 curl -X POST http://127.0.0.1:8088/shutdown ,待关闭成功则返回提示。
注:线上环境该url需要设置权限,可配合 spring-security使用或在nginx中限制内网访问

  1. #启用shutdown
  2. endpoints.shutdown.enabled=true
  3. #禁用密码验证
  4. endpoints.shutdown.sensitive=false
  5. #可统一指定所有endpoints的路径
  6. management.context-path=/manage
  7. #指定管理端口和IP
  8. management.port=8088
  9. management.address=127.0.0.1
  10. #开启shutdown的安全验证(spring-security)
  11. endpoints.shutdown.sensitive=true
  12. #验证用户名
  13. security.user.name=admin
  14. #验证密码
  15. security.user.password=secret
  16. #角色
  17. management.security.role=SUPERUSER

SpringBoot的shutdown原理也不复杂,其实还是通过调用AbstractApplicationContext.close实现的。

  1. @ConfigurationProperties(
  2. prefix = "endpoints.shutdown"
  3. )
  4. public class ShutdownMvcEndpoint extends EndpointMvcAdapter {
  5. public ShutdownMvcEndpoint(ShutdownEndpoint delegate) {
  6. super(delegate);
  7. }
  8. //post请求
  9. @PostMapping(
  10. produces = {"application/vnd.spring-boot.actuator.v1+json", "application/json"}
  11. )
  12. @ResponseBody
  13. public Object invoke() {
  14. return !this.getDelegate().isEnabled() ? new ResponseEntity(Collections.singletonMap("message", "This endpoint is disabled"), HttpStatus.NOT_FOUND) : super.invoke();
  15. }
  16. }
  17. @ConfigurationProperties(
  18. prefix = "endpoints.shutdown"
  19. )
  20. public class ShutdownEndpoint extends AbstractEndpoint<Map<String, Object>> implements ApplicationContextAware {
  21. private static final Map<String, Object> NO_CONTEXT_MESSAGE = Collections.unmodifiableMap(Collections.singletonMap("message", "No context to shutdown."));
  22. private static final Map<String, Object> SHUTDOWN_MESSAGE = Collections.unmodifiableMap(Collections.singletonMap("message", "Shutting down, bye..."));
  23. private ConfigurableApplicationContext context;
  24. public ShutdownEndpoint() {
  25. super("shutdown", true, false);
  26. }
  27. //执行关闭
  28. public Map<String, Object> invoke() {
  29. if (this.context == null) {
  30. return NO_CONTEXT_MESSAGE;
  31. } else {
  32. boolean var6 = false;
  33. Map var1;
  34. class NamelessClass_1 implements Runnable {
  35. NamelessClass_1() {
  36. }
  37. public void run() {
  38. try {
  39. Thread.sleep(500L);
  40. } catch (InterruptedException var2) {
  41. Thread.currentThread().interrupt();
  42. }
  43. //这个调用的就是AbstractApplicationContext.close
  44. ShutdownEndpoint.this.context.close();
  45. }
  46. }
  47. try {
  48. var6 = true;
  49. var1 = SHUTDOWN_MESSAGE;
  50. var6 = false;
  51. } finally {
  52. if (var6) {
  53. Thread thread = new Thread(new NamelessClass_1());
  54. thread.setContextClassLoader(this.getClass().getClassLoader());
  55. thread.start();
  56. }
  57. }
  58. Thread thread = new Thread(new NamelessClass_1());
  59. thread.setContextClassLoader(this.getClass().getClassLoader());
  60. thread.start();
  61. return var1;
  62. }
  63. }
  64. }