项目亮点—动态线程池管理工具

本文介绍了Hippo4j框架,一个针对Java应用的动态线程池解决方案,它提供了实时调整线程池参数、运行时异常报警、监控和适应三方框架线程池的功能,以解决线程池配置问题和提升在线运行保障。
摘要由CSDN通过智能技术生成

问题

你是否在项目中使用线程池遇到过以下问题?

1.创建线程池核心参数不好评估,随着业务流量的波动,极有可能出现生产故障。

2.不支持优雅关闭,当项目关闭时,大量正在运行的线程池任务被丢弃。

3.不支持运行时监控,使用过程中业务无响应,不知道是不是线程池引起。

4.三方框架 RocketMQ、Dubbo 等线程池无法动态修改参数,修改后只能重启应用。

在真实业务场景中,线程池可能遇到的问题比这里描述的还要多,稀奇古怪。所以采用动态线程池—Hippo4j。

功能

通过对 JDK 线程池的增强,以及扩展三方框架底层线程池等功能,为业务系统提高线上运行保

障能力。

Hippo4j 框架提供以下功能支持:

1. 客户端应用运行时实时变更指定线程池核心参数,变更生效支持集群和单实例两种方式。

2. 线程池运行时异常报警,比如:线程池活跃度、阻塞队列容量水位较高,触发了拒绝策略以

及任务运行时间超长等。

3. 定时任务(默认 5 秒)采集线程池运行数据,可上报 Prometheus、InfluxDB 等数据库,

搭配 Grafana 做大屏展示。

4. 运行过程中支持实时查看线程池当前运行状态以及线程池内线程的堆栈信息。

5. 支持 Tomcat、Undertow 和 Jetty 容器线程池运行时查看和动态变更线程池配置。

6. 支持 Dubbo、Hystrix、Kafka、RabbitMQ、RocketMQ 等客户端线程池运行时数据查看

和动态变更线程池配置。

应用场景

1. 动态调参

Google 或者百度搜索线程池和生产事故关键字,几页都放不下,这也间接说明了线程池是个很

考验使用者技术功底的技术点。

那有没有一些技巧或者技术来尽量规避线程池使用上的问题?比如:线程池的配置应该如何选

择?

对于线程池参数的纠结点主要有两个,无外乎设置的线程数多了或者少了:

1. 如果预设的线程数或阻塞队列数量少了,当业务量上来,任务都在排队或者执行拒绝策

略。

2. 如果超量设置线程池的参数,无疑会造成资源浪费。

如果要修改运行中应用线程池参数,需要停止线上应用,调整成功后再发布,而这个过程异常的

繁琐,如果能在运行中动态调整线程池的参数无疑会提高问题解决效率。

Hippo4j 提供了应用线程池运行时变更核心参数的功能。而且,如果应用是集群部署,可以选择

修改线程池某一实例,或者修改集群全部实例,运行时生效,不需要再重启服务。

压测时可以使用 Hippo4j 动态调整线程池参数,判断线程池核心参数设置是否合理。对于开发

测试来说,如果不满足可以随时调整。

2. 告警策略

很多时候,线程池出故障的时候,系统已经发生了很严重的损失。有没有一种方式,在使用的线

程池即将出现问题,但还算比较可控时,触发相关报警提示给用户,进而规避该问题?

Hippo4j 基于上述问题思考,集成了四种报警策略:

1. 活跃度:假设阈值设置 80%,线程池最大线程数 10,当线程数达到 8 发起报警。

2. 阻塞队列容量:假设阈值设置 80%,阻塞队列容量 100,当容量达到 80 发起报警。

3. 触发拒绝策略:当线程池任务触发了拒绝策略时,发起拒绝策略报警。

4. 任务运行超时:假设用户设置单个任务正常执行是 1000ms,实际执行超过该时间发起报

警。

3. 线程池监控

Hippo4j 线程池提供了两种监控方式:线程池运行时数据采集监控以及客户端线程池运行实时状

态查看。

线程池运行时数据采集适合应用负责人巡查应用健康状态和排查问题时使用,实时状态适合排查

多实例之间的运行数据状态。

4. 框架底层线程池

上面讲的动态线程池是业务中开发人员手动创建的线程池,比如下面这个:

@Bean
@DynamicThreadPool
public ThreadPoolExecutor messageConsumeDynamicExecutor() {
    String threadPoolId = "message-consume";
    return ThreadPoolBuilder.builder()
            .threadFactory(threadPoolId)
            .threadPoolId(threadPoolId)
            .dynamicPool()
            .build();
}

