余瑜的博客 余瑜的博客
首页
  • 并发
  • 线程池
  • spring
  • maven
  • 其他
  • redis
  • mysql
  • linux
  • zookeeper
  • docker
  • terminal
  • kong插件开发
  • 资料
  • leetCode-简单
  • blog
  • 其他
关于
GitHub (opens new window)
首页
  • 并发
  • 线程池
  • spring
  • maven
  • 其他
  • redis
  • mysql
  • linux
  • zookeeper
  • docker
  • terminal
  • kong插件开发
  • 资料
  • leetCode-简单
  • blog
  • 其他
关于
GitHub (opens new window)
  • 并发

  • 线程池

  • spring

    • Spring 统一资源加载策略
    • BeanDefinition加载、解析、处理、注册
    • BeanFactory1-DefaultSingletonBeanRegistry
    • spring-boot-data-elasticsearch
    • springboot配置Druid监控
    • springboot项目初始化时读取数据库
    • 文件上传
    • SpringBoot优雅停机的正确姿势.md
      • spring源码阅读神器
    • maven

    • 其他

    • JAVA
    • spring
    余瑜
    2021-08-31
    目录

    SpringBoot优雅停机的正确姿势.md

    目录

    • 何为优雅关机

    • kill 指令

    • Runtime.addShutdownHook

    • Spring 3.2.12

    • SpringBoot

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

    #

    何为优雅关机

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

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

    • Socket 链接,比如:Netty、MQ

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

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

    • 各种堆内堆外内存释放

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

    kill 指令

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

    #查看jvm进程pid
    jps
    #列出所有信号名称
    kill -l
     
    # 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
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32

    思考:JVM 是如何接受处理 Linux 信号量的?

    当然是在 JVM 启动时就加载了自定义 SignalHandler,关闭 JVM 时触发对应的 handle。

    public interface SignalHandler {
        SignalHandler SIG_DFL = new NativeSignalHandler(0L);
        SignalHandler SIG_IGN = new NativeSignalHandler(1L);
     
        void handle(Signal var1);
    }
    class Terminator {
        private static SignalHandler handler = null;
     
        Terminator() {
        }
        //jvm设置SignalHandler,在System.initializeSystemClass中触发
        static void setup() {
            if (handler == null) {
                SignalHandler var0 = new SignalHandler() {
                    public void handle(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) {
                    ;
                }
     
            }
        }
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36

    Runtime.addShutdownHook

    在了解 Shutdown.exit 之前,先看:

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

    则是为 JVM 中增加一个关闭的钩子,当 JVM 关闭的时候调用。

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

    Spring 3.2.12

    在 Spring 中通过 ContextClosedEvent 事件来触发一些动作(可以拓展),主要通过 LifecycleProcessor.onClose 来做 stopBeans。

    由此可见 Spring 也基于 JVM 做了拓展。

    public abstract class AbstractApplicationContext extends DefaultResourceLoader {
     public void registerShutdownHook() {
      if (this.shutdownHook == null) {
       // No shutdown hook registered yet.
       this.shutdownHook = new Thread() {
        @Override
        public void run() {
         doClose();
        }
       };
       Runtime.getRuntime().addShutdownHook(this.shutdownHook);
      }
     }
     protected void doClose() {
      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;
       }
      }
     } 
    }
    public interface LifecycleProcessor extends Lifecycle {
     /**
      * Notification of context refresh, e.g. for auto-starting components.
      */
     void onRefresh();
     
     /**
      * Notification of context close phase, e.g. for auto-stopping components.
      */
     void onClose();
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69

    SpringBoot

    到这里就进入重点了,SpringBoot 中有 spring-boot-starter-actuator 模块提供了一个 restful (opens new window) 接口,用于优雅停机。

    执行请求 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
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18

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

    @ConfigurationProperties(
        prefix = "endpoints.shutdown"
    )
    public class ShutdownMvcEndpoint extends EndpointMvcAdapter {
        public ShutdownMvcEndpoint(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"
    )
    public class ShutdownEndpoint extends AbstractEndpoint<Map<String, Object>> implements ApplicationContextAware {
        private static final Map<String, Object> NO_CONTEXT_MESSAGE = Collections.unmodifiableMap(Collections.singletonMap("message", "No context to shutdown."));
        private static final Map<String, Object> SHUTDOWN_MESSAGE = Collections.unmodifiableMap(Collections.singletonMap("message", "Shutting down, bye..."));
        private ConfigurableApplicationContext context;
     
        public ShutdownEndpoint() {
            super("shutdown", true, false);
        }
        //执行关闭
        public Map<String, Object> invoke() {
            if (this.context == null) {
                return NO_CONTEXT_MESSAGE;
            } else {
                boolean var6 = false;
     
                Map var1;
     
                class NamelessClass_1 implements Runnable {
                    NamelessClass_1() {
                    }
     
                    public void run() {
                        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;
            }
        }
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70

    ------------- END -------------

    ← 文件上传 spring源码阅读神器→

    Theme by Vdoing | Copyright © 2018-2022 逆光世间 | 备案号: 京ICP备19016086号
    • 跟随系统
    • 浅色模式
    • 深色模式
    • 阅读模式