Dubbo优雅关机实现与ShutdownHook介绍

查看dubbo文档,Dubbo是通过JDK的ShutdownHook来完成优雅停机的:

但能实现优雅停机的前提是,在启动时,需要指定参数-Ddubbo.shutdown.hook=true:

com.alibaba.dubbo.container.Main.main源码:

public class Main {

    public static final String CONTAINER_KEY = "dubbo.container";

    public static final String SHUTDOWN_HOOK_KEY = "dubbo.shutdown.hook";
    
    private static final Logger logger = LoggerFactory.getLogger(Main.class);

    private static final ExtensionLoader<Container> loader = ExtensionLoader.getExtensionLoader(Container.class);
    
    private static volatile boolean running = true;   // 提供线程安全,保持可见

    public static void main(String[] args) {
        try {
            if (args == null || args.length == 0) {
                String config = ConfigUtils.getProperty(CONTAINER_KEY, loader.getDefaultExtensionName());
                args = Constants.COMMA_SPLIT_PATTERN.split(config);
            }
            
            final List<Container> containers = new ArrayList<Container>();
            for (int i = 0; i < args.length; i ++) {
                containers.add(loader.getExtension(args[i]));
            }
            logger.info("Use container type(" + Arrays.toString(args) + ") to run dubbo serivce.");
            
            if ("true".equals(System.getProperty(SHUTDOWN_HOOK_KEY))) {
	            Runtime.getRuntime().addShutdownHook(new Thread() {
	                public void run() {
	                    for (Container container : containers) {
	                        try {
	                            container.stop();
	                            logger.info("Dubbo " + container.getClass().getSimpleName() + " stopped!");
	                        } catch (Throwable t) {
	                            logger.error(t.getMessage(), t);
	                        }
	                        synchronized (Main.class) {
	                            running = false;
	                            Main.class.notify(); //通知一个正在等待的对象
	                        }
	                    }
	                }
	            });
            }
            
            for (Container container : containers) {
                container.start();
                logger.info("Dubbo " + container.getClass().getSimpleName() + " started!");
            }
            System.out.println(new SimpleDateFormat("[yyyy-MM-dd HH:mm:ss]").format(new Date()) + " Dubbo service server started!");
        } catch (RuntimeException e) {
            e.printStackTrace();
            logger.error(e.getMessage(), e);
            System.exit(1);
        }
        synchronized (Main.class) {
            while (running) {
                try {
                    Main.class.wait();
                } catch (Throwable e) {
                }
            }
        }
    }
    
}volatile boolean running = true;   // 提供线程安全,保持可见

    public static void main(String[] args) {
        try {
            if (args == null || args.length == 0) {
                String config = ConfigUtils.getProperty(CONTAINER_KEY, loader.getDefaultExtensionName());
                args = Constants.COMMA_SPLIT_PATTERN.split(config);
            }
            
            final List<Container> containers = new ArrayList<Container>();
            for (int i = 0; i < args.length; i ++) {
                containers.add(loader.getExtension(args[i]));
            }
            logger.info("Use container type(" + Arrays.toString(args) + ") to run dubbo serivce.");
            
            if ("true".equals(System.getProperty(SHUTDOWN_HOOK_KEY))) {
	            Runtime.getRuntime().addShutdownHook(new Thread() {
	                public void run() {
	                    for (Container container : containers) {
	                        try {
	                            container.stop();
	                            logger.info("Dubbo " + container.getClass().getSimpleName() + " stopped!");
	                        } catch (Throwable t) {
	                            logger.error(t.getMessage(), t);
	                        }
	                        synchronized (Main.class) {
	                            running = false;
	                            Main.class.notify(); //通知一个正在等待的对象
	                        }
	                    }
	                }
	            });
            }
            
            for (Container container : containers) {
                container.start();
                logger.info("Dubbo " + container.getClass().getSimpleName() + " started!");
            }
            System.out.println(new SimpleDateFormat("[yyyy-MM-dd HH:mm:ss]").format(new Date()) + " Dubbo service server started!");
        } catch (RuntimeException e) {
            e.printStackTrace();
            logger.error(e.getMessage(), e);
            System.exit(1);
        }
        synchronized (Main.class) {
            while (running) {
                try {
                    Main.class.wait();
                } catch (Throwable e) {
                }
            }
        }
    }
    
}

shutdownHook是一种特殊的结构,它允许开发人员插入JVM关闭时执行的一段代码。这种情况在我们需要做特殊清理操作的情况下很有用

<!-- more -->

用途

JbossJetty等容器中都可以看到shutdownHook的身影,在Java程序中可以通过添加关闭钩子,实现在程序退出时关闭资源、平滑退出的功能。 例如在服务优雅下线一文中的spring-boot-starter-actuator就会触发shutdownHook...

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

  • Application正常退出,在退出时执行特定的业务逻辑,或者关闭资源等操作。
  • 虚拟机非正常退出,比如用户按下ctrl+c、OOM宕机、操作系统关闭(kill pid)等。在退出时执行必要的挽救措施。
  • 1. 程序正常退出 
    2. 使用System.exit() 
    3. 终端使用Ctrl+C触发的中断 
    4. 系统关闭 
    5. 使用Kill pid命令干掉进程

