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。
1public interface SignalHandler { 2 SignalHandler SIG_DFL = new NativeSignalHandler(0L); 3 SignalHandler SIG_IGN = new NativeSignalHandler(1L); 4 5 void handle(Signal var1); 6} 7class Terminator { 8 private static SignalHandler handler = null; 9 10 Terminator() { 11 } 12 //jvm设置SignalHandler,在System.initializeSystemClass中触发 13 static void setup() { 14 if (handler == null) { 15 SignalHandler var0 = new SignalHandler() { 16 public void handle(Signal var1) { 17 Shutdown.exit(var1.getNumber() + 128);//调用Shutdown.exit 18 } 19 }; 20 handler = var0; 21 22 try { 23 Signal.handle(new Signal("INT"), var0);//中断时 24 } catch (IllegalArgumentException var3) { 25 26 } 27 28 try { 29 Signal.handle(new Signal("TERM"), var0);//终止时 30 } catch (IllegalArgumentException var2) { 31 32 } 33 34 } 35 } 36} 37 38 39
• Runtime.addShutdownHook。在了解Shutdown.exit之前,先看Runtime.getRuntime().addShutdownHook(shutdownHook);则是为jvm中增加一个关闭的钩子,当jvm关闭的时候调用。
1public 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} 10class 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 17 if (hook.isAlive()) 18 throw new IllegalArgumentException("Hook already running"); 19 20 if (hooks.containsKey(hook)) 21 throw new IllegalArgumentException("Hook previously registered"); 22 23 hooks.put(hook, hook); 24 } 25} 26//它含数据结构和逻辑管理虚拟机关闭序列 27class Shutdown { 28 /* Shutdown 系列状态*/ 29 private static final int RUNNING = 0; 30 private static final int HOOKS = 1; 31 private static final int FINALIZERS = 2; 32 private static int state = RUNNING; 33 /* 是否应该运行所以finalizers来exit? */ 34 private static boolean runFinalizersOnExit = false; 35 // 系统关闭钩子注册一个预定义的插槽. 36 // 关闭钩子的列表如下: 37 // (0) Console restore hook 38 // (1) Application hooks 39 // (2) DeleteOnExit hook 40 private static final int MAX_SYSTEM_HOOKS = 10; 41 private static final Runnable[] hooks = new Runnable[MAX_SYSTEM_HOOKS]; 42 // 当前运行关闭钩子的钩子的索引 43 private static int currentRunningHook = 0; 44 /* 前面的静态字段由这个锁保护 */ 45 private static class Lock { }; 46 private static Object lock = new Lock(); 47 48 /* 为native halt方法提供锁对象 */ 49 private static Object haltLock = new Lock(); 50 51 static void add(int slot, boolean registerShutdownInProgress, Runnable hook) { 52 synchronized (lock) { 53 if (hooks[slot] != null) 54 throw new InternalError("Shutdown hook at slot " + slot + " already registered"); 55 56 if (!registerShutdownInProgress) {//执行shutdown过程中不添加hook 57 if (state > RUNNING)//如果已经在执行shutdown操作不能添加hook 58 throw new IllegalStateException("Shutdown in progress"); 59 } else {//如果hooks已经执行完毕不能再添加hook。如果正在执行hooks时,添加的槽点小于当前执行的槽点位置也不能添加 60 if (state > HOOKS || (state == HOOKS && slot <= currentRunningHook)) 61 throw new IllegalStateException("Shutdown in progress"); 62 } 63 64 hooks[slot] = hook; 65 } 66 } 67 /* 执行所有注册的hooks 68 */ 69 private static void runHooks() { 70 for (int i=0; i < MAX_SYSTEM_HOOKS; i++) { 71 try { 72 Runnable hook; 73 synchronized (lock) { 74 // acquire the lock to make sure the hook registered during 75 // shutdown is visible here. 76 currentRunningHook = i; 77 hook = hooks[i]; 78 } 79 if (hook != null) hook.run(); 80 } catch(Throwable t) { 81 if (t instanceof ThreadDeath) { 82 ThreadDeath td = (ThreadDeath)t; 83 throw td; 84 } 85 } 86 } 87 } 88 /* 关闭JVM的操作 89 */ 90 static void halt(int status) { 91 synchronized (haltLock) { 92 halt0(status); 93 } 94 } 95 //JNI方法 96 static native void halt0(int status); 97 // shutdown的执行顺序:runHooks > runFinalizersOnExit 98 private static void sequence() { 99 synchronized (lock) { 100 /* Guard against the possibility of a daemon thread invoking exit 101 * after DestroyJavaVM initiates the shutdown sequence 102 */ 103 if (state != HOOKS) return; 104 } 105 runHooks(); 106 boolean rfoe; 107 synchronized (lock) { 108 state = FINALIZERS; 109 rfoe = runFinalizersOnExit; 110 } 111 if (rfoe) runAllFinalizers(); 112 } 113 //Runtime.exit时执行,runHooks > runFinalizersOnExit > halt 114 static void exit(int status) { 115 boolean runMoreFinalizers = false; 116 synchronized (lock) { 117 if (status != 0) runFinalizersOnExit = false; 118 switch (state) { 119 case RUNNING: /* Initiate shutdown */ 120 state = HOOKS; 121 break; 122 case HOOKS: /* Stall and halt */ 123 break; 124 case FINALIZERS: 125 if (status != 0) { 126 /* Halt immediately on nonzero status */ 127 halt(status); 128 } else { 129 /* Compatibility with old behavior: 130 * Run more finalizers and then halt 131 */ 132 runMoreFinalizers = runFinalizersOnExit; 133 } 134 break; 135 } 136 } 137 if (runMoreFinalizers) { 138 runAllFinalizers(); 139 halt(status); 140 } 141 synchronized (Shutdown.class) { 142 /* Synchronize on the class object, causing any other thread 143 * that attempts to initiate shutdown to stall indefinitely 144 */ 145 sequence(); 146 halt(status); 147 } 148 } 149 //shutdown操作,与exit不同的是不做halt操作(关闭JVM) 150 static void shutdown() { 151 synchronized (lock) { 152 switch (state) { 153 case RUNNING: /* Initiate shutdown */ 154 state = HOOKS; 155 break; 156 case HOOKS: /* Stall and then return */ 157 case FINALIZERS: 158 break; 159 } 160 } 161 synchronized (Shutdown.class) { 162 sequence(); 163 } 164 } 165} 166 167 168
5.Spring 中是如何实现优雅停机的?
• 以Spring3.2.12在spring中通过ContexClosedEvent事件来触发一些动作,主要通过LifecycleProcessor.onClose来做stopBeans。由此可见spring也基于jvm做了扩展。
1public 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 21 if (actuallyClose) { 22 if (logger.isInfoEnabled()) { 23 logger.info("Closing " + this); 24 } 25 26 LiveBeansView.unregisterApplicationContext(this); 27 28 try { 29 //发布应用内的关闭事件 30 publishEvent(new ContextClosedEvent(this)); 31 } 32 catch (Throwable ex) { 33 logger.warn("Exception thrown from ApplicationListener handling ContextClosedEvent", ex); 34 } 35 36 // 停止所有的Lifecycle beans. 37 try { 38 getLifecycleProcessor().onClose(); 39 } 40 catch (Throwable ex) { 41 logger.warn("Exception thrown from LifecycleProcessor on context close", ex); 42 } 43 44 // 销毁spring 的 BeanFactory可能会缓存单例的 Bean. 45 destroyBeans(); 46 47 // 关闭当前应用上下文(BeanFactory) 48 closeBeanFactory(); 49 50 // 执行子类的关闭逻辑 51 onClose(); 52 53 synchronized (this.activeMonitor) { 54 this.active = false; 55 } 56 } 57 } 58} 59public interface LifecycleProcessor extends Lifecycle { 60 /** 61 * Notification of context refresh, e.g. for auto-starting components. 62 */ 63 void onRefresh(); 64 65 /** 66 * Notification of context close phase, e.g. for auto-stopping components. 67 */ 68 void onClose(); 69} 70 71 72
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 2endpoints.shutdown.enabled=true 3#禁用密码验证 4endpoints.shutdown.sensitive=false 5#可统一指定所有endpoints的路径 6management.context-path=/manage 7#指定管理端口和IP 8management.port=8088 9management.address=127.0.0.1 10 11#开启shutdown的安全验证(spring-security) 12endpoints.shutdown.sensitive=true 13#验证用户名 14security.user.name=admin 15#验证密码 16security.user.password=secret 17#角色 18management.security.role=SUPERUSER 19 20 21
• springboot的shutdown通过调用AbstractApplicationContext.close实现的。
1@ConfigurationProperties( 2 prefix = "endpoints.shutdown" 3) 4public 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) 20public 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 25 public ShutdownEndpoint() { 26 super("shutdown", true, false); 27 } 28 //执行关闭 29 public Map<String, Object> invoke() { 30 if (this.context == null) { 31 return NO_CONTEXT_MESSAGE; 32 } else { 33 boolean var6 = false; 34 35 Map var1; 36 37 class NamelessClass_1 implements Runnable { 38 NamelessClass_1() { 39 } 40 41 public void run() { 42 try { 43 Thread.sleep(500L); 44 } catch (InterruptedException var2) { 45 Thread.currentThread().interrupt(); 46 } 47 //这个调用的就是AbstractApplicationContext.close 48 ShutdownEndpoint.this.context.close(); 49 } 50 } 51 52 try { 53 var6 = true; 54 var1 = SHUTDOWN_MESSAGE; 55 var6 = false; 56 } finally { 57 if (var6) { 58 Thread thread = new Thread(new NamelessClass_1()); 59 thread.setContextClassLoader(this.getClass().getClassLoader()); 60 thread.start(); 61 } 62 } 63 64 Thread thread = new Thread(new NamelessClass_1()); 65 thread.setContextClassLoader(this.getClass().getClassLoader()); 66 thread.start(); 67 return var1; 68 } 69 } 70} 71 72 73
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/** 3 * @ClassName ShutDownHook 4 * @Description 5 * @Date 2022/10/28 17:47 6 **/ 7@Component 8@Slf4j 9public class ShutDownHook { 10 11 @Value("${shutdown.waitTime:10}") 12 private int waitTime; 13 14 @Resource 15 com.jdjr.fmq.client.consumer.MessageConsumer fmqMessageConsumer; 16 17 @Resource 18 com.jd.jmq.client.consumer.MessageConsumer jmqMessageConsumer; 19 20 21 @PreDestroy 22 public void destroyHook() { 23 try { 24 log.info("ShutDownHook destroy"); 25 26 jmqMessageConsumer.pause(); 27 fmqMessageConsumer.pause(); 28 29 int i = 0; 30 while (i < waitTime) { 31 try { 32 Thread.sleep(1000); 33 log.info("距离服务关停还有{}秒", waitTime - i++); 34 } catch (Throwable e) { 35 log.error("异常", e); 36 } 37 } 38 39 } catch (Throwable e) { 40 log.error("异常", e); 41 } 42 43 } 44} 45 46 47
• 在优雅停机时需要先把jsf生产者下线,并预留一定时间消费完毕,行云部署有相关stop.sh脚本,项目中通过在shutdown中编写方法实现。
jsf启停分析:见京东内部cf文档;
1@Component 2@Lazy(value = false) 3public class ShutDown implements ApplicationContextAware { 4 private static Logger logger = LoggerFactory.getLogger(ShutDown.class); 5 6 @Value("${shutdown.waitTime:60}") 7 private int waitTime; 8 9 @Resource 10 com.jdjr.fmq.client.consumer.MessageConsumer fmqMessageConsumer; 11 12 @PostConstruct 13 public void init() { 14 logger.info("ShutDownHook init"); 15 } 16 17 private ApplicationContext applicationContext = null; 18 19 @PreDestroy 20 public void destroyHook() { 21 try { 22 logger.info("ShutDownHook destroy"); 23 destroyJsfProvider(); 24 fmqMessageConsumer.pause(); 25 26 int i = 0; 27 while (i < waitTime) { 28 try { 29 Thread.sleep(1000); 30 logger.info("距离服务关停还有{}秒", waitTime - i++); 31 } catch (Throwable e) { 32 logger.error("异常", e); 33 } 34 } 35 36 } catch (Throwable e) { 37 logger.error("异常", e); 38 } 39 40 } 41 private void destroyJsfProvider() { 42 logger.info("关闭所有JSF生产者"); 43 if (null != applicationContext) { 44 String[] providerBeanNames = applicationContext.getBeanNamesForType(ProviderBean.class); 45 for (String name : providerBeanNames) { 46 try { 47 logger.info("尝试关闭JSF生产者" + name); 48 ProviderBean bean=(ProviderBean)applicationContext.getBean(name); 49 bean.destroy(); 50 logger.info("关闭JSF生产者" + name + "成功"); 51 } catch (BeanCreationNotAllowedException re){ 52 logger.error("JSF生产者" + name + "未初始化,忽略"); 53 } catch (Exception e) { 54 logger.error("关闭JSF生产者失败", e); 55 } 56 } 57 } 58 logger.info("所有JSF生产者已关闭"); 59 } 60 61 @Override 62 public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { 63 this.applicationContext = applicationContext; 64 ((AbstractApplicationContext)applicationContext).registerShutdownHook(); 65 } 66 67} 68 69 70 71
• absfactory-base-custcenter应用优雅停机出现日志无法打印问题,排查定位发现问题如下:通过本地debug发现优雅停机先销毁logback日志打印线程,导致实际倒计时的日志无法打印。
1 <!-- fix-程序关停时,logback先销毁的问题--> 2 <context-param> 3 <param-name>logbackDisableServletContainerInitializer</param-name> 4 <param-value>true</param-value> 5 </context-param> 6 7 8
9.总结
现有的springboot内置Tomcat能通过配置参数达到优雅停机的效果。但是因为业务系统中的代码中存在多种技术交叉应用,针对Tomcat和springmvc不同的应用确实需要花费时间研究底层原理来编写相关类实现同springboot配置参数托管的效果。
作者:京东科技 宋慧超
来源:京东云开发者社区
