不懂优雅停机,搞挂了线上服务,咋办?

2023-11-19

9270337dcaabf8067b79da8ae92da9ab.jpeg

程序员的成长之路

互联网/程序员/技术/资料共享 

关注

阅读本文大概需要 7 分钟。

来自:陈树义 / 树哥聊编程

公司项目是用 consul 进行注册的,在发布微服务的时候,总是会导致调用方出现一定几率的调用失败。一开始百思不得其解,后来咨询了资深的同事才知道:原来是服务下线的时候没有优雅停机,没有去 consul 将自己下线再停机,导致调用方拿到了旧的调用地址,导致调用失败!看来优雅停机还是一个蛮重要的知识点,可不能忽略,今天就让我们来盘盘它吧!

什么是优雅停机?

在 Linux 世界里,一切都是资源。当我们启动一个 JVM 的时候,我们就加载了许多的资源。而当我们关闭 JVM 的时候,JVM 只会释放内存这个资源,而其他资源是不会释放的,例如:网络连接、文件句柄等等。

Linux 的网络连接数、文件句柄数都是有限的,如果我们没有及时释放,时间久了就会导致一些奇怪的问题。那么如何在 JVM 关闭的时候,释放这些资源呢?答案就是:利用 Java 提供的 ShutdownHook 接口。我们所说的优雅停机,就是利用 Java 提供的 ShutdownHook 接口注册一个钩子,让 JVM 在关闭之前执行钩子函数的代码,让其关闭对应的资源。

适用场景

在学会怎么使用优雅停机之前,我们需要弄清楚优雅停机适用于哪些场景,那我们就需要先弄清楚 JVM 关闭的几种情况了。JVM 关闭的情况可以分为 3 大类 11 个情况,如下图所示:

a8015d0b821c1fb5294c8175dc6190a3.png

JVM 关闭的场景

在 JVM 关闭的 3 大类场景中,只有正常关闭与异常关闭是支持优雅停机的,而强制关闭则是不支持的。下面我们通过三个例子来验证一下。

JVM 正常关闭

JVM 正常关闭这种情况,我们只需要正常运行一个 main 函数,然后为其注册一个 ShutdownHook 即可,其代码如下所示。

public class NormalShutdownTest {
    public void start() {
        Runtime.getRuntime().addShutdownHook(new Thread(() ->
                System.out.println("钩子函数被执行,可以在这里关闭资源。")
        ));
    }

    public static void main(String[] args) {
        new NormalShutdownTest().start();
        System.out.println("主应用程序在执行,正常关闭。");
    }
}

输出结果为:

主应用程序在执行,正常关闭。
钩子函数被执行,可以在这里关闭资源。

可以看到钩子函数的代码正常执行了。如果你在 main 函数增加 System.exit(0) 代码,执行之后的结果也还是一样。这说明 JVM 正常关闭情况下,是支持优雅停机的。

异常关闭

JVM 异常关闭这种情况,我们尝试制造内存溢出。只需要声明一个 500 MB 的数组,然后设置 JVM 堆最大为 20 MB 即可(-Xmx20M),其代码如下所示。

public class OomShutdownTest {
    public void start() {
        Runtime.getRuntime().addShutdownHook(new Thread(() ->
                System.out.println("钩子函数被执行,可以在这里关闭资源")
        ));
    }

    public static void main(String[] args) throws Exception {
        new OomShutdownTest().start();
        System.out.println("主应用程序在执行,内存溢出关闭。");
        byte[] b = new byte[500 * 1024 * 1024];
    }
}

执行结果为:

主应用程序在执行,内存溢出关闭。
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
 at tech.shuyi.javacodechip.shutdownhook.OomShutdownTest.main(OomShutdownTest.java:13)
钩子函数被执行,可以在这里关闭资源

可以看到 JVM 抛出了 OOM 错误,但是钩子函数还是被执行了。如果你在 main 函数中自行抛出 RuntimeException,钩子函数也还是会被执行。感兴趣的朋友可以自行尝试一下。

强制关闭

JVM 强制关闭这种情况,我们可以使用 Runtime.getRuntime().halt(1) 进行测试,其代码如下所示。