而框架线程池指的是某些三方中间件底层使用到的线程池,比如 Dubbo、RocketMQ 等框架,这些底层框架为了增强性能选择使用线程池进行扩展。

为什么要适配这些中间件框架的线程池?相信这是很多小伙伴的疑问。以 Dubbo 举例,当服务高并发调用时,如果 Dubbo 底层线程池没有经过个性化配置,极有可能导致线程池打满,最终导致无法提供服务。

当遇到这种情况,可以使用 Hippo4j 对 Dubbo 线程池进行核心参数调整,避免生产故障时间持续。

再举个例子,当 RocketMQ 消息积压时,可能大部分公司的解决方案是添加客户端应用节点。而这种方式虽然可以解决问题,但是问题也很明显,太复杂且资源浪费。完全可以调整RocketMQ SDK 底层线程池的线程数来达到快速消费的逻辑,有效解决 MQ 消息堆积问题。

目前 Hippo4j 已支持的三方中间件线程池列表:

Apache Dubbo

Alibaba Dubbo

Apache Kafka

Apache RocketMQ

RabbitMQ

SpringCloud Stream RocketMQ

SpringCloud Hystrix

Tomcat

Jetty

Undertow

上述中间件线程池都可以在 Hippo4j 页面上操作核心参数动态变更以及监控功能,如下所示:

项目使用

12306 中有个很好的场景,如果一个用户购买多种类型的车座,比如同一订单购买了一等座和二等座,那么就要按照不同的座位逻辑进行选座。

上述这个流程在我们系统里是串行的,也就是说执行完一等座选座后再执行二等座。这个逻辑是可以并行的,可以极大程度上提高咱们的执行效率,提升系统吞吐量。

// 根据用户购买的座位类型进行分组
Map<Integer, List<PurchaseTicketPassengerDetailDTO>> seatTypeMap = passengerDetails.stream()
        .collect(Collectors.groupingBy(PurchaseTicketPassengerDetailDTO::getSeatType));
// 分配好用户相关的座位信息
List<TrainPurchaseTicketRespDTO> actualResult = new ArrayList<>();
// 串行化执行座位分配流程,比如先执行一等座,再执行二等座...
seatTypeMap.forEach((seatType, passengerSeatDetails) -> {
    SelectSeatDTO selectSeatDTO = SelectSeatDTO.builder()
            .seatType(seatType)
            .passengerSeatDetails(passengerSeatDetails)
            .requestParam(requestParam)
            .build();
    // 座位分配
    List<TrainPurchaseTicketRespDTO> aggregationResult = abstractStrategyChoose.chooseAndExecuteResp(VehicleTypeEnum.findNameByCode(trainType) + VehicleSeatTypeEnum.findNameByCode(seatType), selectSeatDTO);
    if (!Objects.equals(aggregationResult.size(), passengerSeatDetails.size())) {
        throw new ServiceException("站点余票不足,请尝试更换座位类型或选择其它站点");
    }
    // 分配好座位的用户信息添加到返回集合中
    actualResult.addAll(aggregationResult);
});

第二个版本,我们引入线程池去做这个单订单多座位购票场景。

首先引入动态可监控线程池 Hippo4j,通过中间件定义的增强后的动态线程池帮助我们完成并行业务。

private final ThreadPoolExecutor selectSeatThreadPoolExecutor;

// 根据用户购买的座位类型进行分组
Map<Integer, List<PurchaseTicketPassengerDetailDTO>> seatTypeMap = passengerDetails.stream()
        .collect(Collectors.groupingBy(PurchaseTicketPassengerDetailDTO::getSeatType));
// 分配好用户相关的座位信息
List<TrainPurchaseTicketRespDTO> actualResult = new ArrayList<>();
// 串行化执行座位分配流程,比如先执行一等座,再执行二等座...
seatTypeMap.forEach((seatType, passengerSeatDetails) -> {
    selectSeatThreadPoolExecutor.execute(() -> {
        SelectSeatDTO selectSeatDTO = SelectSeatDTO.builder()
                .seatType(seatType)
                .passengerSeatDetails(passengerSeatDetails)
                .requestParam(requestParam)
                .build();
        // 执行策略模式分配座位
        List<TrainPurchaseTicketRespDTO> aggregationResult = abstractStrategyChoose.chooseAndExecuteResp(VehicleTypeEnum.findNameByCode(trainType) + VehicleSeatTypeEnum.findNameByCode(seatType), selectSeatDTO);
        if (!Objects.equals(aggregationResult.size(), passengerSeatDetails.size())) {
            throw new ServiceException("站点余票不足,请尝试更换座位类型或选择其它站点");
        }
        // 分配好座位的用户信息添加到返回集合中
        actualResult.addAll(aggregationResult);
    });
});

