<返回更多

Java 如何利用钩子函数实现优雅停服?刨根问底

2020-04-30    
加入收藏

JAVA 的世界里遨游,如果能拥有一双善于发现的眼睛,有很多东西留心去看,外加耐心助力,仔细去品,往往会品出不一样的味道。

通过本次分享,能让你轻松 get 如下几点,绝对收获满满。

a)如何让 Java 程序实现优雅停服?有思想才是硬道理!

 

b)addShutdownHook 的使用场景?会用才是王道!

 

c)addShutdownHook 钩子函数到底是个啥?刨根问底!

1. 如何让 Java 程序实现优雅停服?

无论是自研基础服务框架,还是分析开源项目源码,细心的 Java 开发同学,都会发现 Runtime.getRuntime().addShutdownHook 这么一句代码的身影,这句到底是干什么用的?

接下来就一起细品,看看它香不香?

阿里开源的数据同步神器 Canal 启动时的部分源码:

Java 如何利用钩子函数实现优雅停服?刨根问底

 

Apache 麾下的用于海量日志收集的 Flume 启动时的部分源码:

Java 如何利用钩子函数实现优雅停服?刨根问底

 

仰望了一下开源的项目,不妨从中提炼一下共性(同样的代码遇到多次,势必会品出味道),写段代码跑跑看(站在 flume 源码的肩膀上,起飞)。

import java.util.concurrent.ScheduledThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

/**
 * 体验 Java 优雅停服
 *
 * @author 一猿小讲
 */
public class Application {

    /**
     * 监控服务
     */
    private ScheduledThreadPoolExecutor monitorService;

    public Application() {
        monitorService = new ScheduledThreadPoolExecutor(1);
    }

    /**
     * 启动监控服务,监控一下内存信息
     */
    public void start() {
        System.out.println(String.format("启动监控服务 %s", Thread.currentThread().getId()));
        monitorService.scheduleWithFixedDelay(new Runnable() {
            @Override
            public void run() {
                System.out.println(String.format("最大内存: %dm  已分配内存: %dm  已分配内存中的剩余空间: %dm  最大可用内存: %dm",
                        Runtime.getRuntime().maxMemory() / 1024 / 1024,
                        Runtime.getRuntime().totalMemory() / 1024 / 1024,
                        Runtime.getRuntime().freeMemory() / 1024 / 1024,
                        (Runtime.getRuntime().maxMemory() - Runtime.getRuntime().totalMemory() +
                                Runtime.getRuntime().freeMemory()) / 1024 / 1024));
            }
        }, 2, 2, TimeUnit.SECONDS);
    }

    /**
     * 释放资源(代码来源于 flume 源码)
     * 主要用于关闭线程池(看不懂的同学莫纠结,当做黑盒去对待)
     */
    public void stop() {
        System.out.println(String.format("开始关闭线程池 %s", Thread.currentThread().getId()));
        if (monitorService != null) {
            monitorService.shutdown();
            try {
                monitorService.awaitTermination(10, TimeUnit.SECONDS);
            } catch (InterruptedException e) {
                System.err.println("Interrupted while waiting for monitor service to stop");
            }
            if (!monitorService.isTerminated()) {
                monitorService.shutdownNow();
                try {
                    while (!monitorService.isTerminated()) {
                        monitorService.awaitTermination(10, TimeUnit.SECONDS);
                    }
                } catch (InterruptedException e) {
                    System.err.println("Interrupted while waiting for monitor service to stop");
                }
            }
        }
        System.out.println(String.format("线程池关闭完成 %s", Thread.currentThread().getId()));
    }

    /**
     * 应用入口
     */
    public static void main(String[] args) {
        Application application = new Application();
        // 启动服务(每隔一段时间监控输出一下内存信息)
        application.start();

        // 添加钩子,实现优雅停服(主要验证钩子的作用)
        final Application appReference = application;
        Runtime.getRuntime().addShutdownHook(new Thread("shutdown-hook") {
            @Override
            public void run() {
                System.out.println("接收到退出的讯号,开始打扫战场,释放资源,完成优雅停服");
                appReference.stop();
            }
        });
        System.out.println("服务启动完成");
    }
}

经常读文的我很清楚,耐心读文章中源码的同学应该很少,所以我还是用图给你简单捋一捋。

Java 如何利用钩子函数实现优雅停服?刨根问底

 

