一次线上关键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

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值