Spring的ShutDownHook和dubbo的优雅关闭

目录

spring的优雅关闭

dubbo的优雅关闭

前言

优雅停机原理

在 SpringBoot 应用中存在的问题


欢迎跳转到本文的原文链接:JAVA虚拟机关闭钩子(Shutdown Hook)_朱小厮的博客-CSDN博客_shutdownhook

spring的优雅关闭

Java程序经常也会遇到进程挂掉的情况,一些状态没有正确的保存下来,这时候就需要在JVM关掉的时候执行一些清理现场的代码。JAVA中的ShutdownHook提供了比较好的方案。

JDK提供了Java.Runtime.addShutdownHook(Thread hook)方法,可以注册一个JVM关闭的钩子,这个钩子可以在一下几种场景中被调用:

    程序正常退出
    使用System.exit()
    终端使用Ctrl+C触发的中断
    系统关闭
    OutOfMemory宕机
    使用Kill pid命令干掉进程(注:在使用kill -9 pid时,是不会被调用的)

下面是JDK1.7中关于钩子的定义:

    public void addShutdownHook(Thread hook)
参数:
    hook - An initialized but unstarted Thread object
抛出:
    IllegalArgumentException - If the specified hook has already been registered, or if it can be determined that the hook is already running or has already been run
    IllegalStateException - If the virtual machine is already in the process of shutting down
    SecurityException - If a security manager is present and it denies RuntimePermission("shutdownHooks")
从以下版本开始:
    1.3
另请参见:
    removeShutdownHook(java.lang.Thread), halt(int), exit(int)

首先来测试第一种,程序正常退出的情况:

    package com.hook;
     
    import java.util.concurrent.TimeUnit;
     
    public class HookTest
    {
    	public void start()
    	{
    		Runtime.getRuntime().addShutdownHook(new Thread(new Runnable() {
    			@Override
    			public void run()
    			{
    				System.out.println("Execute Hook.....");
    			}
    		}));
    	}
    	
    	public static void main(String[] args)
    	{
    		new HookTest().start();
    		System.out.println("The Application is doing something");
    		
    		try
    		{
    			TimeUnit.MILLISECONDS.sleep(5000);
    		}
    		catch (InterruptedException e)
    		{
    			e.printStackTrace();
    		}
    	}
    }

运行结果:

The Application is doing something
Execute Hook.....

如上可以看到,当main线程运行结束之后就会调用关闭钩子。

下面再来测试第五种情况(顺序有点乱,表在意这些细节):

 package com.hook;
     
    import java.util.concurrent.TimeUnit;
     
    public class HookTest2
    {
        public void start()
        {
            Runtime.getRuntime().addShutdownHook(new Thread(new Runnable() {
                @Override
                public void run()
                {
                    System.out.println("Execute Hook.....");
                }
            }));
        }
        
        public static void main(String[] args)
        {
            new HookTest().start();
            System.out.println("The Application is doing something");
            byte[] b = new byte[500*1024*1024];
            try
            {
                TimeUnit.MILLISECONDS.sleep(5000);
            }
            catch (InterruptedException e)
            {
                e.printStackTrace();
            }
        }
     
    }

   

运行参数设置为:-Xmx20M  这样可以保证会有OutOfMemoryError的发生。

运行结果:

The Application is doing something
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
    at com.hook.HookTest2.main(HookTest2.java:22)
Execute Hook.....

可以看到程序遇到内存溢出错误后调用关闭钩子,与第一种情况中,程序等待5000ms运行结束之后推出调用关闭钩子不同。

接下来再来测试第三种情况:

   

package com.hook;
     
    import java.util.concurrent.TimeUnit;
     
    public class HookTest3
    {
        public void start()
        {
            Runtime.getRuntime().addShutdownHook(new Thread(new Runnable() {
                @Override
                public void run()
                {
                    System.out.println("Execute Hook.....");
                }
            }));
        }
        
        public static void main(String[] args)
        {
            new HookTest3().start();
            Thread thread = new Thread(new Runnable(){
     
                @Override
                public void run()
                {
                    while(true)
                    {
                        System.out.println("thread is running....");
                        try
                        {
                            TimeUnit.MILLISECONDS.sleep(100);
                        }
                        catch (InterruptedException e)
                        {
                            e.printStackTrace();
                        }
                    }
                }
                
            });
            thread.start();
        }
     
    }

在命令行中编译:javac com/hook/HookTest3.java

在命令行中运行:java com.hook.HookTest3  (之后按下Ctrl+C)

运行结果:
可以看到效果如预期。
还有几种情况就不一一列出了,有兴趣的读者可以试一下。

 

欢迎跳转到本文的原文链接:JAVA虚拟机关闭钩子(Shutdown Hook)_朱小厮的博客-CSDN博客_shutdownhook

dubbo的优雅关闭

前言

主要是前一阵子换了工作,第一个任务就是解决目前团队在 Dubbo 停机时产生的问题,同时最近又看了一下 Dubbo 的源码,想重新写一下 Dubbo 相关的文章。

优雅停机原理

对于一个 java 应用,如果想在关闭应用时,执行一些释放资源的操作一般是通过注册一个 ShutDownHook ,当关闭应用时,不是调用 kill -9 命令来直接终止应用,而是通过调用 kill -15 命令来触发这个 ShutDownHook 进行停机前的释放资源操作。
对于 Dubbo 来说,需要停机前执行的操作包括两部分:

  1. 对于服务的提供者,需要通知注册中心来把自己在服务列表中摘除掉。
  2. 根据所配置的协议,关闭协议的端口和连接。

而何为优雅停机呢?就是在集群环境下,有一个应用停机,并不会出现异常。下面来看一下 Dubbo 是怎么做的。

