我用极简方案实现动态线程池

402 阅读6分钟

一、动态线程池的由来与概念

1.1 痛点

如果有在项目中实际使用线程池,相信你可能会遇到以下痛点:

  • 生产环境的“黑盒炸弹”

    • 线程池上线后,开发人员无法感知运行情况,完全黑盒
    • 当业务出现超时、熔断等问题时,因为没有监控,无法确定是不是线程池引起。
    • 线程池任务执行时间超过平均执行周期,开发人员无法感知。
  • 参数难以评定

    • 线程池随便定义,线程资源过多,造成服务器高负载。
    • 线程池参数不易评估,随着业务的并发提升,业务面临出现故障的风险。
  • 对突发流量束手无策

    • 线程池任务堆积,触发拒绝策略,影响既有业务正常运行。

1.2 概念的由来

动态线程池概念第一次提出是来自于2020年04月02日美团发布的Java线程池实现原理及其在美团业务中的实践

美团提出动态化线程池的核心设计包括以下三个方面:

  • 简化 线程池 配置:

    • 关注核心参数:corePoolSize、maximumPoolSize 和 workQueue。

    • 两种并发场景:

      • 提高响应速度:使用同步队列,立即执行任务。
      • 提升吞吐量:使用有界队列,缓冲大批量任务,并限制队列容量。
  • 参数动态修改:

    • 封装线程池以支持动态调整参数。
    • 通过监听外部消息,允许实时更改配置。
    • 将配置放在平台侧,便于查看和修改。
  • 增加 线程池 监控:

    • 增强对线程池状态的监控。

    • 帮助开发者了解线程池运行情况,以便优化性能。

可惜美团没开源,只能自己搞了😅

二、为什么选择自己造轮子

2.1 各显神通

目前对动态线程池的实现数不胜数,各厂都有自己的实现,目前最热门的开源项目主要是2个:

hippo4j****

dynamic-tp****

开源组建的优点十分明显:功能全面且丰富

  • 全局管控

  • 动态变更

  • 通知报警

  • 数据采集

  • 运行监控

  • 功能扩展

  • 多种模式

  • 容器管理

  • 框架适配

  • 变更审核

  • 动态化插件

  • .....

开源动态线程池不仅包含核心的动态、监控、告警功能,甚至覆盖插件、权限控制、数据采集等等功能。

开源作者为了适配覆盖全面、适配了非常多的技术,例如市面上几乎所有的配置中心、所有的通知类型、所有的采集方式,技术栈全面覆盖的同时带来了另外一个问题:组件冗余且沉重、 mvn****依赖冲突十分严重

2.2 适合食亨的动态线程池组件是什么样的?

除去冗余的功能,我们真正需要的功能其实只有文章中提出的提供如下功能

  1. 动态调参:支持线程池参数动态调整、界面化操作;包括修改线程池核心大小、最大核心大小、队列长度等;参数修改后及时生效。

  2. 任务监控:支持应用粒度、线程池粒度、任务粒度的Transaction监控;可以看到线程池的任务执行情况、最大任务执行时间、平均任务执行时间、95/99线等。

  3. 负载告警:线程池队列任务积压到一定值的时候会通过大象(美团内部通讯工具)告知应用开发负责人;当线程池负载数达到一定阈值的时候会通过大象告知应用开发负责人。

  4. 操作监控:创建/修改和删除线程池都会通知到应用的开发负责人。

  5. 操作日志:可以查看线程池参数的修改记录,谁在什么时候修改了线程池参数、修改前的参数值是什么。

  6. 权限校验:只有应用开发负责人才能够修改应用的线程池参数。

而我们根据食亨的技术特点分解后,可以一一实现探讨最简实现方案

  1. 动态调参: 动态调参可以通过修改Apollo配置,各个容器节点监听Apollo变更事件从而改变动态线程池参数。

  2. 任务监控: 监控可以直接对接Granfa实现大屏监控。

  3. 负载告警:直接对接团队的监控中心,通过监控中心来实现告警 (可由各自己团队的技术特点抉择)。

  4. 操作监控:Apollo支持查看操作日志

  5. 操作日志:Apollo支持查看操作日志

  6. 权限校验:Apollo支持权限校验,只有产研团队的人员有Apollo权限

从上我们可以总结出我们自己封装 SDK 需要做的事:

  • 基于Apollo变更事件来动态修改 线程池

    • 实现动态调参、操作监控、操作日志、权限校验功能
  • 对接监控中心实现监控报警

    • 实现负载告警功能
  • 对接Granfa实现任务监控

    • 实现任务监控功能

三、动态配置的实现方案

  • 实现ApplicationContextAware接口获取应用上下文
  • @ApolloConfigChangeListener 监听Apollo变更事件
  • interestedKeyPrefixes指定对应的前缀判断是否为变更线程池事件

四、观测、监控的实现方案

  • 实现ApplicationContextAware接口获取应用上下文
  • 实现ApplicationRunner接口,容器启动时开启任务
  • @Async 启动异步任务
  • 循环+睡眠实现数据采集
  • 使用动态配置frequencies可动态调控采集频率

五、封装spring-boot-starter一键启动

现在我们已经封装了动态配置组件、观测监控组件。如果项目要应用我们的SDK则需要将这2个组件注入到Spring容器中,可以在项目中使用@Import组件注入。

本文使用的是第二种方式---实现spring-boot-starter自动注入、且基于自定义注解启动 SDK

1、 maven 依赖

2、spring.factories

在resources/META-INF目录下,创建spring.factories,主要要引入全路径

3、定义自定义注解

4、在组件上加上@ConditionalOnBean、@Component

其中@ConditionalOnBean(annotation = EnableDynamicThreadPool.class)控制组件是否注入

以上就可实现基于注解一件开启SDK(本文此处不展开实现原理)

六、监控看版的重要指标

看版如下:

重要指标有:

  • 线程池 核心参数

    • 观测线程池的核心参数:最大线程数、核心线程数、阻塞队列大小
  • 线程池 等待任务数

    • 观测当前服务各个节点之间的任务堆积情况
  • 当前 线程池 线程数

    • 查看当前线程池内有多少个线程,这是判断线程池运行情况的重要指标
  • 已完成的任务数

    • 可以通过这个参数判断自己的线程池的运行状况
  • 活跃 线程数

    • 当前正在执行任务的线程数
  • 等待执行的任务数

    • 观测服务各个节点之间的任务堆积情况的趋势

七、总结:极简主义

这次实现动态线程池的核心思路就是:

  1. 做减法:只保留核心功能,去掉所有花哨特性

  2. 借力打力:现成的Apollo 做配置中心,Grafana 做监控

  3. 自动化:通过 spring-boot-starter 实现一键集成

其实很多时候,我们不需要追求 "大而全" 的框架,根据自己的实际需求,用最简单的方式解决问题,反而更高效、更可靠。