ToB企服应用市场:ToB评测及商务社交产业平台

标题: 你们的优雅停机真的优雅吗? [打印本页]

作者: 一给    时间: 2023-8-14 12:19
标题: 你们的优雅停机真的优雅吗?
1.前言

emm,又又遇到问题啦,现有业务系统应用上线存在窗口期,不能满足正常任务迭代上线。在非窗口期上线容易导致数据库、mq、jsf等线程中断,进而导致需要手动修单问题。故而通过添加优雅停机功能进行优化,令其在上线前选择优雅停机后,会优先断掉新流量的涌入,并预留一定时间处理现存连接,最后完全下线,可有效扩大上线预留窗口时间并降低上线期间线程中断,进而降低手动修单。可是什么是优雅停机呢?为什么现有的系统技术没有原生的优雅停机机制呢?通过调研整理文章如下。

2.何为优雅停机?

• 优雅停机是指为确保应用关闭时,通知应用进程释放所占用的资源。
• 线程池,shutdown(不接受新任务等待处理完)还是shutdownNow(调用Thread.interrupt进行中断)。
• socket链接,比如:netty、jmq、fmq。(需要着重处理)
• 告知注册中心快速下线,比如jsf。(需要着重处理)
• 清理临时文件。
• 各种堆内堆外内存释放。
总之,进程强行终止会带来数据丢失或者终端无法恢复到正常状态,在分布式环境下可能导致数据不一致的情况。
3.导致优雅停机不优雅的元凶之-kill命令

kill指令
kill -15 :kill指令默认就是-15,知识发送一个SIGTERM信号通知进程终止,由进程自行决定怎么做,即进程不一定终止。一般不直接使用kill -15,不一定能够终止进程。
kill -9:强制终止进程,进程会被立刻终止。kill -9 过于暴力,往往会出现事务执行、业务处理中断的情况,导致数据库中存在脏数据、系统中存在残留文件等情况。如果要使用kill -9,尽量先使用kill -15给进程一个处理善后的机会。该命令可以模拟一次系统宕机,系统断电等极端情况。
kill -2:类似Ctrl + C退出,会先保存相关数据再终止进程。kill -2立刻终止正在执行的代码->保存数据->终止进程,只是在进程终止之前会保存相关数据,依然会出现事务执行、业务处理中断的情况,做不到优雅停机。
4.引申问题:jvm如何接受处理linux信号量的?

• 在jvm启动时就加载了自定义SingalHandler,关闭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. }
复制代码
5.Spring 中是如何实现优雅停机的?

• 以Spring3.2.12在spring中通过ContexClosedEvent事件来触发一些动作,主要通过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. }
复制代码
6.SpringBoot是如何做到优雅停机的?

• 优雅停机是springboot的特性之一,在收到终止信号后,不再接受、处理新请求,但会在终止进程之前预留一小段缓冲时间,已完成正在处理的请求。注:优雅停机需要在tomcat的9.0.33及其之后的版本才支持。
• 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. }
复制代码
7.知识拓展之Tomcat和Spring的关系?

通过参与云工厂优雅停机重构发现Tomcat和Spring均存在问题,故而查询探究两者之间。
• Tomcat和jettey是HTTP服务器Servlet容器,负责给类似Spring这种servlet提供一个运行的环境,其中:Http服务器与Servlet容器的功能界限是:可以把HTTP服务器想象成前台的接待,负责网络通信和解析请求,Servlet容器是业务部门,负责处理业务请求
• Tomcat和Servlet作为Web服务器Servlet容器的结合,可以接受网络http请求解析为Servlet规范的请求对象和响应对象。比如,HttpServletRequest对象是Tomcat提供的,Servlet是规范,Tomcat是实现规范的Servlet容器,SpringMVC是处理Servlet请求的应用,其中DispatcherServlet实现了Servlet接口,Tomcat负责加载和调用DispatcherServlet。同时,DispatcherServlet有自己的容器(SpringMVC)容器,这个容器负责管理SpringMVC相关的bean,比如Controler和ViewResolver等。同时,Spring中还有其他的Bean比如Service和DAO等,这些由全局的Spring IOC容器管理,因此,Spring有两个IOC容器。
• 如果只是使用spring(不包含springmvc),那么是tomcat容器解析xml文件,通过反射实例化对应的类,根据这些servlet规范实现类,触发对应的代码处理逻辑,这个时候tomcat负责http报文的解析和servlet调度的工作。
• 如果使用spring mvc,那么tomcat只是解析http报文,然后将其转发给dispatchsetvlet,然后由springmvc根据其配置,实例对应的类,执行对应的逻辑,然后返回结果给dispatchservlet,最后由它转发给tomcat,由tomcat负责构建http报文数据。
8.实战演练