虽然第二种解决方案看着是挺好的,但是还有一些并行以及性能的问题,如下:

  1. 如果用户下单仅选择单一座位,通过线程池执行会额外增加性能消耗。
  2. 线程池并行的情况下,向 actualResult 添加数据,会存在并发问题。因为 actualResult 是 ArrayList。

结合上面两个问题,对代码做了相关优化。最终代码如下所示:

private final ThreadPoolExecutor selectSeatThreadPoolExecutor;

// 根据用户购买的座位类型进行分组
Map<Integer, List<PurchaseTicketPassengerDetailDTO>> seatTypeMap = passengerDetails.stream()
        .collect(Collectors.groupingBy(PurchaseTicketPassengerDetailDTO::getSeatType));
// 分配好用户相关的座位信息,容器修改为线程安全的数组集合
List<TrainPurchaseTicketRespDTO> actualResult = Collections.synchronizedList(new ArrayList<>());
// 如果座位类型超过单个并行分配座位
if (seatTypeMap.size() > 1) {
    // 串行化执行座位分配流程,比如先执行一等座,再执行二等座...
    seatTypeMap.forEach((seatType, passengerSeatDetails) -> {
        selectSeatThreadPoolExecutor.execute(() -> {
            SelectSeatDTO selectSeatDTO = SelectSeatDTO.builder()
                    .seatType(seatType)
                    .passengerSeatDetails(passengerSeatDetails)
                    .requestParam(requestParam)
                    .build();
            // 执行策略模式分配座位
            List<TrainPurchaseTicketRespDTO> aggregationResult = abstractStrategyChoose.chooseAndExecuteResp(VehicleTypeEnum.findNameByCode(trainType) + VehicleSeatTypeEnum.findNameByCode(seatType), selectSeatDTO);
            if (!Objects.equals(aggregationResult.size(), passengerSeatDetails.size())) {
                throw new ServiceException("站点余票不足,请尝试更换座位类型或选择其它站点");
            }
            // 分配好座位的用户信息添加到返回集合中
            actualResult.addAll(aggregationResult);
        });
    });
} else {
    // 如果座位类型只有单个,串行执行即可
    seatTypeMap.forEach((seatType, passengerSeatDetails) -> {
        List<TrainPurchaseTicketRespDTO> aggregationResult = distributeSeats(trainType, seatType, requestParam, passengerSeatDetails);
        actualResult.addAll(aggregationResult);
    });
}

上面有个问题是线程池执行是异步的,有可能我们购票流程都结束了,这里还没执行完分配座位逻辑。

为此,我们需要拿到线程池执行的结果后再继续执行后续的提交订单逻辑。

Map<Integer, List<PurchaseTicketPassengerDetailDTO>> seatTypeMap = passengerDetails.stream()
        .collect(Collectors.groupingBy(PurchaseTicketPassengerDetailDTO::getSeatType));
List<TrainPurchaseTicketRespDTO> actualResult = new CopyOnWriteArrayList<>();
if (seatTypeMap.size() > 1) {
    // 添加多线程执行结果类获取 Future,流程执行完成后获取
    List<Future<List<TrainPurchaseTicketRespDTO>>> futureResults = new ArrayList<>();
    seatTypeMap.forEach((seatType, passengerSeatDetails) -> {
        Future<List<TrainPurchaseTicketRespDTO>> completableFuture = selectSeatThreadPoolExecutor
                .submit(() -> distributeSeats(trainType, seatType, requestParam, passengerSeatDetails));
        futureResults.add(completableFuture);
    });
    // 获取座位分配结果
    futureResults.parallelStream().forEach(completableFuture -> {
        try {
            actualResult.addAll(completableFuture.get());
        } catch (Exception e) {
            throw new ServiceException("站点余票不足,请尝试更换座位类型或选择其它站点");
        }
    });
} else {
    seatTypeMap.forEach((seatType, passengerSeatDetails) -> {
        List<TrainPurchaseTicketRespDTO> aggregationResult = distributeSeats(trainType, seatType, requestParam, passengerSeatDetails);
        actualResult.addAll(aggregationResult);
    });
}

为了避免座位分配数量出现问题,我们应该在座位全部分配完成后进行一次验证。

if (CollUtil.isEmpty(actualResult) || !Objects.equals(actualResult.size(), passengerDetails.size())) {
    throw new ServiceException("站点余票不足,请尝试更换座位类型或选择其它站点");
}

  • 20
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值