可以实时修改参数的线程池--动态线程池实现探索

不知不觉已经半年没有动笔写过东西了,工作和生活的节奏都变化得太快也太多了

一、引言

在一本书上看到的,关于对线程池使用存在几个阶段。

石器时代-只会用最原始的工具,以生存为第一要素。线程池配置在初始化线程池的代码中指定,每次修改时需要在代码里找到并直接修改,再打包部署

农业时代-这时的人们已经能够创造各种工具,掌握了一些自然规律,靠着这些知识实现自给自足。线程池的配置信息写在配置文件,修改后重新打包部署

工业时代-工业化带来生产力提高表现在机械化和集中化。这时的线程池配置信息写在分布式配置中心,但是需要重启

信息时代-信息时代是对工业时代的继承,最大化发挥工业提供的能力。这时的线程池配置信息也是写在分布式配置中心,但是不需要重启配置修改后能够直接生效

二、背景

其实这是发生在很久以前的事了,大概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,但也算是自己的一个小小的项目吧。这篇文章的草稿一直待在笔记里没有动过,最近半年更是一篇文章都没有写过,于是已经过了快两年又开始炒这盘冷饭。

就像文章开头提到的工作和生活都变化得太快了,在看美团的技术博客和做这个项目的时候没有想到会入职美团,也没有想到会因为太忙连博客都不怎么写了。

希望自己未来还是能够多思考,不管是技术上的还是业务上的。

附录

整体的类图

类图

  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值