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。
publicinterfaceSignalHandler{
SignalHandlerSIG_DFL=newNativeSignalHandler(0L);
SignalHandlerSIG_IGN=newNativeSignalHandler(1L);
voidhandle(Signalvar1);
}
classTerminator{
privatestaticSignalHandlerhandler=null;
Terminator(){
}
//jvm设置SignalHandler,在System.initializeSystemClass中触发
staticvoidsetup(){
if(handler==null){
SignalHandlervar0=newSignalHandler(){
publicvoidhandle(Signalvar1){
Shutdown.exit(var1.getNumber()+128);//调用Shutdown.exit
}
};
handler=var0;
try{
Signal.handle(newSignal("INT"),var0);//停止时
}catch(IllegalArgumentExceptionvar3){
}
try{
Signal.handle(newSignal("TERM"),var0);//停止时
}catch(IllegalArgumentExceptionvar2){
}
}
}
}
•Runtime.addShutdownHook。在相识Shutdown.exit之前,先看Runtime.getRuntime().addShutdownHook(shutdownHook);则是为jvm中增长一个关闭的钩子,当jvm关闭的时间调用。
publicclassRuntime{
publicvoidaddShutdownHook(Threadhook){
SecurityManagersm=System.getSecurityManager();
if(sm!=null){
sm.checkPermission(newRuntimePermission("shutdownHooks"));
}
ApplicationShutdownHooks.add(hook);
}
}
classApplicationShutdownHooks{
/*Thesetofregisteredhooks*/
privatestaticIdentityHashMapThread,Threadhooks;
staticsynchronizedvoidadd(Threadhook){
if(hooks==null)
thrownewIllegalStateException("Shutdowninprogress");
if(hook.isAlive())
thrownewIllegalArgumentException("Hookalreadyrunning");
if(hooks.containsKey(hook))
thrownewIllegalArgumentException("Hookpreviouslyregistered");
hooks.put(hook,hook);
}
}
//它含数据布局和逻辑管理假造构造闭序列
classShutdown{
/*Shutdown系列状态*/
privatestaticfinalintRUNNING=0;
privatestaticfinalintHOOKS=1;
privatestaticfinalintFINALIZERS=2;
privatestaticintstate=RUNNING;
/*是否应该运行以是finalizers来exit?*/
privatestaticbooleanrunFinalizersOnExit=false;
//体系关闭钩子注册一个预界说的插槽.
//关闭钩子的列表如下:
//(0)Consolerestorehook
//(1)Applicationhooks
//(2)DeleteOnExithook
privatestaticfinalintMAX_SYSTEM_HOOKS=10;
privatestaticfinalRunnable[]hooks=newRunnable[MAX_SYSTEM_HOOKS];
//当前运行关闭钩子的钩子的索引
privatestaticintcurrentRunningHook=0;
/*前面的静态字段由这个锁掩护*/
privatestaticclassLock{};
privatestaticObjectlock=newLock();
/*为nativehalt方法提供锁对象*/
privatestaticObjecthaltLock=newLock();
staticvoidadd(intslot,booleanregisterShutdownInProgress,Runnablehook){
synchronized(lock){
if(hooks[slot]!=null)
thrownewInternalError("Shutdownhookatslot"+slot+"alreadyregistered");
if(!registerShutdownInProgress){//实行shutdown过程中不添加hook
if(stateRUNNING)//假如已经在实行shutdown操纵不能添加hook
thrownewIllegalStateException("Shutdowninprogress");
}else{//假如hooks已经实行完毕不能再添加hook。假如正在实行hooks时,添加的槽点小于当前实行的槽点位置也不能添加
if(stateHOOKS||(state==HOOKSslot=currentRunningHook))
thrownewIllegalStateException("Shutdowninprogress");
}
hooks[slot]=hook;
}
}
/*实行全部注册的hooks
*/
privatestaticvoidrunHooks(){
for(inti=0;iMAX_SYSTEM_HOOKS;i++){
try{
Runnablehook;
synchronized(lock){
//acquirethelocktomakesurethehookregisteredduring
//shutdownisvisiblehere.
currentRunningHook=i;
hook=hooks[i];
}
if(hook!=null)hook.run();
}catch(Throwablet){
if(tinstanceofThreadDeath){
ThreadDeathtd=(ThreadDeath)t;
throwtd;
}
}
}
}
/*关闭JVM的操纵
*/
staticvoidhalt(intstatus){
synchronized(haltLock){
halt0(status);
}
}
//JNI方法
staticnativevoidhalt0(intstatus);
//shutdown的实行次序:runHooksrunFinalizersOnExit
privatestaticvoidsequence(){
synchronized(lock){
/*Guardagainstthepossibilityofadaemonthreadinvokingexit
*afterDestroyJavaVMinitiatestheshutdownsequence
*/
if(state!=HOOKS)return;
}
runHooks();
booleanrfoe;
synchronized(lock){
state=FINALIZERS;
rfoe=runFinalizersOnExit;
}
if(rfoe)runAllFinalizers();
}
//Runtime.exit时实行,runHooksrunFinalizersOnExithalt
staticvoidexit(intstatus){
booleanrunMoreFinalizers=false;
synchronized(lock){
if(status!=0)runFinalizersOnExit=false;
switch(state){
caseRUNNING:/*Initiateshutdown*/
state=HOOKS;
break;
caseHOOKS:/*Stallandhalt*/
break;
caseFINALIZERS:
if(status!=0){
/*Haltimmediatelyonnonzerostatus*/
halt(status);
}else{
/*Compatibilitywitholdbehavior:
*Runmorefinalizersandthenhalt
*/
runMoreFinalizers=runFinalizersOnExit;
}
break;
}
}
if(runMoreFinalizers){
runAllFinalizers();
halt(status);
}
synchronized(Shutdown.class){
/*Synchronizeontheclassobject,causinganyotherthread
*thatattemptstoinitiateshutdowntostallindefinitely
*/
sequence();
halt(status);
}
}
//shutdown操纵,与exit差别的是不做halt操纵(关闭JVM)
staticvoidshutdown(){
synchronized(lock){
switch(state){
caseRUNNING:/*Initiateshutdown*/
state=HOOKS;
break;
caseHOOKS:/*Stallandthenreturn*/
caseFINALIZERS:
break;
}
}
synchronized(Shutdown.class){
sequence();
}
}
}
5.Spring中是怎样实现优雅停机的?
•以Spring3.2.12在spring中通过ContexClosedEvent变乱来触发一些动作,重要通过LifecycleProcessor.onClose来做stopBeans。由此可见spring也基于jvm做了扩展。
publicabstractclassAbstractApplicationContextextendsDefaultResourceLoader{
publicvoidregisterShutdownHook(){
if(this.shutdownHook==null){
//Noshutdownhookregisteredyet.
this.shutdownHook=newThread(){
@Override
publicvoidrun(){
doClose();
}
};
Runtime.getRuntime().addShutdownHook(this.shutdownHook);
}
}
protectedvoiddoClose(){
booleanactuallyClose;
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(newContextClosedEvent(this));
}
catch(Throwableex){
logger.warn("ExceptionthrownfromApplicationListenerhandlingContextClosedEvent",ex);
}
//克制全部的Lifecyclebeans.
try{
getLifecycleProcessor().onClose();
}
catch(Throwableex){
logger.warn("ExceptionthrownfromLifecycleProcessoroncontextclose",ex);
}
//烧毁spring的BeanFactory大概会缓存单例的Bean.
destroyBeans();
//关闭当前应用上下文(BeanFactory)
closeBeanFactory();
//实行子类的关闭逻辑
onClose();
synchronized(this.activeMonitor){
this.active=false;
}
}
}
}
publicinterfaceLifecycleProcessorextendsLifecycle{
/**
*Notificationofcontextrefresh,e.g.forauto-startingcomponents.
*/
voidonRefresh();
/**
*Notificationofcontextclosephase,e.g.forauto-stoppingcomponents.
*/
voidonClose();
}
6.SpringBoot是怎样做到优雅停机的?
•优雅停机是springboot的特性之一,在收到停止信号后,不再担当、处理惩罚新哀求,但会在停止进程之前预留一小段缓冲时间,已完成正在处理惩罚的哀求。注:优雅停机必要在tomcat的9.0.33及其之后的版本才支持。
•springboot中有spring-boot-starter-actuator模块提供了一个restful接口,用于优雅停机。实行哀求curl-XPOSThttp://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"
publicclassShutdownMvcEndpointextendsEndpointMvcAdapter{
publicShutdownMvcEndpoint(ShutdownEndpointdelegate){
super(delegate);
}
//post哀求
@PostMapping(
produces={"application/vnd.spring-boot.actuator.v1+json","application/json"}
@ResponseBody
publicObjectinvoke(){
return!this.getDelegate().isEnabled()?newResponseEntity(Collections.singletonMap("message","Thisendpointisdisabled"),HttpStatus.NOT_FOUND):super.invoke();
}
}
@ConfigurationProperties(
prefix="endpoints.shutdown"
publicclassShutdownEndpointextendsAbstractEndpointMapString,ObjectimplementsApplicationContextAware{
privatestaticfinalMapString,ObjectNO_CONTEXT_MESSAGE=Collections.unmodifiableMap(Collections.singletonMap("message","Nocontexttoshutdown."));
privatestaticfinalMapString,ObjectSHUTDOWN_MESSAGE=Collections.unmodifiableMap(Collections.singletonMap("message","Shuttingdown,bye..."));
privateConfigurableApplicationContextcontext;
publicShutdownEndpoint(){
super("shutdown",true,false);
}
//实行关闭
publicMapString,Objectinvoke(){
if(this.context==null){
returnNO_CONTEXT_MESSAGE;
}else{
booleanvar6=false;
Mapvar1;
classNamelessClass_1implementsRunnable{
NamelessClass_1(){
}
publicvoidrun(){
try{
Thread.sleep(500L);
}catch(InterruptedExceptionvar2){
Thread.currentThread().interrupt();
}
//这个调用的就是AbstractApplicationContext.close
ShutdownEndpoint.this.context.close();
}
}
try{
var6=true;
var1=SHUTDOWN_MESSAGE;
var6=false;
}finally{
if(var6){
Threadthread=newThread(newNamelessClass_1());
thread.setContextClassLoader(this.getClass().getClassLoader());
thread.start();
}
}
Threadthread=newThread(newNamelessClass_1());
thread.setContextClassLoader(this.getClass().getClassLoader());
thread.start();
returnvar1;
}
}
}
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等,这些由全局的SpringIOC容器管理,因此,Spring有两个IOC容器。
•假如只是利用spring(不包罗springmvc),那么是tomcat容器分析xml文件,通过反射实例化对应的类,根据这些servlet规范实现类,触发对应的代码处理惩罚逻辑,这个时间tomcat负责http报文的分析和servlet调治的工作。
•假如利用springmvc,那么tomcat只是分析http报文,然后将其转发给dispatchsetvlet,然后由springmvc根据其设置,实例对应的类,实行对应的逻辑,然后返回结果给dispatchservlet,末了由它转发给tomcat,由tomcat负责构建http报文数据。
8.实战演练
•mq(jmq、fmq)通过添加hook在停机时调用pause先克制该应用的斲丧,防止出现上线期间mq中线程池的线程停止的环境发生。
/**
*@ClassNameShutDownHook
*@Description
*@Date2022/10/2817:47
**/
@Component
@Slf4j
publicclassShutDownHook{
@Value("${shutdown.waitTime:10}")
privateintwaitTime;
@Resource
com.jdjr.fmq.client.consumer.MessageConsumerfmqMessageConsumer;
@Resource
com.jd.jmq.client.consumer.MessageConsumerjmqMessageConsumer;
@PreDestroy
publicvoiddestroyHook(){
try{
log.info("ShutDownHookdestroy");
jmqMessageConsumer.pause();
fmqMessageConsumer.pause();
inti=0;
while(iwaitTime){
try{
Thread.sleep(1000);
log.info("间隔服务关停尚有{}秒",waitTime-i++);
}catch(Throwablee){
log.error("非常",e);
}
}
}catch(Throwablee){
log.error("非常",e);
}
}
}
•在优雅停机时必要先把jsf生产者下线,并预留肯定时间斲丧完毕,行云摆设有相干stop.sh脚本,项目中通过在shutdown中编写方法实现。
jsf启停分析:见京东内部cf文档;
@Component
@Lazy(value=false)
publicclassShutDownimplementsApplicationContextAware{
privatestaticLoggerlogger=LoggerFactory.getLogger(ShutDown.class);
@Value("${shutdown.waitTime:60}")
privateintwaitTime;
@Resource
com.jdjr.fmq.client.consumer.MessageConsumerfmqMessageConsumer;
@PostConstruct
publicvoidinit(){
logger.info("ShutDownHookinit");
}
privateApplicationContextapplicationContext=null;
@PreDestroy
publicvoiddestroyHook(){
try{
logger.info("ShutDownHookdestroy");
destroyJsfProvider();
fmqMessageConsumer.pause();
inti=0;
while(iwaitTime){
try{
Thread.sleep(1000);
logger.info("间隔服务关停尚有{}秒",waitTime-i++);
}catch(Throwablee){
logger.error("非常",e);
}
}
}catch(Throwablee){
logger.error("非常",e);
}
}
privatevoiddestroyJsfProvider(){
logger.info("关闭全部JSF生产者");
if(null!=applicationContext){
String[]providerBeanNames=applicationContext.getBeanNamesForType(ProviderBean.class);
for(Stringname:providerBeanNames){
try{
logger.info("实行关闭JSF生产者"+name);
ProviderBeanbean=(ProviderBean)applicationContext.getBean(name);
bean.destroy();
logger.info("关闭JSF生产者"+name+"乐成");
}catch(BeanCreationNotAllowedExceptionre){
logger.error("JSF生产者"+name+"未初始化,忽略");
}catch(Exceptione){
logger.error("关闭JSF生产者失败",e);
}
}
}
logger.info("全部JSF生产者已关闭");
}
@Override
publicvoidsetApplicationContext(ApplicationContextapplicationContext)throwsBeansException{
this.applicationContext=applicationContext;
((AbstractApplicationContext)applicationContext).registerShutdownHook();
}
}
•absfactory-base-custcenter应用优雅停机出现日记无法打印题目,排查定位发现题目如下:通过本地debug发现优雅停机先烧毁logback日记打印线程,导致实际倒计时的日记无法打印。
!--fix-程序关停时,logback先烧毁的题目--
context-param
param-namelogbackDisableServletContainerInitializer/param-name
param-valuetrue/param-value
/context-param
9.总结
现有的springboot内置Tomcat能通过设置参数到达优雅停机的结果。但是由于业务体系中的代码中存在多种技能交错应用,针对Tomcat和springmvc差别的应用确实必要耗费时间研究底层原理来编写相干类实现同springboot设置参数托管的结果。
作者:京东科技宋慧超
泉源:京东云开辟者社区
我要评论