public class ForceShutdownTest {
    public void start() {
        Runtime.getRuntime().addShutdownHook(new Thread(() ->
                System.out.println("钩子函数被执行,可以在这里关闭资源。")
        ));
    }

    public static void main(String[] args) throws Exception {
        new ForceShutdownTest().start();
        System.out.println("主应用程序在执行,强制关闭。");
        Runtime.getRuntime().halt(1);
    }
}

执行结果:

主应用程序在执行,强制关闭。

可以看到钩子函数并没有被执行,所以 JVM 强制关闭这种场景不支持优雅停机。

最佳实践

看了上面的例子,看起来优雅停机没那么复杂嘛。实际上,优雅停机用不好,很可能出现一些其他问题。这里给出几个最佳实践原则,帮助大家用好优雅停机!

只注册一个钩子

我们都知道 JVM 可以注册多个钩子,而钩子本质上是一个线程,可以并发执行。那么就很可能出现钩子之间相互依赖,这样就会导致依赖死锁了。另外,也可能因为多个钩子操作同一个资源,导致资源竞争出现死锁。因此,较好的一种方式就是只注册一个钩子,所有的资源释放都在这个钩子中操作。

确保线程安全

因为钩子本质上也是一个线程,JVM 可能会并发执行多个钩子,JVM 并不保证它们的执行顺序,因此需要保证钩子中的操作是线程安全的。当然了,如果你只有一个钩子的话,那这个提示可以忽略了。

不要做耗时的操作

在钩子中,不要做耗时的操作。因为当我们要关闭 JVM 时,用户肯定是希望尽快关闭,因此钩子中主要用于关闭残留资源,不应该再做其他耗时的操作。

不要做注册、移除钩子的操作

在关闭钩子中,不能执行注册、移除钩子的操作,否则 JVM 抛出IllegalStateException。

不要调用 System.exit () 操作

也不能调用 System.exit() 操作,但是调用 Runtime.halt() 操作是可以的。我想,这是因为调用 System.exit () 操作会导致循环进入钩子,导致死循环吧。

需要考虑的资源

除了上面一些代码上的操作需要考虑,我们还需要注意下面这些场景的处理:

  • 池化资源的释放:数据库连接池、HTTP 连接池、线程池。

  • 在处理线程的释放:已经被连接的 HTTP 请求。

  • MQ 消费者的处理:正在处理的消息。

  • 隐形受影响的资源的处理:Zookeeper、Nacos 实例下线等。

应用案例

Java 提供的优雅停机机制,可以说是许多框架的基础。诸如 Spring、Consul 等中间件框架,都是利用 Java 提供的这个机制进行优雅停机的。

Spring 的优雅停机

例如 Spring 是基于 Java 语言开发的框架,那其也势必依赖于 JVM 的 ShutdownHook。Spring 关于优雅停机的代码在 org.springframework.context.support.AbstractApplicationContext#registerShutdownHook 处,代码如下图所示。

@Override
public void registerShutdownHook() {
    if (this.shutdownHook == null) {
        // No shutdown hook registered yet.
        this.shutdownHook = new Thread(SHUTDOWN_HOOK_THREAD_NAME) {
            @Override
            public void run() {
                synchronized(startupShutdownMonitor) {
                    doClose();
                }
            }
        };
        // 增加 ShutdownHook 钩子
        Runtime.getRuntime().addShutdownHook(this.shutdownHook);
    }
}

可以看到 Spring 在 registerShutdownHook() 函数里,注册了一个关闭的钩子,钩子中调用了 doClose() 方法。

服务治理的优雅停机

不论是 Dubbo 还是 Spring Cloud 的分布式服务框架,需要关注的是怎么能在服务停止前,先将提供者在注册中心进行反注册,然后在停止服务提供者,这样才能保证业务系统不会产生各种 503、timeout 等现象。为了实现上述说到的效果,那么我们就必须关注优雅停机这件事情。

彩蛋

我们都知道通过 kill -15 可以让 JVM 优雅停机,那我们是否可以监听特定的信号量,从而让程序做特定的操作呢?例如:让 JVM 监听第 12 信号量,然后打印一条日志,随后优雅停机。

