一次线上关键REST接口调用卡死bug排查

一次线上关键REST接口调用卡死bug排查

背景介绍

本来是可以在文章标题中将bug现象说的更具体一点,但介于聪明TX可能一眼就知道问题所在,相对来说没有了挑战性。问题背景如下 :
微服务架构中,其中的一个提供协议拓扑的微服务组件,在线上运行时,突然无法查询拓扑数据,手动执行拓扑查询,相关接口调动也一直阻塞并且无数据返回。如图所示:
在这里插入图片描述

初步排查

在线上第一次出现该问题时,就进行了常规的日志分析和jstack线程dump操作,命令如下 :

jstack -l pid > a1.txt

当时急于恢复,进行了多次重启,最终才恢复正常。
针对该问题,给出一个大概的定位方向
在这里插入图片描述

组织攻关排查

继续分析现场的线程dump文件,查询拓扑的线程,都阻塞在了同一个地方:
在这里插入图片描述
定位到关键代码如下(忽略不忍直视的代码规范):

    private Set<Link> segmentOperToStore(List<List<Object>> lists) {
        Set<Link> sets = ImmutableSet.of();
        if (lists != null && lists.size() > 0) {
            List<List<List<Object>>> splitLists = QueryUtils.splitListForListList(lists);
            List<CompletableFuture<List<Link>>> domainFeature =
                    splitLists.stream().map(list -> CompletableFuture.supplyAsync(() -> {
                        return list.stream().map(store2OperFuntion).collect(Collectors.toList());
                    })).collect(Collectors.toList());
            List<Link> listsLinks = domainFeature.stream().map(CompletableFuture::join).reduce(new ArrayList<>(),
                    (all, item) -> {
                        all.addAll(item);
                        return all;
                    });
            sets = Sets.newHashSet(listsLinks);
        }
        return sets;
    }

从调用栈看到线程都阻塞在:

domainFeature.stream().map(CompletableFuture::join)

熟悉JAVA8同学应该知道对于JAVA8的parallelStream和CompletableFutrue在不显示指定线程池的前提下,使用是默认的线程池ForkJoin线程,具体名称为:

ForkJoinPool.commonPool-worker-6]

ps.注意如果是自己实例化的ForkJoin对象,其线程名称为:

/**
* 该线程池取代parallelStream默认线程池,大于也等于CPU核心数
* 名称为ForkJoinPool-" + nextPoolId() + "-worker-
*/
static ForkJoinPool forkJoinPool = new ForkJoinPool(CPU_CORE);

分析到这,直观的结论有两类:

  1. ForkJoin.commonPool线程池所有线程被阻塞,导致store2OperFuntion里的代码逻辑无线程可用
  2. store2OperFuntion方法中代码逻辑自身阻塞

但通过走查代码,发现store2OperFunction方法中其并无复杂操作。
此外还有其它各种异常现象,如kafka rebance、消息无法消费等。
此外kafka的主题定阅,也一直无法收到消息和正常处理消息。

乱查一通

虽然前面已经给出正确思路和方向(后来发现),但出于领导先入为主,其主观上认为是拓扑组件本身的问题,直接忽略第一封邮件的判断。即使在第二天的复现排查中,相关同事已经走查到怀疑点,依旧没有重视。
还有同事直接给出了与其无关的结论。这期间经历了kafka服务器问题、CPU性能不够等一系列奇怪结论。
这期间,对上述代码进行了简单的重构,放弃使用默认的forkJoin线程,作为万不得已的修复方案。

        //超过400条链路需要组装,则启用多线程方案
        if (storeLinks.size() > 400) {
            ExecutorService executorService = Executors.newFixedThreadPool(16, UserThreadFactory.build("combine-store-links"));
            List<CompletableFuture<List<Link>>> futures = Lists.partition(storeLinks, 200).stream().map(p ->
                    CompletableFuture.supplyAsync(() -> {
                        return p.stream().map(store2OperFuntion).collect(toList());
                    }, executorService)
            ).collect(toList());
            Set<Link> result = futures.stream().map(p -> {
                try {
                    return p.get();
                } catch (Exception e) {
                    logger.error("error when get result.", e);
                }
                return null;
            }).filter(Objects::nonNull).flatMap(List::stream).filter(Objects::nonNull).collect(toSet());
            executorService.shutdown();
            return result;
        } else {
            for (List<Object> obj : storeLinks) {
                Link link = store2OperFuntion.apply(obj);
                linkSet.add(link);
            }
        }

经过一天的复现过程,在最终将CPU核数改为4核,出现了kafka rebance的问题,导致kafka无法继续拉取消息。结论一度成为:CPU核心数据不够,压力过大

现场再次复现

现场再次出现拓扑无法刷新,无法查询的问题。在申请保留一晚定位时间,再次定位。一顿操作后,在jstack中的ForkJoin线程发现了关键代码线索:
在这里插入图片描述
在这里插入图片描述
在咨询相关开发同事,发现其在远程调用Pcep进行远程下发,存在阻塞的超时等待。至此真相大白,拓扑的查询与kafka消费都是受害者:

ForkJoinPool.commonPool里19个线程都在做远程调度,线程池中线程占满。其它使用了该公共线程的的工作都在等待而无法执行。

总结

1、对于公共线程池的使用,要小心,建议还是自己定义线程池进行。
2、自己定义的线程池,必须命名,且命名规则为query-node-3-thread-3,不能丢失线程池ID号
4、本地线程要用完一定要关闭,全局线程要定义成static

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
Kettle 是一款开源的数据集成工具,支持通过 REST 接口进行调用。它提供了一组 RESTful API,可以用于执行转换(Transformation)和作业(Job)。 要使用 Kettle 的 REST 接口进行调用,您需要了解以下几个关键概念: 1. 转换(Transformation):表示数据处理的流程。它由一系列的步骤(Steps)组成,每个步骤执行特定的数据处理操作。 2. 作业(Job):表示一组转换和其他控制逻辑的组合,用于实现更复杂的工作流程。 3. 转换和作业的定义文件:转换使用 .ktr 文件进行定义,作业使用 .kjb 文件进行定义。 下面是使用 Kettle REST API 进行转换和作业调用的一般步骤: 1. 构建请求:根据 API 文档,构建包含必要参数和请求体的 HTTP 请求。 2. 发送请求:使用 HTTP 客户端库发送请求到 Kettle 的 REST 接口。 3. 处理响应:解析服务器返回的响应,获取需要的信息。 以下是一些常见的 Kettle REST API 调用示例: - 执行转换:使用 POST 请求发送转换定义文件(.ktr 文件)到 `/kettle/executeTrans` 接口。 - 执行作业:使用 POST 请求发送作业定义文件(.kjb 文件)到 `/kettle/executeJob` 接口。 - 获取转换或作业的状态:使用 GET 请求访问 `/kettle/status` 接口,并提供转换或作业的 ID。 - 获取转换或作业的日志:使用 GET 请求访问 `/kettle/log` 接口,并提供转换或作业的 ID。 请注意,具体的 API 调用方式和参数取决于您使用的 Kettle 版本和配置。建议查阅 Kettle 的官方文档或相关资源,以获取更详细的信息和示例代码。 希望这些信息对您有所帮助!如果您有任何进一步的问题,请随时提问。
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值