写点什么

ShutdownHook 妙用

作者:FunTester
  • 2023-09-12
    北京
  • 本文字数:2636 字

    阅读完需:约 9 分钟

上期文章分享了ShutdownHook的 API 和基本使用,但是少了一些实际工作中的案例,总感觉没啥大用一样。

最近总结工作中可以用到ShutdownHook来解决一些实际问题的例子,分享给大家。

任务统计

FunTester测试框架定义了好几个自定义的异步关键字,例如funfunnyfunner等。一旦使用到异步,肯定会用到线程池。但是「Java」线程池销毁需要手动操作,之前的视线中是放在「daemon」线程中实现。如果我想在每次 JVM 关闭之前都统计一下线程池执行任务总数,就不能写在daemon线程中了,原因有二:

  1. daemon线程可能出现异常,导致退出。

  2. 用户可能会主动停止程序,例如 C

虽然可以通过自定义daemon线程解决这个问题。如果你想使用一种优雅的方法完成这个任务的话,那么ShutdownHook绝对是不二选择。

static {      addShutdownHook {          if (asyncPool != null) {              print "finished: " + getFunPool().getCompletedTaskCount() + " task"          }          RuntimeMXBean runtimeMXBean = ManagementFactory.getRuntimeMXBean()          print(" uptime:" + runtimeMXBean.getUptime() + " s")      }  }

复制代码

通过静态代码块把两个ShutdownHook注册到 JVM 中,这一点也是从Web3j异步线程池源码中学到的。当然这只是个初级版本,这里抛砖引玉。

这里本地的动态 QPS 模型经常会用到java.util.Scanner来接收控制台输入信息,也需要进行资源回收。

static {      Runtime.getRuntime().addShutdownHook(new Thread(() -> closeScanner()));  }
复制代码

释放连接

ShutdownHook最重要的一个功能就是释放资源,通常如果是非服务形式启动一个JVM进程的话,只有确保JVM进程能够正常退出的话,通常不用担心资源泄露或者无法回收的问题。如果对于Java服务的话,通常使用池化框架来管理连接资源。对于测试工作来讲,我觉得足够了,只有很少细微的差别。

所以我对com.funtester.httpclient.FunHttp做了一点改造,这次用的JavaAPI

    /**     * 结束测试,关闭连接池     */    static {        Runtime.getRuntime().addShutdownHook(new Thread(() -> {            try {                ClientManage.httpsClient.close();            } catch (IOException e) {                e.printStackTrace();            }            try {                ClientManage.httpAsyncClient.close();            } catch (IOException e) {                e.printStackTrace();            }        }));
    }

复制代码

这里查了一些客户端主动断开连接和客户端JVM直接关闭对服务端的影响,可供参考:

  1. 「客户端主动发送断开连接:」 当客户端在与服务器建立的 HTTPS 连接上主动发送断开连接请求(通过关闭连接),服务器会接收到这个请求,并根据 HTTP 协议的规范进行处理。服务器会知道连接被关闭,这可以触发服务器端的一些清理操作,例如释放连接资源,清理会话状态等。这是一个正常的连接关闭过程,服务器端会收到关闭连接的通知。

  2. 「客户端直接关闭:」 当客户端断开与服务端连接(例如断网),服务器无法立即感知到这个变化。这是因为客户端和服务器之间的连接是通过网络进行的,服务器没有直接的方式知道客户端是否断开了网络连接。在这种情况下,服务器会等待一段时间,直到发现客户端不再发送请求或响应。服务器可能会将这个连接保持一段时间,然后最终超时并关闭连接。

总之,客户端主动发送断开连接是一个明确的行为,服务器能够立即响应。而客户端断网可能会导致连接在服务器端保持一段时间,直到服务器超时或检测到连接不再活动。服务器通常会实现一些超时机制来管理连接,以防止持久的无效连接占用资源。

所以如果考虑这些细微的差别,还是选用主动断开回收这些资源。

PS:到这里,当我们需要添加超过 1 个ShutdownHook的时候,就可以非常明显地感受到daemon线程实现方案的差异,因为ShutdownHook可以到处写,多点开花。

应对 JVM 异常退出

如果你在使用 docker 和 k8s 的话,压测很容易导致 JVM 进程被迫退出。这个时候容器可能还在,只是 JVM 进程退出了,如果我们在基础依赖中,添加上下面这段代码,就可以发送消息。

    static {        addShutdownHook {            send("我挂了,顺便采集了快照!")            snapshot()        }    }
复制代码

状态记录

性能测试中,造数据的工作同行是繁琐且量大,一般都会使用脚本或者平台实现。但在实际使用当中总会遇到各种各样的异常,导致部分已经创建好的数据丢失,或者需要重新查库才能恢复。

下面是一个例子,场景:需要创建 100w 个新用户(假设用户属性均在已经封装好的方法中完成设置)。

import com.funtester.frame.Saveimport com.funtester.frame.SourceCodeimport com.funtester.utils.RWUtil
class HookTest extends SourceCode {
    static Vector<Integer> ids = RWUtil.readByNumLine(getLongFile("ids"))
    static void main(String[] args) {        100_0000.times {            if (ids.size() > 100_0000) fail()            fun {                def id = create()                if (id > 0) {                    ids << id                }            }        }    }
    static {        addShutdownHook {            Save.saveIntegerList(ids, getLongFile("ids"))        }    }
    static int create() {        getRandomInt(Integer.MAX_VALUE)    }}

复制代码

这里循环 100w 次,使用默认的线程池并发创建用户,如果用户的 ID 是大于 0,认为正常用户,添加到线程安全的队列中。

一旦遇到异常,ShutdownHook中的方法会把已经创建好的用户 ID 存在文件中。每次执行,已创建好的用户 ID 集合会从文件中进行初始化。

多次执行,直到fail()抛异常,最后一次ids文件就是我们需要的创建用户的 ID 集合。

发布于: 刚刚阅读数: 5
用户头像

FunTester

关注

公众号:FunTester,800篇原创,欢迎关注 2020-10-20 加入

Fun·BUG挖掘机·性能征服者·头顶锅盖·Tester

评论

发布
暂无评论
ShutdownHook妙用_FunTester_InfoQ写作社区