答案是当然可以啦!我们只需要利用 Signal 类,并实现一个 SignHandler 类就可以了。其实现代码如下所示:

public class CustomShutdownTest {
    public void start() {
        Runtime.getRuntime().addShutdownHook(new Thread(() ->
                System.out.println("钩子函数被执行,可以在这里关闭资源。")
        ));
    }

    public static void main(String[] args) {
        // custom signal kill
        Signal sg = new Signal("USR2"); // kill -12 pid
        Signal.handle(sg, new SignalHandler() {
            @Override
            public void handle(Signal signal) {
                System.out.println("接收到信号量:" + signal.getName());
                // 监听信号量,通过System.exit(0)正常关闭JVM,触发关闭钩子执行收尾工作
                System.exit(0);
            }
        });
        // other logic
        new CustomShutdownTest().start();
        System.out.println("主应用程序在执行,正常关闭。");
         try {
          Thread.sleep(30000);
         } catch (InterruptedException e) {
          e.printStackTrace();
         }
    }
}

我们启动该类后,先让其休眠 30 秒,随后用 jps 命令找到进程 ID,随后运行 kill -USR2 PID 即可,如截图所示。

5caf2d80c8ba75d1357c75900ca04139.png

随后可以看到控制台打印出如下消息:

主应用程序在执行,正常关闭。
接收到信号量:USR2
钩子函数被执行,可以在这里关闭资源。

从上面消息我们知道,JVM 成功接收到了 USR2 信号量,也成功执行了钩子函数。搞定!

提示:其实 USR2 是 Linux 第 12 个信号量,是留给用户使用的一个信号量。我们可以通过该信号量做一些定制化操作,从而实现更加复杂的功能。

<END>

推荐阅读:

网易一面,痛失30K:为啥用阻塞队列,list不行吗?

扔掉工具类!MyBatis 一个简单配置搞定加密、解密,好用!

互联网初中高级大厂面试题(9个G)
内容包含Java基础、JavaWeb、MySQL性能优化、JVM、锁、百万并发、消息队列、高性能缓存、反射、Spring全家桶原理、微服务、Zookeeper......等技术栈!
⬇戳阅读原文领取!                                  朕已阅
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

不懂优雅停机,搞挂了线上服务,咋办? 的相关文章

