zoukankan      html  css  js  c++  java
  • Java 如何实现优雅停服?刨根问底

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

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

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

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

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

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

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

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

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

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

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

     1 import java.util.concurrent.ScheduledThreadPoolExecutor;
     2 import java.util.concurrent.TimeUnit;
     3 
     4 /**
     5  * 体验 Java 优雅停服
     6  *
     7  * @author 一猿小讲
     8  */
     9 public class Application {
    10 
    11     /**
    12      * 监控服务
    13      */
    14     private ScheduledThreadPoolExecutor monitorService;
    15 
    16     public Application() {
    17         monitorService = new ScheduledThreadPoolExecutor(1);
    18     }
    19 
    20     /**
    21      * 启动监控服务,监控一下内存信息
    22      */
    23     public void start() {
    24         System.out.println(String.format("启动监控服务 %s", Thread.currentThread().getId()));
    25         monitorService.scheduleWithFixedDelay(new Runnable() {
    26             @Override
    27             public void run() {
    28                 System.out.println(String.format("最大内存: %dm  已分配内存: %dm  已分配内存中的剩余空间: %dm  最大可用内存: %dm",
    29                         Runtime.getRuntime().maxMemory() / 1024 / 1024,
    30                         Runtime.getRuntime().totalMemory() / 1024 / 1024,
    31                         Runtime.getRuntime().freeMemory() / 1024 / 1024,
    32                         (Runtime.getRuntime().maxMemory() - Runtime.getRuntime().totalMemory() +
    33                                 Runtime.getRuntime().freeMemory()) / 1024 / 1024));
    34             }
    35         }, 2, 2, TimeUnit.SECONDS);
    36     }
    37 
    38     /**
    39      * 释放资源(代码来源于 flume 源码)
    40      * 主要用于关闭线程池(看不懂的同学莫纠结,当做黑盒去对待)
    41      */
    42     public void stop() {
    43         System.out.println(String.format("开始关闭线程池 %s", Thread.currentThread().getId()));
    44         if (monitorService != null) {
    45             monitorService.shutdown();
    46             try {
    47                 monitorService.awaitTermination(10, TimeUnit.SECONDS);
    48             } catch (InterruptedException e) {
    49                 System.err.println("Interrupted while waiting for monitor service to stop");
    50             }
    51             if (!monitorService.isTerminated()) {
    52                 monitorService.shutdownNow();
    53                 try {
    54                     while (!monitorService.isTerminated()) {
    55                         monitorService.awaitTermination(10, TimeUnit.SECONDS);
    56                     }
    57                 } catch (InterruptedException e) {
    58                     System.err.println("Interrupted while waiting for monitor service to stop");
    59                 }
    60             }
    61         }
    62         System.out.println(String.format("线程池关闭完成 %s", Thread.currentThread().getId()));
    63     }
    64 
    65     /**
    66      * 应用入口
    67      */
    68     public static void main(String[] args) {
    69         Application application = new Application();
    70         // 启动服务(每隔一段时间监控输出一下内存信息)
    71         application.start();
    72 
    73         // 添加钩子,实现优雅停服(主要验证钩子的作用)
    74         final Application appReference = application;
    75         Runtime.getRuntime().addShutdownHook(new Thread("shutdown-hook") {
    76             @Override
    77             public void run() {
    78                 System.out.println("接收到退出的讯号,开始打扫战场,释放资源,完成优雅停服");
    79                 appReference.stop();
    80             }
    81         });
    82         System.out.println("服务启动完成");
    83     }
    84 }

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

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

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

    程序跑起来,效果如下。

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

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

    呜呼!结果不会骗人的,当用 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 钩子函数是个啥?

    刨根还要问到底!

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

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

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

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

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

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

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

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

    4. 寄语,写在最后

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

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

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

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

    本次分享就到这里,希望对你有所帮助吧。一起聊技术、谈业务、喷架构,少走弯路,不踩大坑。

    会持续输出原创精彩分享,敬请期待!关注同名公众号:一猿小讲,回复「1024」可以获取精心为您准备的职场打怪进阶资料。

  • 相关阅读:
    Laravel框架之Session操作
    Laravel框架之Response操作
    Laravel之简单的学生信息管理平台
    Laravel中的模板引擎Blade
    Laravel中的查询构造器
    Laravel中使用模型对数据进行操作
    Laravel中的模型的创建
    springboot
    不丢失log的情况下迁移git空间
    Vue2.0中v-for迭代语法变化(key、index)
  • 原文地址:https://www.cnblogs.com/socoool/p/12921552.html
Copyright © 2011-2022 走看看