意义

很多时候,我们会有这样的一些场景,比如说nginx反向代理若干个负载均衡的web容器,又或者微服务架构中存在的若干个服务节点,需要进行无间断的升级发布。 
在重启服务的时候,除非我们去变更nginx的配置,否则重启很可能会导致正在执行的线程突然中断,本来应该要完成的事情只完成了一半,并且调用方出现错误警告。 
如果能有一种简单的方式,能够让进程在退出时能执行完当前正在执行的任务,并且让服务的调用方将新的请求定向到其他负载节点,这将会很有意义。 
自己注册ShutdownHook可以帮助我们实现java进程的平滑退出/优雅关闭。

思路

  1. 在服务启动时注册自己的ShutdownHook
  2. ShutdownHook在被运行时,首先不会接收新的请求,或者告诉调用方重新定向到其他服务功能节点
  3. 等待当前的执行线程运行完毕,如果五秒后仍在运行,则强制退出/或做其他处理

用法

正常退出

public class ShutdownHook {

    public static void main(String[] args) throws InterruptedException {
        Runtime.getRuntime().addShutdownHook(new Thread(() -> {
            try (FileWriter fw = new FileWriter("hook.log")) {
                // 假设记录日志/或者发送消息
                fw.write("完成销毁操作,回收内存! " + (new Date()).toString());
                System.out.println("退出程序...");
            } catch (IOException e) {
                e.printStackTrace();
            }
        }));
        IntStream.range(0, 10).forEach(i -> {
            try {
                System.out.println("正在工作...");
                Thread.sleep(2_000L);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });
    }
}

当我们运行上面的代码时,会看到在完成main方法的执行时JVM调用shutdownHook

正在工作...
...
正在工作...
退出程序...

kill pid方式

注意事项

虽然编写一个shutdownHook很简单,但是需要了解shutdownHook后面的内部部件才能正确使用。因此,在后续中,将探讨shutdownHook设计背后的一些陷阱

应用程序无法保证shutdownHook总是运行的

如JVM由于某些内部错误而崩溃,或(Unix / Linux中的kill -9)或TerminateProcess(Windows)),那么应用程序需要立即终止而不会甚至等待任何清理活动。除了上述之外,还可以通过调用Runime.halt()方法来终止JVM,而阻止shutdownHook运行。

shutdownHook可以在完成前强行停止

虽然shutdownHook开始执行,但是在操作系统关闭的情况下,任然可以在完成之前终止它。在这种情况下,一旦SIGTERM被提供,O/S等待一个进程终止指定的时间。如果进程在该时间限制内没有终止,则O/S通过发出SIGTERM(或Windows中的对等方)强制终止进程。所以有可能这是在shutdownHook中途执行时发生的。

因此,建议谨慎地编写shutdownHook,确保它们快速完成,并且不会造成死锁等情况。另外特别注意的是,不应该执行长时间计算或等待用户I/O操作在钩子。

可以有多个shutdownHook,但其执行顺序无法保证

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);
    }
}

通过源码发现,可以注册多个shutdownHook。但是因为它是存储在IdentityHashMap中的,JVM并不能保证其执行顺序。但是可以同时执行所有的shutdownHook

关闭顺序开始后,无法注册/取消注册shutdownHook

一旦关闭顺序是由JVM发起的,将不在允许添加或删除任何现有的shutdownHook,否则抛出IllegalStateException异常。

关闭顺序开始后,只能由Runtime.halt()停止

关闭顺序开始后,只能通过Runtime.halt()(强制终止JVM),可以停止关闭顺序的执行(外部影响除外,如SIGKILL)。

使用shutdownHook需要安全权限

如果我们使用Java Security Managers,则执行添加/删除shutdownHook的代码需要在运行时具有shutdownHooks权限。否则会导致SecurityException

参考:http://docs.oracle.com/javase/1.5.0/docs/api/java/lang/Runtime.html#addShutdownHook(java.lang.Thread) "官方文档")

也许有人会担心性能问题,shutdown hook会不会占用太多的VM的资源,答案是shutdown hook不会占用VM太多的资源,因为shutdown hook 只是一个已初始化但尚未启动的线程。这意味着它只在程序关闭的时候才会启动,而不是在程序一开始运行时就启动。而在大多数的Java平台中,如果一个线程没有启动(即没有调用线程的start()函数)VM不会分配资源给线程。因此维护一群没有启动的线程不会给VM带来太大的负担.

最后还要注意以下两点:如果VM crash,那么不能保证关闭挂钩(shutdown hooks)能运行.试想一下如果Windows XP突然蓝屏了那么本来计划在关机之前的更新也就无法进行了.
如果调用Runtime.halt()方法来结束程序的话,那么关闭挂钩(shutdown hooks)也不会执行


点关注,不迷路

文章每周持续更新,可以微信搜索「 十分钟学编程 」第一时间阅读和催更,如果这个文章写得还不错,觉得有点东西的话 ~求点赞👍 求关注❤️ 求分享❤️ 
各位的支持和认可,就是我创作的最大动力,我们下篇文章见!

  • 3
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 5
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

蒋老湿

你的鼓励是我创作最大的动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值