一个线上小问题解决过程 —— java线程池使用注意点

表象和场景

最近小伙伴在凌晨0点40分左右会收到某台服务器报警,cpu使用率过高(达到95%),但是不到10分钟使用率降到45%,在之后5分钟内降到10%以内,服务恢复。

背景:公司是用微服务架构,某个产品大约有30多个微服务,前端用阿里云的SLB,架了nginx集群;

为了节省机器成本,某些机器上部署了多个服务,单个服务也部署在多个机器上,互为备份

通过consul来做服务注册与发现

分析

某个机器出现报警cpu过高,过几分钟就不会出现了,有时效性

排查思路:

思路一:根据监控内容查看;(包括这个机器的性能数据,应用服务日志等)

思路二:蹲点出现立马去服务器看现场信息;

思路三:时间节点比较特殊,按照经验,可能会出现的地方思考:半夜出问题周期这么奇怪,很有可能定时任务类没有处理好导致;

根据分析思路找问题

思路一:

根据监控查看机器性能数据,发现这个机器的B服务cpu使用率达到了80%,基本确认是该服务问题;

查看该服务日志,没有任何错误信息,看到接口调用情况中有异样,在接口监控中看到某个对外的接口被调用次数暴增,是平时的100倍+,而且都是A服务调用的,都集中在报警的这段时间内。确认是A服务发起的调用引起的问题;

查看A服务调用的地方,去排查业务,确认最终问题。

思路二:

蹲点出现立马去看现场信息:查看cpu使用情况,查看哪个进程使用率高,查看线程,数量和dump线程内容,具体干的事情,找到结果是B服务的某个对外接口执行线程多,查看日志或根据业务排查场景,得到是A服务调用引起的问题。

思路三:

通过定时任务出发,根据任务调度中心查看配置的定时任务时间,在这个周期之内的任务有哪些,针对具体的任务业务场景去分析,结合日志 和 代码 很快也可以找到问题了。

具体问题说明

   


A服务的部分代码说明:
分页从数据源中获取List<BillPlanPO>,调用如下process方法


    @Async
    public void process(List<BillPlanPO> billPlanPOs) {
        if (CollectionUtils.isEmpty(billPlanPOs)) {
            return;
        }
        billPlanPOs.forEach(item -> {
            calLateFee(item);
        });
    }





    calLateFee 方法内部调用B服务的接口。

1.分页从数据源获取数据速度很快

2.通过异步调用process方法(异步调用线程池没有自己配置,使用springboot框架自带的)

3.每一个billPlanPO去调用B服务的接口;而B服务处理一个的速度较慢。

结论:导致B服务瞬间压力很大,因为A服务获取List<BillPlanPO>速度很快,又是异步线程调用。

找到问题,解决过程就简单多了。

简单介绍下springboot中异步线程池

默认配置:

corePoolSize:表示线程池核心线程,正常情况下开启的线程数量。

queueCapacity:当核心线程都在跑任务,还有多余的任务会存到此处。

maxPoolSize:如果queueCapacity存满了,还有任务就会启动更多的线程,直到线程数达到maxPoolSize。如果还有任务,则根据拒绝策略进行处理。

针对如上问题,添加了下面代码:


import java.util.concurrent.ThreadPoolExecutor;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.task.AsyncTaskExecutor;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;

/**
 *  配置线程池
 */
@EnableAsync
@Configuration
public class ThreadPoolsConfig {

    /**
     * 线程相关参数
     */
    @Value("${pay.threadNamePrefix}")
    private String threadNamePrefix;    // 配置线程池中的线程名称前缀

    @Value("${pay.corePoolSize}")
    private Integer corePoolSize;       // 配置线程池中的核心线程数

    @Value("${pay.maxPoolSize}")
    private Integer maxPoolSize;        // 配置最大线程数

    @Value("${pay.queueCapacity}")
    private Integer queueCapacity;      // 配置队列大小

    /**
     * 线程池配置
     */
    @Bean
    public AsyncTaskExecutor paymentTaskExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setThreadNamePrefix(threadNamePrefix);
        executor.setCorePoolSize(corePoolSize);
        executor.setMaxPoolSize(maxPoolSize);
        executor.setQueueCapacity(queueCapacity);
      /**
       * 当任务数量超过MaxPoolSize和QueueCapacity时使用的策略,该策略是调用任务的线程执行
       */
        executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
        return executor;
    }

}

另外几个细节注意点:

a.corePoolSize数量配置

 /**
     * IO密集型任务  = 一般为2*CPU核心数(常出现于线程中:数据库数据交互、文件上传下载、网络数据传输等等)
     * CPU密集型任务 = 一般为CPU核心数+1(常出现于线程中:复杂算法)
     * 混合型任务  = 视机器配置和复杂度自测而定
     */

private static int corePoolSize = Runtime.getRuntime().availableProcessors();

b.当队列满了之后的处理,setRejectedExecutionHandler,根据场景看需要怎么样的拒绝策略

ThreadPoolExecutor自身提供了4种策略
1.CallerRunsPolicy :这个策略重试添加当前的任务,他会自动重复调用 execute() 方法,直到成功。
2.AbortPolicy :对拒绝任务抛弃处理,并且抛出异常。
3.DiscardPolicy :对拒绝任务直接无声抛弃,没有异常信息。
4.DiscardOldestPolicy :对拒绝任务不抛弃,而是抛弃队列里面等待最久的一个线程,然后把拒绝任务加到队列。

扯远了,打住,根据调整,测试环境走一波。

验证与观察

通过各种工具压一波,问题已解决,根据资源情况配置合适的线程池参数。

发布后,线上验证通过,晚上可以安静的睡觉了~

其他建议

大部分线上问题,都是一个很小的点引起的,排查是一个反向推导的过程,这个过程往往比理解原因或者解决问题复杂。

问题排查是复杂的,不可控的,所以不要把排查和解决混在一起,尽量先解决、再排查。解决的方式基本上都是那么几板斧:重启、回滚、扩容、降级、迁移等。

系统要尽可能的对外暴露内部状态和干预手段,比如说少打了一句日志,没把变量输出出来,那么出现问题的时候很不但要用某些复杂的工具去查询变量,而且很有可能要多绕一个大圈。

系统是不稳定的,所以对于高可用架构设计来说,隔离是必须的,不管是何种依赖方式,都需要考虑“实在不行了”的情况。

问题的原因、传播路径和现象不是一一对应的。同一个问题,这次的表现是多打了一行WARN日志,下次可能就是一次系统雪崩。墨菲定律,如果有可能出问题,那一定会出问题。

提前准备

a.监控

b.日志(分维度过滤和查询),集中检索

c.保存现场

d.报警机制,(机器与服务,业务层数据)

这个4点可以单独写很多内容,这里只是备注下,出了问题才会想到这些内容何其重要,所以平时要更加积累去处理。

排查问题不是一个人的事情,最好拉个消防群,把各个岗位的人员一起处理(开发,运维,DBA,系统,硬件等),大家工作的细分也是有道理的,专业的人干专业的事情。问题一定要先解决再排查,问题一定要先解决再排查,问题一定要先解决再排查~

猜你喜欢

转载自blog.csdn.net/lsblsb/article/details/82840197