随机推荐

  • 线程池的使用实例

    线程池的使用实例 number 创建线程个数 创建线程池 ExecutorService cachedThreadPool Executors newCachedThreadPool 每次线程执行完毕 计数 1 当计数减到 0 之后 才能解
  • 红帽7.9部署telnet服务

    升级ssh 为预防万一提前配置telnet服务 安装软件包 yum install telnet server yum install xinetd xinetd加入开机自启 systemctl enable xinetd service
  • DB2分组查询之row_number() over(partition by order by )说明

    作用 数据库根据某几个字段去除重复记录 并进行排序 语法 ROW NUMBER OVER PARTITION BY COL1 ORDER BY COL2 功能 表示根据COL1分组 在分组内部根据 COL2排序 而这个值就表示每组内部排序后
  • 图像的打开、修改、显示和保存示例(>OpenCV 2.0)

    代码如下 include
  • Vue项目中使用Sass变量自定义ag-grid表格主题样式

    1 在Vue项目中新建一个样式文件 src assets styles table scss 引入ag grid默认的样式文件 table scss ag icons path node modules ag grid community
  • 蓝桥杯每日一题2023.9.16

    蓝桥杯2022年第十三届省赛真题 X进制减法 C语言网 dotcpp com 题目描述 进制规定了数字在数位上逢几进一 X 进制是一种很神奇的进制 因为其每一数位的进制并不固定 例如说某种 X 进制数 最低数位为二进制 第二数位为十进制 第
  • 【机器学习】【逻辑回归】最大似然估计的推导和求解步骤和梯度上升算法求解

    伯努利分布 如果随机变量X 0 1 并且相应的概率满足 P X 1 p 0
  • halfShader 模型切割

    Shader Unlit halfShader Properties Diffuse Diffuse Color 1 1 1 1 MainTex Texture 2D white StartCutoff StartCutoff Range
  • Kafka 架构及原理分析

    Kafka 架构及原理分析 文章目录 Kafka 架构及原理分析 简介 使用场景 架构 Broker Topic 副本机制 存储 消费分组 消费编号 数据多写支持 基于 binlog 实现主从复制 Kafka 的进阶功能 消息幂等性 事务
  • (五)数据清理之stata的使用----------变量进行具体处理的命令

    br 浏览文件所存储的内容 set obs n 具体样本数量 该命令会自动帮你生成n个样本 list 显示样本的内容 使用方法为 list in 5 显示第5个样本的内容 list in 1 10 表示至的关系 显示1到至10的样本内容 l
  • ldap 初始化配置

    dn cn config objectClass olcGlobal cn config olcArgsFile var run openldap slapd args olcPidFile var run openldap slapd p
  • 数学建模之BP神经网络

    1 BP网络基本数学原理 BP网络是一种多层前馈神经网络 它的名字源于在网络训练中 调整网络权值的训练算法是反向传播算法 即BP学习算法 BP网络是一种具有三层或者三层以上神经元的神经网络 包括输入层 隐含层和输出层 上下层之间实现全连接
  • Vuetify笔记(6):v-form表单及校验

    在表单验证方面 Vuetify拥有集成了众多的功能 想要使用第三方验证插件 您可以开箱即用Vee validate和Vuelidate 1 v form表单 1 1 v form的属性和方法 v form表单的常用属性 1 lazy val
  • 什么是视图?普通视图和物化视图什么区别?看这一篇就明白了

    目 录 普通视图 如何创建普通视图 物化视图 增量刷新是如何判断哪些记录被改动的 物化视图创建案例 普通视图 视图可以理解为一张表或多张表的预计算 这些表称为基表 它可以将所需要查询的结果封装成一张虚拟表 基于它创建时指定的查询语句返回的结
  • U-Boot顶层Makefile详解

    文章目录 一 U Boot工程目录分析 1 打包编译好的uboot 2 目录介绍 1 arch 2 board 3 configs 4 Makefile 5 config 6 README 二 VSCode工程创建 1 新建工程 2 屏蔽不
  • 【docker】文档 [不断补充中...]

    全栈技术分享 文档API化 简单易懂 快速入门 动手党福音 跨界佬福利 直接搞技术 不背八股文 觉得对你有帮助的话点个赞吧 感兴趣的加关注 收藏书签方便随时查阅 同文档会不定期更新补充 有问题欢迎留言讨论 虚拟化 一种资源管理技术 硬件资源
  • 忍3服务器维护奖励,7月3日服务器维护公告

    亲爱的忍村学员 感谢大家对 忍者村大战2 的支持 为给大家带来更好的游戏体验 我们已于7月3日7 00 10 00进行全区停机版本更新 注 请勿擅自修改或替换客户端图片文件 会导致您的游戏崩溃或更新版本失败 如出现以上情况请卸载本地的客户端
  • mysql服务器搭建方法_windows下搭建MySQL服务器步骤详解

    Mysql是一个数据库系统 它包括数据库服务器 并且有一个数据库管理系统对数据库服务器进行管理 同时还包括有一个数据库客户端 用于与用户交互 从官方网站下载Mysql数据库系统的安装包程序 http www mysql com downlo
  • 怎样将好多个字符串组装成一个数组

    最近在写一个项目 在这个写的途中 发现了一个问题 就是不会将字符串组装成数组 然后去问了学长才知道 于是赶紧过来做个笔记 首先 我们需要先创建一个存储字符串的数组 创建数组 String hids new String hrs size 然
  • 不懂优雅停机,搞挂了线上服务,咋办?

    程序员的成长之路 互联网 程序员 技术 资料共享 关注 阅读本文大概需要 7 分钟 来自 陈树义 树哥聊编程 公司项目是用 consul 进行注册的 在发布微服务的时候 总是会导致调用方出现一定几率的调用失败 一开始百思不得其解 后来咨询了