不知不觉已经半年没有动笔写过东西了,工作和生活的节奏都变化得太快也太多了
一、引言
在一本书上看到的,关于对线程池使用存在几个阶段。
石器时代-只会用最原始的工具,以生存为第一要素。线程池配置在初始化线程池的代码中指定,每次修改时需要在代码里找到并直接修改,再打包部署
农业时代-这时的人们已经能够创造各种工具,掌握了一些自然规律,靠着这些知识实现自给自足。线程池的配置信息写在配置文件,修改后重新打包部署
工业时代-工业化带来生产力提高表现在机械化和集中化。这时的线程池配置信息写在分布式配置中心,但是需要重启
信息时代-信息时代是对工业时代的继承,最大化发挥工业提供的能力。这时的线程池配置信息也是写在分布式配置中心,但是不需要重启配置修改后能够直接生效
二、背景
其实这是发生在很久以前的事了,大概20年底,当时自己负责的一个C端项目有服务性能上的指标要求,需要进行大量的压测和服务性能调优,当时写过一篇文章实践服务可用性的提高方式 – JVM参数调优来介绍压测过程中虚拟机调优的相关操作。
而当时还遇到了另外的一些问题,修改线程池参数实在是太麻烦了。部门内的通用做法比较简单,因为大部分项目都是ToB的,所以线程池参数也很简单,基本上只是为了并发来降低响应时间而使用的,所以参数的配置也都是放在代码里硬编码的。
在这个服务里,使用线程池同步处理请求,希望能够达到流量削峰的作用,至于为什么没有用消息队列这又是另一个故事了,暂且不表。所以在压测过程中,为了能够快速找到最合适的线程池参数,代码里硬编码线程池参数的方式就成了一个痛点,每次分析完之后都需要修改代码提交再重新编译和部署,这个过程十分耗时。
于是就有了能不能提高效率,把线程池的配置放到配置中心,能够实时更新线程池的想法,也是很巧,看博客的时候,正好看到了美团技术团队关于他们的动态线程池的实现方式介绍。
于是打算自己写实现一个动态线程池看看,企图从线程池的农业时代跨越到信息时代
三、设计
设计上大致分为了几个部分
核心的几个模块包括:自定义线程池,线程池管理器,配置监听,阻塞队列扩展;
非核心的模块有:日志,报警
1、自定义线程池
DynamicThreadPoolExecutor,继承自ThreadPoolExecutor
在这一个类里可以进行构造方法的重写,用来增强线程池的能力,比如重新制定线程的命名规则之类的
提供自定义的工厂,DynamicThreadFactory,实现ThreadFactory,在初始化线程时支持自定义名称;还有自定义拒绝策略选择方法,和自定义的拒绝策略,用来丰富和扩展线程池的能力。
2、线程池管理器
线程池管理器DynamicThreadPoolManager,是整个动态线程池的核心部分,提供对线程池的初始化和基本操作
从结构上看,持有一个DynamicThreadPoolExecutor的Map对象,用来记录管理的线程池;以及一个DynamicThreadPoolProperties对象用于读取配置信息进行操作
实现了线程池参数的刷新方法,用于初始化和刷新时创建和更新线程池的各项参数,伪代码如下:
public void refreshThreadPoolExecutor {
dynamicThreadPoolProperties.getExecutors.foreach(executor -> {
if (threadPoolExecutorMap.get(executor.getThreadPoolName) == null) {
// 新增线程池加入map中
DynamicThreadPoolExecutor newExecutor = createThreadPoolExecutor(executor);
threadPoolExecutorMap.put(executor.getThreadPoolName(), newExecutor);
} else {
// 更新线程池参数
updateThreadPoolExecutor(threadPoolExecutorMap.get(executor.getThreadPoolName), executor);
}
});
}
以及通过名称获取线程池的方法给外部使用方调用
3、配置更新机制
不同的配置中心可能会有不一样的监听机制和接入方式,这里以当时公司在用的apollo的实现为例来做说明
ThreadPoolProperties 类作为配置类对象,里面包含了线程池基础信息,包括线程池名称,核心池数,最大线程数,队列最大长度,队列类型,拒绝策略,空闲线程存活时间,空闲线程存活时间时间单位,队列容量阈值等线程池可配置属性。
ApolloyConfigUpdateListener监听配置变化,调用refresh方法加载读取到的配置信息,需要实现Converter,通过手动配置Binder的方式将string类型的配置数据转换成ThreadPoolProperties对象;参数校验通过后,之后再调用DynamicThreadPoolManager方法里的refreshThreadPoolExecutor方法,操作线程池对象map里的线程池,进行线程池的新增或更新
至此,一次线程池动态更新完成
4、阻塞队列扩展
阻塞队列是唯一一个线程池没有开放修改api的参数,原因在于阻塞队列长度的修改对等待队列中的任务存在影响,而观察一些阻塞队列代码的实现,发现根本没有考虑队列中任务数大于设置的容量时的场景,自己实现的话也不好兼容,出于时间成本的考虑,于是只是简单的实现一个支持resize的阻塞队列,可以往大修改队列的capacity,但是不能改小
如果要实现容量可以改大也可以改小的队列,需要对put,take等方法进行修改,支持对count > capacity这种情况的处理
5、日志&监控报警
通常来说是通过埋点或者接入日志系统实现,通过上传线程池的状态来进行监控,各个公司应该都有成熟的方案,监控的内容和实现也和使用场景有关系,这里就不细讲了
结语
记不得当时用了多少时间了,也许是花了两天时间?也参考了一些开源的动态线程池实现,很多也都是看到美团的文章后产生了类似的想法。实现完成后,在自己项目的压测分支上试了一下,基本完成了目标,达到了预期的效果。不过从今天来看,如果是作为一个正式项目的话,还有很多需要完善的点,例如参数完整性的校验等等。
总的来说只是一个demo,但也算是自己的一个小小的项目吧。这篇文章的草稿一直待在笔记里没有动过,最近半年更是一篇文章都没有写过,于是已经过了快两年又开始炒这盘冷饭。
就像文章开头提到的工作和生活都变化得太快了,在看美团的技术博客和做这个项目的时候没有想到会入职美团,也没有想到会因为太忙连博客都不怎么写了。
希望自己未来还是能够多思考,不管是技术上的还是业务上的。
附录
整体的类图