线程池使用不规范,连接数被占满,系统卡顿

本文涉及的产品
日志服务 SLS,月写入数据量 50GB 1个月
应用实时监控服务-可观测链路OpenTelemetry版,每月50GB免费额度
简介: 线程池使用不规范,连接数被占满,系统卡顿

故障现象

问题影响时间:

钉版:09:50-10:30 11:00-12:00 14:30-15:30 16:00-16:30

ng版:09:40-10:00 11:00-11:30 12:00-12:30 14:30-15:00 16:00-16:30

现象:报销单提单卡顿,数据库连接池耗尽。

恢复情况:16:30 全线恢复访问

故障处理过程

09:03 dyna 告警,连接数不够

image.png

image.png


都是dyna的线程数不够,这次没有重启实例。看起来只是线程数用的比较多

不过目前sizeWarn是40%就开始报警,是不是可以试着调高一些。

09:52 运维在群里提供了dyna 服务的dump 信息,并且第二次重启dyna服务。

10:12 dyna 服务恢复正常

image.png


在此之前,我们一直都认为是dyna 服务的卡顿导致系统的卡顿。


10:58 业务告警群出现响应时间超过500ms 的告警

image.png


11:07 研发查看日志发现是xxx-pro有一台响应时间比较长

image.png


11:08出现dt无法连接数据库告警

image.png


11:15 研发查看数据库会话管理,发现有400多条异常会话。研发查看数据库发现有较多慢sql,从慢sql的数量和执行时间来看不是主要的原因

image.png



11:23 研发查看日志,发现是数据库连接池满了

image.png


11:25 查看链路追踪的应用详情,发现有4台服务器响应时间过高

image.png


11:26 运维重启了这些异常的xxx-pro服务,11:35左右dt-prod恢复正常。

在重启之前志成导出了其中一台异常的xxx-pro服务的stack,

12:00 研发查看了stack文件,发现由http、taskExecutor和smartApproveTaskExecutor这几个线程数加起来的合计线程数达到了接近400个,而数据库的线程最大值只设置了100。猜测可能是因为昨天晚上发版的时候临时加了几个线程池引起数据库线程不足的情况,研发把xxx-pro的数据库连接池最大值调成了200,对xxx-prod和xxx-prod的xxxx-pro分别进行部署,于12:40左右部署完成。

下午14:30左右研发查看xxx-prod数据库会话管理,发现异常会话达到了1K多条,先通过手动杀线程的方式处理

image.png


14:43出现了数据库连接池占满的情况

image.png


对早上导出的线程明细进行了分析,发现等待线程中datahubGrpcService的调用占了大部分

image.png


15:00 对这里的异步线程池策略进行了调整,新增了一个线程池,3点左右分别对dt-prod和ng-prod的xxxx-pro进行部署,部署之后异常会话消除。

private final ExecutorService DATAHUB_SERVICE_POOL =

       new ThreadPoolExecutor(5, 20, 30, TimeUnit.SECONDS, new ArrayBlockingQueue<>(50),

           new NamedThreadFactory("datahub-service-pool"), new ThreadPoolExecutor.CallerRunsPolicy());



过了一会,ng 版提单卡顿,通过链路追踪的功能去定位当前耗时比较长的请求

image.png


15:40 通过日志定位,没有发现什么异常,联系运维同学导出当前机器的堆栈信息

image.png

image.png


通过上面观察,xxx-service-pool 有77个线程在等待,而创建的线程池只有70个,线程开始等待,导致系统卡顿。

注释掉submit 方法中,关于xxx.fetchBillDetailByFormDataCode 方法。半小时后,问题解决。

故障原因

datahubServiceImpl:
    public void asyncFetchBillDetail(String entCode, String userCode, String formDataCode) {
        DATAHUB_SERVICE_POOL.execute(()-> dataHubGrpcService.fetchBillDetail(entCode,userCode,formDataCode));
    }
/**
  * 生成账单明细
  * @param entCode 企业code
  * @param userCode 用户code
  * @param formDataCode 报销单code
 */
public void fetchBillDetail(String entCode,String userCode,String formDataCode) {
    logger.info("{}/{} fetchBillDetail with formDataCode {}",entCode,userCode,formDataCode);
    var request =
        DatahubService.BillDetailFetchV2Request.newBuilder()
        .setEntCode(entCode)
        .setOperator(userCode)
        .setFormDataCode(formDataCode)
        .build();
    callGRpcService(servicePrx -> servicePrx.fetchBillDetailByFormDataCode(request));
}
datahub 的grpc 接口实现类
/**
     * 根据报销单号获取账单明细
     * @param request
     * @param responseObserver
     */
@Override
public void fetchBillDetailByFormDataCode(DatahubService.BillDetailFetchV2Request request,
                                          StreamObserver<Empty> responseObserver) {
    try {
        List<DatahubBill> datahubBills =
            billService.listBillByFormDataCode(request.getEntCode(), request.getFormDataCode());
        if(CollectionUtils.isEmpty(datahubBills)){
            return;
        }
        List<String> dhBillCodes = datahubBills.stream().map(DatahubBill::getCode).collect(Collectors.toList());
        billService.fetchBillDetail(request.getEntCode(),request.getOperator(),dhBillCodes);
        responseObserver.onNext(null);
        responseObserver.onCompleted();
    } catch (Exception e) {
        logger.warn("exception when fetchBillDetail {}",e);
        responseObserver.onError(Status.UNKNOWN.withDescription(e.getMessage()).withCause(e).asRuntimeException());
    }
}