标注1:start 方法利用线程池启动一个线程去定时监控内存信息;

标注2:stop 方法用于在退出程序之前,进行关闭线程池进而释放资源。

程序跑起来,效果如下。

Java 如何利用钩子函数实现优雅停服?刨根问底

 

当进行 kill 操作时,程序确实进行了资源释放,效果确实很优雅。

Java 如何利用钩子函数实现优雅停服?刨根问底

 

一切看似那么自然,一切又是那么完美,这是真的吗?杀进程时候如果用 kill -9,这种情况下会发生什么现象呢?

Java 如何利用钩子函数实现优雅停服?刨根问底

 

呜呼!结果不会骗人的,当用 kill -9 的时候,就显得很粗暴了,压根不管什么资源释放,不管三七二十一,就是终止程序。

估计很多同学,都擅长用 kill -9 进行杀进程,为了线上的应用安全,还是用 kill -15 命令杀进程吧,这样会给应用留点时间去打扫一下战场,释放一下资源。

好了,通过仔细品味,借助 JDK 自带的 addShutdownHook 来助力应用,确实能让线上服务跑起来很优雅。

有思想才是硬道理!

2. addShutdownHook 的使用场景?

通过代码试验,能够感知 addShutdownHook(new Thread(){}) 是 JVM 销毁前要执行的一个线程,那么只要是涉及到资源回收的场景,应该都可以满足,下面简单列举几个。

a)数据同步神器 Canal 借助它,来进行关闭 socket 链接、释放 canal 的工作节点、清理缓存信息等;

 

b)海量日志收集 Flume 借助它,来实现线程池资源关闭、工作线程停止等;

 

c)在应用正常退出时,执行特定的业务逻辑、关闭资源等操作。

 

d)在 OOM 宕机、 CTRL+C、或执行 kill pid,导致 JVM 非正常退出时,加入必要的挽救措施成为可能。

其实,在 Java 的世界里遨游,只有想不到的,没有做不到的!

3. addShutdownHook 钩子函数是个啥?

刨根还要问到底!

Java 如何利用钩子函数实现优雅停服?刨根问底

 

Hook 翻译过来是「钩子」的意思,那顾名思义就是用来挂东西的。

Java 如何利用钩子函数实现优雅停服?刨根问底

 

如图所示,在现实生活中,要制作腊肉,首先用钩子把肉勾住,然后挂在竹竿上,这应该是钩子的作用。

生活如此,一切设计理念都源于生活,在 Java 的世界里,亦是如此。

Java 如何利用钩子函数实现优雅停服?刨根问底

 

如上图 Runtime 的源码所示,遵循 Java 的核心思想「一切皆是对象」,那么可以把 addShutdownHook 方法可以视作挂钩子,其实称之为钩子函数会好一些,而现实生活中的肉就可以抽象为释放资源的线程。

只要有这个钩子函数,对外就提供了扩展能力,研发人员就可以往钩子上挂各种自定义的场景实现,这种设计你细品那绝对是香!这也就是 Canal、Flume、Tomcat 等不同应用,在优雅停服时有着不同的实现的原因吧。

大白话,钩子函数有了,想挂什么东西,根据心情自己定就好了。

再深入去刨会发现,由于底层数据结构采用 Map 来进行存储,那么就支持研发人员挂多个 shutdownHook 的实现,又带来了无限的可能性(又带来了无限的「刺激」,自己好好去体会)。

Java 如何利用钩子函数实现优雅停服?刨根问底

 

好了,避免头大,就刨到这儿吧,感兴趣的可自行顺着思路继续刨下去。

4. 寄语,写在最后

作为研发人员:要拥有一双善于发现的眼睛,要善于发现代码之美。

作为研发人员:要时常思考面对当前的项目,是否能够简单重构让程序跑的更顺溜。

作为研发人员:要多看、多悟、多提炼、多实践。

作为研发人员:请不要放弃代码,因为程序终会铸就人生。

本次分享就到这里,希望对你有所帮助吧。

一起聊技术、谈业务、喷架构,少走弯路,不踩大坑。欢迎关注「一猿小讲」,会持续输出原创精彩分享,敬请期待!

声明:本站部分内容来自互联网,如有版权侵犯或其他问题请与我们联系,我们将立即删除或处理。
▍相关推荐
更多资讯 >>>