你们的优雅停机真的优雅吗? | 京东云技术团队
1.前言
emm,又又遇到问题啦,现有业务系统应用上线存在窗口期,不能满足正常任务迭代上线。在非窗口期上线容易导致数据库、mq、jsf等线程中断,进而导致需要手动修单问题。故而通过添加优雅停机功能进行优化,令其在上线前选择优雅停机后,会优先断掉新流量的涌入,并预留一定时间处理现存连接,最后完全下线,可有效扩大上线预留窗口时间并降低上线期间线程中断,进而降低手动修单。可是什么是优雅停机呢?为什么现有的系统技术没有原生的优雅停机机制呢?通过调研整理文章如下。
2.何为优雅停机?
线程池
,shutdown(不接受新任务等待处理完)还是shutdownNow(调用Thread.interrupt进行中断)。 jmq、fmq
。(需要着重处理) jsf
。(需要着重处理) 总之,进程强行终止会带来数据丢失或者终端无法恢复到正常状态,在分布式环境下可能导致数据不一致的情况。
3.导致优雅停机不优雅的元凶之-kill命令
SIGTERM
信号通知进程终止,由进程自行决定
怎么做,即进程不一定终止。一般不直接使用kill -15,不一定能够终止进程。 脏数据
、系统中存在残留文件等情况。如果要使用kill -9,尽量先使用kill -15给进程一个处理善后的机会。该命令可以模拟一次系统宕机,系统断电等极端情况。
终止正在执行的代码
->保存数据
->终止进程
,只是在进程终止之前会保存相关数据,依然会出现事务执行、业务处理中断的情况,做不到优雅停机。 4.引申问题:jvm如何接受处理linux信号量的?
SingalHandler
,关闭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) { } } } }
Shutdown.exit
之前,先看Runtime.getRuntime().addShutdownHook(shutdownHook)
;则是为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(); } } }
5.Spring 中是如何实现优雅停机的?
spring
中通过ContexClosedEvent
事件来触发一些动作,主要通过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(); }
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限制内网访问
。
#启用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
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; } } }
7.知识拓展之Tomcat和Spring的关系?
通过参与云工厂优雅停机重构发现Tomcat
和Spring
均存在问题,故而查询探究两者之间。
Tomcat
和jettey
是HTTP服务器和Servlet容器,负责给类似Spring这种servlet提供一个运行的环境,其中:Http服务器与Servlet容器的功能界限是:可以把HTTP服务器想象成前台
的接待,负责网络通信和解析请求,Servlet容器是业务
部门,负责处理业务请求。 8.实战演练
mq
(jmq、fmq
)通过添加hook
在停机时调用pause
先停止该应用的消费,防止出现上线期间mq
中线程池的线程中断
的情况发生。 /** * @ClassName ShutDownHook * @Description * @Date 2022/10/28 17:47 **/ @Component @Slf4j public class ShutDownHook { @Value("${shutdown.waitTime:10}") private int waitTime; @Resource com.jdjr.fmq.client.consumer.MessageConsumer fmqMessageConsumer; @Resource com.jd.jmq.client.consumer.MessageConsumer jmqMessageConsumer; @PreDestroy public void destroyHook() { try { log.info("ShutDownHook destroy"); jmqMessageConsumer.pause(); fmqMessageConsumer.pause(); int i = 0; while (i < waitTime) { try { Thread.sleep(1000); log.info("距离服务关停还有{}秒", waitTime - i++); } catch (Throwable e) { log.error("异常", e); } } } catch (Throwable e) { log.error("异常", e); } } }
jsf
生产者下线,并预留一定时间消费完毕,行云部署有相关stop.sh脚本,项目中通过在shutdown中编写方法实现。 jsf启停分析
:见京东内部cf文档;
@Component @Lazy(value = false) public class ShutDown implements ApplicationContextAware { private static Logger logger = LoggerFactory.getLogger(ShutDown.class); @Value("${shutdown.waitTime:60}") private int waitTime; @Resource com.jdjr.fmq.client.consumer.MessageConsumer fmqMessageConsumer; @PostConstruct public void init() { logger.info("ShutDownHook init"); } private ApplicationContext applicationContext = null; @PreDestroy public void destroyHook() { try { logger.info("ShutDownHook destroy"); destroyJsfProvider(); fmqMessageConsumer.pause(); int i = 0; while (i < waitTime) { try { Thread.sleep(1000); logger.info("距离服务关停还有{}秒", waitTime - i++); } catch (Throwable e) { logger.error("异常", e); } } } catch (Throwable e) { logger.error("异常", e); } } private void destroyJsfProvider() { logger.info("关闭所有JSF生产者"); if (null != applicationContext) { String[] providerBeanNames = applicationContext.getBeanNamesForType(ProviderBean.class); for (String name : providerBeanNames) { try { logger.info("尝试关闭JSF生产者" + name); ProviderBean bean=(ProviderBean)applicationContext.getBean(name); bean.destroy(); logger.info("关闭JSF生产者" + name + "成功"); } catch (BeanCreationNotAllowedException re){ logger.error("JSF生产者" + name + "未初始化,忽略"); } catch (Exception e) { logger.error("关闭JSF生产者失败", e); } } } logger.info("所有JSF生产者已关闭"); } @Override public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { this.applicationContext = applicationContext; ((AbstractApplicationContext)applicationContext).registerShutdownHook(); } }
absfactory-base-custcenter
应用优雅停机出现日志无法打印问题,排查定位发现问题如下:通过本地debug发现优雅停机先销毁logback
日志打印线程,导致实际倒计时的日志无法打印。 <!-- fix-程序关停时,logback先销毁的问题--> <context-param> <param-name>logbackDisableServletContainerInitializer</param-name> <param-value>true</param-value> </context-param>
9.总结
现有的springboot内置Tomcat能通过配置参数达到优雅停机的效果。但是因为业务系统中的代码中存在多种技术交叉应用,针对Tomcat和springmvc不同的应用确实需要花费时间研究底层原理来编写相关类实现同springboot配置参数托管的效果。
作者:京东科技 宋慧超
来源:京东云开发者社区
低调大师中文资讯倾力打造互联网数据资讯、行业资源、电子商务、移动互联网、网络营销平台。
持续更新报道IT业界、互联网、市场资讯、驱动更新,是最及时权威的产业资讯及硬件资讯报道平台。
转载内容版权归作者及来源网站所有,本站原创内容转载请注明来源。
- 上一篇
JDK 17 营销初体验 —— 亚毫秒停顿 ZGC 落地实践 | 京东云技术团队
前言 自 2014 年发布以来, JDK 8 一直都是相当热门的 JDK 版本。其原因就是对底层数据结构、JVM 性能以及开发体验做了重大升级,得到了开发人员的认可。但距离 JDK 8 发布已经过去了 9 年,那么这 9 年的时间,JDK 做了哪些升级?是否有新的重大特性值得我们尝试?能否解决一些我们现在苦恼的问题?带着这份疑问,我们进行了 JDK 版本的调研与尝试。 新特性一览 现如今的 JDK 发布节奏变快,每次新出一个版本,我们就会感叹一下:我还在用 JDK 8,现在都 JDK 9、10、11 …… 21 了?然后就会瞅瞅又多了哪些新特性。有一些新特性很香,但考虑一番还是决定放弃升级。主要原因除了新增特性对我们来说改变不大以外,最重要的就是 JDK 9 带来的模块化(JEP 200),导致我们升级十分困难。 模块化的本意是将 JDK 划分为一组模块,这些模块可以在编译时、构建时和运行时组合成各种配置,主要目标是使实现更容易扩展到小型设备,提高安全性和可维护性,并提高应用程序性能。但付出的代价非常大,最直观的影响就是,一些 JDK 内部类不能访问了。 但是除此之外,并没有太多阻塞升...
- 下一篇
Log4j疯狂写日志问题排查 | 京东云技术团队
一、问题是怎么发现的 最近有个Java系统上线后不久就收到了磁盘使用率告警,磁盘使用率已经超过了90%以上,并且磁盘使用率还在不停增长。 二、问题带来的影响 由于服务器磁盘被打满,导致了系统正常的业务日志无法继续打印,严重影响了系统的可靠性。 三、排查问题的详细过程 刚开始收到磁盘告警的时候,怀疑是日志级别问题,业务日志输出过多导致磁盘打满。但是查看我们自己的业务日志文件目录,每个日志文件内容都不是很大。 于是通过堡垒机登陆问题服务器,查看磁盘使用率很高的目录列表,发现根目录有个很大的日志文件,日志文件名称为log4j.log。但是检查应用日志配置后,日志输出配置路径并没有配置这个日志路径。而且我们用的是logback日志组件和配置文件,并没有使用log4j来输出日志。于是便打开这个未知来源的日志文件内容,记录的日志内容确实是我们自己的java系统写入的日志内容,且大部分都是debug级别日志内容。于是猜测在系统依赖的jar包内也有一个log4j的日志配置文件。于是便把部署包下载下来,然后通过文档遍历扫描所有jar包内的日志配置文件,结果在一个第三方jar包内找到一个log4j.xml...
相关文章
文章评论
共有0条评论来说两句吧...