欢迎大家关注我的微信公众号【老周聊架构】,Java后端主流技术栈的原理、源码分析、架构以及各种互联网高并发、高性能、高可用的解决方案。
我们上一篇 《一文读懂线程池的实现原理 》已经从线程池如何维护自身状态、线程池如何管理任务、线程池如何管理线程三个维度来深入剖析线程池的底层原理与源码剖析,这让我们对线程池的原理有了较为深入的理解。这对我们多线程编程有很大的帮助,但在使用线程池时还是会面临几个棘手的问题。
我们先来看下美团调研的业界一些线程池参数配置方案:
基于上面线程池的几个痛点,那有没有好的解决方案呢?有的,那就是动态调整线程池参数。
尽管业界没有一些成熟的经验配置策略,那么我们是不是可以从修改线程池参数的成本入手?毕竟每次线上线程池故障的话,都得修改代码里的线程池相应的参数,然后再部署上线,这个过程在对可用性要求极高的项目中那是极其慢的,可能给公司造成巨大的损失。
既然改代码里的线程池相应的参数并上线这个过程慢,那我们是不是可以把相应的线程池参数配置到分布式配置中心上去?实现线程池参数可动态配置和即时生效,线程池参数动态化前后的参数修改流程对比如下:
2.1 整体设计
动态化线程池的核心设计包括以下三个方面:
2.1.1 简化线程池配置
线程池构造参数有 7 个,但是最核心的是 3 个:corePoolSize、maximumPoolSize、workQueue,它们最大程度地决定了线程池的任务分配和线程分配策略。考虑到在实际应用中我们获取并发性的场景主要是两种:
所以线程池只需要提供这三个关键参数的配置,并且提供两种队列的选择,就可以满足绝大多数的业务需求。
2.1.2 参数可动态修改
为了解决参数不好配,修改参数成本高等问题。在 Java 线程池留有高扩展性的基础上,封装线程池,允许线程池监听同步外部的消息,根据消息进行修改配置。将线程池的配置放置在平台侧,允许开发同学简单的查看、修改线程池配置。
2.1.3 增加线程池监控
对某事物缺乏状态的观测,就对其改进无从下手。在线程池执行任务的生命周期添加监控能力,帮助开发同学了解线程池状态。
动态化线程池提供如下功能:
了解完动态化线程池的整体设计与功能架构后,我相信你也可以设计出一款动态线程池组件出来的。下面跟着老周来实践一下动态调整线程池参数,可能不像上面设计的那样那么全面,但会把动态调整线程池参数的核心给实现一下。
不难发现动态化线程池的核心是配置管理,那我们就得找一个分布式配置中心,这里老周用的 Apollo,还有其它的像 Spring Cloud Config、disconf、某些大型互联网公司自研的分布式配置中心等,根据自己的项目情况以及使用场景来选择就行。
3.1 Apollo 总体设计
Apollo(阿波罗)是携程框架部门研发的分布式配置中心,能够集中化管理应用不同环境、不同集群的配置,配置修改后能够实时推送到应用端,并且具备规范的权限、流程治理等特性,适用于微服务配置管理场景。
3.1.1 基础模型
如下图即是 Apollo 的基础模型:
上图简要描述了 Apollo 的总体设计,我们可以从下往上看:
3.2 服务端设计
3.2.1 配置发布后的实时推送设计
在配置中心中,一个重要的功能就是配置发布后实时推送到客户端。本文重点分析配置更新推送方式,下面我们简要看一下这块是怎么设计实现的。
上图的发送 ReleaseMessage 的实现方式详情请往下继续看:
Admin Service 在配置发布后,需要通知所有的 Config Service 有配置发布,从而 Config Service 可以通知对应的客户端来拉取最新的配置。
从概念上来看,这是一个典型的消息使用场景,Admin Service 作为 producer 发出消息,各个 Config Service 作为 consumer 消费消息。通过一个消息组件(Message Queue)就能很好的实现 Admin Service 和 Config Service 的解耦。
在实现上,考虑到 Apollo 的实际使用场景,以及为了尽可能减少外部依赖,我们没有采用外部的消息中间件,而是通过数据库实现了一个简单的消息队列。
我们从基础模型、服务端设计、客户端设计三个维度来分析了 Apollo 总体设计,相信你对 Apollo 分布式配置中心有了全面且清晰的理解了。为了照顾没有用过 Apollo 这款分布式配置中心的同学,老周这里还是简单给个 Apollo 开发样例演示,希望对你后面的动态调整线程池参数实践有所帮助。
我们了解原理以及架构后,那我们开始实践了。
4.1 服务端安装
请看官方文档进行相应的安装:https://ctripcorp.github.io/apollo/#/zh/deployment/quick-start
执行启动脚本后,当看到如下输出后,就说明启动成功了!
启动成功后访问地址:http://localhost:8070
点击 SampleApp,我们看到在 DEV 环境包含一个 timeout 配置项,100 是这个配置项的值,下面我们在应用程序读取这个配置项:
4.2.1 引入依赖
<dependencies>
<dependency>
<groupId>com.ctrip.framework.apollogroupId>
<artifactId>apollo-clientartifactId>
<version>1.7.0version>
dependency>
<dependency>
<groupId>org.slf4jgroupId>
<artifactId>slf4j-simpleartifactId>
<version>1.7.25version>
<scope>compilescope>
dependency>
dependencies>
4.2.2 样例测试
/**
* 动态获取Apollo配置
* 注意要配置:-Dapp.id=myApp -Denv=DEV -Dapollo.cluster=default -Ddev_meta=http://localhost:8080
*
* @author 微信公众号【老周聊架构】
*/
public class GetApolloConfigTest {
public static void main(String[] args) throws InterruptedException {
Config config = ConfigService.getAppConfig();
config.addChangeListener(new ConfigChangeListener() {
@Override
public void onChange(ConfigChangeEvent changeEvent) {
System.out.println("Changes for namespace " + changeEvent.getNamespace());
for (String key : changeEvent.changedKeys()) {
ConfigChange change = changeEvent.getChange(key);
System.out.println(String.format("Found change - key: %s, oldValue: %s, newValue: %s, changeType: %s", change.getPropertyName(), change.getOldValue(), change.getNewValue(), change.getChangeType()));
}
}
});
Thread.sleep(1000000L);
}
}
我们现在把配置项默认的值 100 改为 200 程序输出结果如下:
控制台会出现以下日志,表明动态获取 Apollo 配置成功了。
4.3 动态线程池
上面我们把 Apollo 的动态监听修改配置的功能整明白了以后,再把线程池和 Apollo 结合起来构建动态线程池那就方便多了。首先我们用默认值构建一个线程池,然后线程池会监听 Apollo 关于相关配置项,如果相关配置有变化则刷新相关参数。
代码演示:
/**
* 动态线程池工厂
*
* @author 微信公众号【老周聊架构】
*/
@Slf4j
@Component
public class DynamicThreadPoolFactory {
/** 这里是你的namespace,我这里是默认的application **/
private static final String NAME_SPACE = "application";
/** 线程执行器 **/
private volatile ThreadPoolExecutor executor;
/** 核心线程数 **/
private Integer corePoolSize = 10;
/** 最大值线程数 **/
private Integer maximumPoolSize = 20;
/** 待执行任务的队列的长度 **/
private Integer workQueueSize = 1000;
/** 线程空闲时间 **/
private Long keepAliveTime = 1000L;
/** 线程名 **/
private String threadName;
public DynamicThreadPoolFactory() {
Config config = ConfigService.getConfig(NAME_SPACE);
init(config);
listen(config);
}
/**
* 初始化
*/
private void init(Config config) {
if (executor == null) {
synchronized (DynamicThreadPoolFactory.class) {
if (executor == null) {
String corePoolSizeProperty = config.getProperty(ParamsEnum.CORE_POOL_SIZE.getParam(), corePoolSize.toString());
String maximumPoolSizeProperty = config.getProperty(ParamsEnum.MAXIMUM_POOL_SIZE.getParam(), maximumPoolSize.toString());
String keepAliveTImeProperty = config.getProperty(ParamsEnum.KEEP_ALIVE_TIME.getParam(), keepAliveTime.toString());
BlockingQueue<Runnable> workQueueProperty = new LinkedBlockingQueue<>(workQueueSize);
executor = new ThreadPoolExecutor(Integer.valueOf(corePoolSizeProperty), Integer.valueOf(maximumPoolSizeProperty),
Long.valueOf(keepAliveTImeProperty), TimeUnit.MILLISECONDS, workQueueProperty);
}
}
}
}
/**
* 监听器
*/
private void listen(Config config) {
config.addChangeListener(new ConfigChangeListener() {
@Override
public void onChange(ConfigChangeEvent changeEvent) {
log.info("命名空间发生变化={}", changeEvent.getNamespace());
for (String key : changeEvent.changedKeys()) {
ConfigChange change = changeEvent.getChange(key);
String newValue = change.getNewValue();
refreshThreadPool(key, newValue);
log.info("发生变化key={},oldValue={},newValue={},changeType={}", change.getPropertyName(), change.getOldValue(), change.getNewValue(), change.getChangeType());
}
}
});
}
/**
* 刷新线程池
*/
private void refreshThreadPool(String key, String newValue) {
if (executor == null) {
return;
}
if (ParamsEnum.CORE_POOL_SIZE.getParam().equals(key)) {
executor.setCorePoolSize(Integer.valueOf(newValue));
log.info("修改核心线程数key={},value={}", key, newValue);
}
if (ParamsEnum.MAXIMUM_POOL_SIZE.getParam().equals(key)) {
executor.setMaximumPoolSize(Integer.valueOf(newValue));
log.info("修改最大线程数key={},value={}", key, newValue);
}
if (ParamsEnum.KEEP_ALIVE_TIME.getParam().equals(key)) {
executor.setKeepAliveTime(Integer.valueOf(newValue), TimeUnit.MILLISECONDS);
log.info("修改线程空闲时间key={},value={}", key, newValue);
}
}
public ThreadPoolExecutor getExecutor(String threadName) {
return executor;
}
}
@AllArgsConstructor
public enum ParamsEnum {
CORE_POOL_SIZE("corePoolSize", "核心线程数"),
MAXIMUM_POOL_SIZE("maximumPoolSize", "最大线程数"),
KEEP_ALIVE_TIME("keepAliveTime", "线程空闲时间"),
;
@Getter
private String param;
@Getter
private String desc;
}
/**
* 动态线程池执行器
*
* @author 微信公众号【老周聊架构】
*/
@Component
public class DynamicThreadExecutor {
@Resource
private DynamicThreadPoolFactory threadPoolFactory;
public void execute(String bizName, Runnable job) {
threadPoolFactory.getExecutor(bizName).execute(job);
}
public Future<?> sumbit(String bizName, Runnable job) {
return threadPoolFactory.getExecutor(bizName).submit(job);
}
}
@Slf4j
public class DynamicThreadPoolExecutorTest {
@Resource
private DynamicThreadExecutor dynamicThreadExecutor;
/**
* 记得 IDEA VM options 要记得加下面的参数
* -Dapp.id=SampleApp -Denv=DEV -Dapollo.meta=http://localhost:8080
*/
@Test
public void testExecute() throws InterruptedException {
while (true) {
dynamicThreadExecutor.execute("bizName", new Runnable() {
@Override
public void run() {
System.out.println("bizInfo");
}
});
TimeUnit.SECONDS.sleep(1);
}
}
}
这里可以通过 JDK 自带的 JVisualVM 工具可以查看到相应的线程使用情况。
我们在配置中心修改配置项把核心线程数设置为 50,最大线程数设置为 100:
这里还可以在代码中通过打印相应的线程状态,更加直观的从日志上观察到核心线程、最大线程数的修改情况。
private static void threadPoolStatus(ThreadPoolExecutor executor, String name) {
BlockingQueue<Runnable> queue = executor.getQueue();
System.out.println(Thread.currentThread().getName() + "-" + name + "-:" +
"核心线程数:" + executor.getCorePoolSize() +
" 活动线程数:" + executor.getActiveCount() +
" 最大线程数:" + executor.getMaximumPoolSize() +
" 线程池活跃度:" + divide(executor.getActiveCount(), executor.getMaximumPoolSize()) +
" 任务完成数:" + executor.getCompletedTaskCount() +
" 队列大小:" + (queue.size() + queue.remainingCapacity()) +
" 当前排队线程数:" + queue.size() +
" 队列剩余大小:" + queue.remainingCapacity() +
" 队列使用度:" + divide(queue.size(), queue.size() + queue.remainingCapacity()));
}
这样的话就可以实现动态调整线程池参数,这就很好的解决了我们线程池现有的痛点,不至于线上出了问题还得改代码部署那么漫长的修复时间了,动态线程池大大简化了运维以及开发快速修复相关问题的难度。