问题代码在第13行,如果没有关联月结账单,直接就return,没有关闭grpc 请求的流。

反思与改进方案

代码层面:

  1. 开发的时候没有考虑周全。为了让代码逻辑收敛、和对接口client 端友好,临时把两个接口合并成一个,在合并两个请求的时候按照通常的逻辑,直接return,没有注意到流的关闭。
  2. 自测的时候没有发现问题。在本地写自测用例,没有通过grpc 接口,重现不了。合到uat自己测试了几次,因为有线程池的存在,自己没有复现出来。
  3. 问题的定位分析。之前定位分析问题的时候,把自己的思路局限在了日志、ARMS 还有数据库的健康状态。这个问题涉及到线程的阻塞,很通过上面的方式直接定位到。还需要通过堆栈信息找到根本原因。
  4. 异步调用没有加链路监控,通过链路追踪无法排查

对策:

  1. 在方案设计的时候周全一点,不一定要用线程池做异步,还可以用mq。

 2. taskExecutor 类的滥用,目前系统中有超过100处的使用,后续可以根据具体的场景,判断是否需要使用单独的连接池。

 3. 对于异步调用, 增加日志链路监控。在项目中封装一个tracableTread 类,继承这个类,异步线程也可以实现链路追踪。

 4. grpc 远程调用的设置超时时间,根据服务选择合适的超时时间。修改AbstractBaseGRpcService,用户可以自定义超时的时间。

 5. grpc 服务端,需要统一下grpc服务端的写法规范。这里后续跟基础架构部的同事沟通下,最好是能够封装到service-common 中。

相关文章
|
6月前
|
消息中间件 存储 缓存
【嵌入式软件工程师面经】Linux系统编程(线程进程)
【嵌入式软件工程师面经】Linux系统编程(线程进程)
131 1
|
4月前
|
监控 Java 测试技术
Java并发编程最佳实践:设计高性能的多线程系统
Java并发编程最佳实践:设计高性能的多线程系统
71 1
|
2月前
|
监控 安全 算法
线程死循环确实是多线程编程中的一个常见问题,它可能导致应用程序性能下降,甚至使整个系统变得不稳定。
线程死循环是多线程编程中常见的问题,可能导致性能下降或系统不稳定。通过代码审查、静态分析、日志监控、设置超时、使用锁机制、测试、选择线程安全的数据结构、限制线程数、使用现代并发库及培训,可有效预防和解决死循环问题。
74 1
|
2月前
|
监控 安全 算法
线程死循环是多线程编程中的常见问题,可能导致应用性能下降甚至系统不稳定。
【10月更文挑战第6天】线程死循环是多线程编程中的常见问题,可能导致应用性能下降甚至系统不稳定。为了解决这一问题,可以通过代码审查、静态分析、添加日志监控、设置超时机制、使用锁和同步机制、进行全面测试、选用线程安全的数据结构、限制线程数量、利用现代并发库,并对团队进行培训等方法来预防和减少死循环的发生。尽管如此,多线程编程的复杂性仍需要持续监控和维护以确保系统稳定。
65 3
|
5月前
|
Java Spring
spring boot 中默认最大线程连接数,线程池数配置查看
spring boot 中默认最大线程连接数,线程池数配置查看
387 4
|
5月前
|
算法 Java 编译器
多线程线程安全问题之系统层面的锁优化有哪些常见的策略
多线程线程安全问题之系统层面的锁优化有哪些常见的策略
|
4月前
|
编译器 C语言 iOS开发
iOS 16 系统键盘修复问题之确定_lock是否用于保护对_deferredTasks的多线程读写如何解决
iOS 16 系统键盘修复问题之确定_lock是否用于保护对_deferredTasks的多线程读写如何解决
|
5月前
|
设计模式 存储 安全
Java面试题:设计一个线程安全的单例类并解释其内存占用情况?使用Java多线程工具类实现一个高效的线程池,并解释其背后的原理。结合观察者模式与Java并发框架,设计一个可扩展的事件处理系统
Java面试题:设计一个线程安全的单例类并解释其内存占用情况?使用Java多线程工具类实现一个高效的线程池,并解释其背后的原理。结合观察者模式与Java并发框架,设计一个可扩展的事件处理系统
70 1
|
6月前
|
存储 测试技术
【工作实践(多线程)】十个线程任务生成720w测试数据对系统进行性能测试
【工作实践(多线程)】十个线程任务生成720w测试数据对系统进行性能测试
66 0
【工作实践(多线程)】十个线程任务生成720w测试数据对系统进行性能测试
|
5月前
|
设计模式 存储 缓存
Java面试题:结合设计模式与并发工具包实现高效缓存;多线程与内存管理优化实践;并发框架与设计模式在复杂系统中的应用
Java面试题:结合设计模式与并发工具包实现高效缓存;多线程与内存管理优化实践;并发框架与设计模式在复杂系统中的应用
62 0