注册ShutDownHook

Duubo 在 AbstractConfig 的静态构造函数中注册了 JVM 的 ShutDownHook,而 ShutdownHook 主要是调用 ProtocolConfig.destroyAll() ,源码如下:

    static {
        Runtime.getRuntime().addShutdownHook(new Thread(new Runnable() {
            public void run() {
                if (logger.isInfoEnabled()) {
                    logger.info("Run shutdown hook now.");
                }
                ProtocolConfig.destroyAll();
            }
        }, "DubboShutdownHook"));
    }

ProtocolConfig.destroyAll()

先看一下 ProtocolConfig.destroyAll() 源码:


  public static void destroyAll() {
        if (!destroyed.compareAndSet(false, true)) {
            return;
        }
        AbstractRegistryFactory.destroyAll();  //1.

        // Wait for registry notification
        try {
            Thread.sleep(ConfigUtils.getServerShutdownTimeout()); //2.
        } catch (InterruptedException e) {
            logger.warn("Interrupted unexpectedly when waiting for registry notification during shutdown process!");
        }

        ExtensionLoader<Protocol> loader = ExtensionLoader.getExtensionLoader(Protocol.class);
        for (String protocolName : loader.getLoadedExtensions()) {
            try {
                Protocol protocol = loader.getLoadedExtension(protocolName);
                if (protocol != null) {
                    protocol.destroy(); //3.
                }
            } catch (Throwable t) {
                logger.warn(t.getMessage(), t);
            }
        }
    }

ProtocolConfig.destroyAll() 有三个比较重要的操作:

  1. 在1这个点调用AbstractRegistryFactory.destroyAll(),其内部会对每个注册中心进行 destroy 操作,进而把注册到注册中心的服务取消注册。
  2. 2这个点是最近 Dubbo 版本新增的操作,用来增强 Dubbo 的优雅停机,在老版本的 Dubbo 其逻辑是直接摘除服务列表,关闭暴露的连接,因为服务取消注册,注册中心是异步的通知消费者变更其存放在自己内存中的提供者列表。因为是异步操作,当调用量比较大的应用时消费者会拿到已经关闭连接点的提供者进行调用,这时候就会产生大量的错误,而2这个点就是通过Sleep 来延迟关闭协议暴露的连接。
  3. 因为 Dubbo 的扩展机制 ,loader.getLoadedExtensions() 会获取到已使用的所有协议,遍历调用 destroy 方法来关闭其打开的端口和连接。

而在第3步会在 Exchange 层 对所有打开的连接进行判断其有没有正在执行的请求,如果有会自旋 Sleep 直到设置的 ServerShutdownTimeout 时间或者已经没有正在执行的请求了才会关闭连接,源码如下:

  public void close(final int timeout) {
       startClose();
       if (timeout > 0) {
           final long max = (long) timeout;
           final long start = System.currentTimeMillis();
           if (getUrl().getParameter(Constants.CHANNEL_SEND_READONLYEVENT_KEY, true)) {
               sendChannelReadOnlyEvent();
           }
           while (HeaderExchangeServer.this.isRunning() //判断是否还有正在处理的请求
                   && System.currentTimeMillis() - start < max) { //判断是否超时
               try {
                   Thread.sleep(10);
               } catch (InterruptedException e) {
                   logger.warn(e.getMessage(), e);
               }
           }
       }
       doClose();  
       server.close(timeout); //正在的关闭连接
   }

在 SpringBoot 应用中存在的问题

简单的描述一下问题:就是在应用停机时,瞬间会产生大量的报错,比如拿到的数据库连接已经关闭等问题。 其实一看就知道是在停机时还存在正在处理的请求,而这些请求所需要的资源被 Spring 容器所关闭导致的。原来在SpringBoot 启动时会在 refreshContext 操作也注册一个 ShotdownHook 来关闭Spring容器。

    private void refreshContext(ConfigurableApplicationContext context) {
       this.refresh(context);
       if (this.registerShutdownHook) {
           try {
               context.registerShutdownHook();
           } catch (AccessControlException var3) {
           }
       }
   }

而要解决这个问题就需要取消掉这个 ShutDownHook ,然后再 Dubbo 优雅停机执行后关闭 Spring 容器。具体的修改如下:

  1. 在启动Main方法中,修改SpringBoot 启动代码,取消注册ShutDownHook。
    public static void main(String[] args) {
       SpringApplication app = new SpringApplication(XxxApplication.class);
       app.setRegisterShutdownHook(false);
       app.run(args);
   }
  1. 注册一个Bean 来让 Dubbo 关闭后关闭Spring容器。
public class SpringShutdownHook {
   private static final Logger logger = LoggerFactory.getLogger(SpringShutdownHook.class);
   @Autowired
   private ConfigurableApplicationContext configurableApplicationContext;

   public SpringShutdownHook() {
   }

   @PostConstruct
   public void registerShutdownHook() {
       logger.info("[SpringShutdownHook] Register ShutdownHook....");
       Thread shutdownHook = new Thread() {
           public void run() {
               try {
                   int timeOut = ConfigUtils.getServerShutdownTimeout();
                   logger.info("[SpringShutdownHook] Application need sleep {} seconds to wait Dubbo shutdown", (double)timeOut / 1000.0D);
                   Thread.sleep((long)timeOut);
                   this.configurableApplicationContext.close();
                   logger.info("[SpringShutdownHook] ApplicationContext closed, Application shutdown");
               } catch (InterruptedException var2) {
                   SpringShutdownHook.logger.error(var2.getMessage(), var2);
               }

           }
       };
       Runtime.getRuntime().addShutdownHook(shutdownHook);
   }
}
  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值