• mq(jmq、fmq)通过添加hook在停机时调用pause先停止该应用的消费,防止出现上线期间mq中线程池的线程中断的情况发生。
  1. /**
  2. * @ClassName ShutDownHook
  3. * @Description
  4. * @Date 2022/10/28 17:47
  5. **/
  6. @Component
  7. @Slf4j
  8. public class ShutDownHook {
  9.     @Value("${shutdown.waitTime:10}")
  10.     private int waitTime;
  11.     @Resource
  12.     com.jdjr.fmq.client.consumer.MessageConsumer fmqMessageConsumer;
  13.     @Resource
  14.     com.jd.jmq.client.consumer.MessageConsumer jmqMessageConsumer;
  15.     @PreDestroy
  16.     public void destroyHook() {
  17.         try {
  18.             log.info("ShutDownHook destroy");
  19.             jmqMessageConsumer.pause();
  20.             fmqMessageConsumer.pause();
  21.             int i = 0;
  22.             while (i < waitTime) {
  23.                 try {
  24.                     Thread.sleep(1000);
  25.                     log.info("距离服务关停还有{}秒", waitTime - i++);
  26.                 } catch (Throwable e) {
  27.                     log.error("异常", e);
  28.                 }
  29.             }
  30.         } catch (Throwable e) {
  31.             log.error("异常", e);
  32.         }
  33.     }
  34. }
复制代码
• 在优雅停机时需要先把jsf生产者下线,并预留一定时间消费完毕,行云部署有相关stop.sh脚本,项目中通过在shutdown中编写方法实现。
jsf启停分析:见京东内部cf文档;
  1. @Component
  2. @Lazy(value = false)
  3. public class ShutDown implements ApplicationContextAware {
  4.     private static Logger logger = LoggerFactory.getLogger(ShutDown.class);
  5.     @Value("${shutdown.waitTime:60}")
  6.     private int waitTime;
  7.     @Resource
  8.     com.jdjr.fmq.client.consumer.MessageConsumer fmqMessageConsumer;
  9.     @PostConstruct
  10.     public void init() {
  11.         logger.info("ShutDownHook init");
  12.     }
  13.     private ApplicationContext applicationContext = null;
  14.     @PreDestroy
  15.     public void destroyHook() {
  16.         try {
  17.             logger.info("ShutDownHook destroy");
  18.             destroyJsfProvider();
  19.             fmqMessageConsumer.pause();
  20.             int i = 0;
  21.             while (i < waitTime) {
  22.                 try {
  23.                     Thread.sleep(1000);
  24.                     logger.info("距离服务关停还有{}秒", waitTime - i++);
  25.                 } catch (Throwable e) {
  26.                     logger.error("异常", e);
  27.                 }
  28.             }
  29.         } catch (Throwable e) {
  30.             logger.error("异常", e);
  31.         }
  32.     }
  33.     private void destroyJsfProvider() {
  34.         logger.info("关闭所有JSF生产者");
  35.         if (null != applicationContext) {
  36.             String[] providerBeanNames = applicationContext.getBeanNamesForType(ProviderBean.class);
  37.             for (String name : providerBeanNames) {
  38.                 try {
  39.                     logger.info("尝试关闭JSF生产者" + name);
  40.                     ProviderBean bean=(ProviderBean)applicationContext.getBean(name);
  41.                     bean.destroy();
  42.                     logger.info("关闭JSF生产者" + name + "成功");
  43.                 } catch (BeanCreationNotAllowedException re){
  44.                     logger.error("JSF生产者" + name + "未初始化,忽略");
  45.                 } catch (Exception e) {
  46.                     logger.error("关闭JSF生产者失败", e);
  47.                 }
  48.             }
  49.         }
  50.         logger.info("所有JSF生产者已关闭");
  51.     }
  52.     @Override
  53.     public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
  54.         this.applicationContext = applicationContext;
  55.         ((AbstractApplicationContext)applicationContext).registerShutdownHook();
  56.     }
  57. }
复制代码
• absfactory-base-custcenter应用优雅停机出现日志无法打印问题,排查定位发现问题如下:通过本地debug发现优雅停机先销毁logback日志打印线程,导致实际倒计时的日志无法打印。
  1.    
  2.     <context-param>
  3.         <param-name>logbackDisableServletContainerInitializer</param-name>
  4.         <param-value>true</param-value>
  5.     </context-param>
复制代码
9.总结

现有的springboot内置Tomcat能通过配置参数达到优雅停机的效果。但是因为业务系统中的代码中存在多种技术交叉应用,针对Tomcat和springmvc不同的应用确实需要花费时间研究底层原理来编写相关类实现同springboot配置参数托管的效果。
作者:京东科技 宋慧超
来源:京东云开发者社区

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!




欢迎光临 ToB企服应用市场:ToB评测及商务社交产业平台 (https://dis.qidao123.com/) Powered by Discuz! X3.4