记一次JSF异步调用引起的接口可用率降低

news2024/11/17 13:19:53

前言

本文记录了由于JSF异步调用超时引起的接口可用率降低问题的排查过程,主要介绍了排查思路和JSF异步调用的流程,希望可以帮助大家了解JSF的异步调用原理以及提供一些问题排查思路。本文分析的JSF源码是基于JSF 1,7.5-HOTFIX-T6版本。

起因

问题背景

1.广告投放系统是典型的I/O密集型(I/O Bound)服务,系统中某些接口单次操作可能依赖十几个外部接口,导致接口耗时较长,严重影响用户体验,因此需要将这些外部调用切换为异步模式,通过并发的模式降低整体耗时,提高接口的响应速度。

2.在同步调用的场景下,接口耗时长、性能差,接口响应时间长。这时为了缩短接口的响应时间,一般会使用线程池的方式并行获取数据,但是如果使用线程池来做,不同业务需要不同的线程池,最后会导致难以维护,随着CPU调度线程数的增加,会导致更严重的资源争用,宝贵的CPU资源被损耗在上下文切换上,而且线程本身也会占用系统资源,且不能无限增加。

3.通过阅读JSF的文档发现JSF是支持异步调用模式的,既然中间件已经支持这个功能,所以我们就采用了JSF提供的异步调用模式,目前JSF支持三种异步调用方式,分别是ResponseFuture方式、CompletableFuture方式和定义返回值为 CompletableFuture 的接口签名方式。

(1)RpcContext中获取ResponseFuture方式

该方式需要先将Consumer端的async属性设置为true,代表开启异步调用,然后在调用Provider的地方使用RpcContext.getContext().getFuture()方法获取一个ResponseFuture,拿到Future以后就可以使用get方法去阻塞等待返回,但是这种方式已经不推荐使用了,因为第二种CompletableFuture的模式更加强大。

代码示例:

asyncHelloService.sayHello("The ResponseFuture One");
ResponseFuture<Object> future1 = RpcContext.getContext().getFuture();
asyncHelloService.sayNoting("The ResponseFuture Two");
ResponseFuture<Object> future2 = RpcContext.getContext().getFuture();
try {
     future1.get();
     future2.get();
} catch (Throwable e) {
    LOGGER.error("catch " + e.getClass().getCanonicalName() + " " + e.getMessage(), e);
}

(2)RpcContext中获取CompletableFuture方式(1.7.5及以上版本支持)

该方式需要先将Consumer端的async属性设置为true,代表开启异步调用,然后在调用Provider的地方使用RpcContext.getContext().getCompletableFuture()方法获取到一个CompletableFuture进行后续操作。CompletableFuture对Future进行了扩展,可以通过设置回调的方式处理计算结果,支持组合操作,也支持进一步的编排,一定程度解决了回调地狱的问题。

代码示例:

asyncHelloService.sayHello("The CompletableFuture One");
CompletableFuture<String> cf1 = RpcContext.getContext().getCompletableFuture();
asyncHelloService.sayNoting("The CompletableFuture Two");
CompletableFuture<String> cf2 = RpcContext.getContext().getCompletableFuture();

CompletableFuture<String> cf3 = RpcContext.getContext().asyncCall(() -> {
    asyncHelloService.sayHello("The CompletableFuture Three");
});
try {
    cf1.get();
    cf2.get();
    cf3.get();
} catch (Throwable e) {
    LOGGER.error("catch " + e.getClass().getCanonicalName() + " " + e.getMessage(), e);
}

(3)使用 CompletableFuture 签名的接口(1.7.5及以上版本支持)

这种模式需要改造代码,需要服务的提供者事先定义方法的返回值签名为CompletableFuture,这种调用端无需配置即可使用异步。

代码示例:

CompletableFuture<String> cf4 = asyncHelloService.sayHelloAsync("The CompletableFuture Fore");
cf4.whenComplete((res, err) -> {
    if (err != null) {
        LOGGER.error("interface async cf4 now complete error " + err.getClass().getCanonicalName() + " " + err.getMessage(), err);
    } else {
        LOGGER.info("interface async cf4 now complete : {}", res);
    }
});
CompletableFuture<Void> cf5 = asyncHelloService.sayNotingAsync("The CompletableFuture Five");

try {
    LOGGER.info("interface async cf1 now is : {}", cf4.get());
    LOGGER.info("interface async cf2 now is : {}", cf5.get());
} catch (Throwable e) {
    LOGGER.error("catch " + e.getClass().getCanonicalName() + " " + e.getMessage(), e);
}

通过对已上三种异步调用模式的分析,第三种需要提供者修改方法签名支持异步,难以实现;本着改动最小化,API使用最优化,我们最终选择了第二种方式,即在调用端设置async属性为true,同时在发起调用后从RpcContext中获取一个CompletableFuture对象进行后续的操作。

问题现象

经过异步模式改造,部分依赖很多外部服务的接口耗时有明显的下降,表面看系统一片祥和,但是偶尔的接口可用率降低却是一个非常危险的信号,下面是使用异步调用的某个接口的可用率监控

通过监控我们可以发现,这个接口偶尔会出现可用率降低,一般接口可用率降低可能是因为超时或者触发了某些隐藏问题导致,但是这个接口的逻辑非常简单,就是根据id查询数据库,业务逻辑非常简单,理论上不应该出现这么多可用率降低的情况。我们通过日志排查发现在异步调用使用CompletableFuture的get方法阻塞等待的时候发生了TimeOutException异常,目前接口配置的超时时间为5s,本来接口超时是一个我们经常遇见的问题,但是我们去提供者端查询日志发现,本次请求只耗费了几毫秒,明明提供者端几毫秒或者几十毫秒就返回了,为什么消费端还超时了,带着这个疑问我们继续分析,会不会是JSF异步的原因导致的。

排查定位原因

通过阅读JSF的源码,我们了解到JSF异步调用的基本流程为客户端向服务端发送请求前,会先判断本次请求是否需要走异步调用,如果需要的话,会生成一个JSFCompletableFuture对象 这个类是继承自CompletableFuture的,同时使用一个futureMap对象缓存了请求的唯一msgId和一个MsgFuture对象,MsgFuture对象里面持有了本次调用使用的channel、message、timeout、compatibleFuture等属性,方便服务端回调后,可以通过msgId找到对应的MsgFuture对象做后续处理。

首先在doSendAsyn方法里生成MsgId和MsgFuture对象的映射,然后序列化数据,最后通过netty的长连接向channel里面写入要发送的数据。

(1)生成JSFCompletableFuture

(2)维护msgId和MsgFuture的关系

(3) 维护msgId和MsgFuture的关系

(4)发起调用

服务端收到请求后,会触发服务端的ServerChannelHandler类的channelRead方法被回调,这个方法里面会验证序列化协议,然后生成一个JSFTask的任务,将这个任务提交到JSF的业务线程池去执行,等业务线程池里的任务执行完成以后,会调用write方法将返回值通过channel写回客户端。

(1)服务端收到响应处理

(2)服务端回写响应

客户端收到响应后,会触发客户端的ClientChannelHandler类的channelRead方法,这个方法里面会通过服务端返回的msgId找到客户端缓存的MsgFuture对象,然后会判断对象内的compatibleFuture属性是不是非空,如果非空,会往Callback线程池内提交一个任务,这个任务的主要功能是执行CompletableFuture的completeExceptionally和complete方法,用于触发CompletableFuture的下一阶段执行。

(1)客户端收到响应

(2)找到本地的MsgFuture

(3)将MsgFuture添加到线程池

(4) 触发CompletableFuture的complete或者completeExceptionally方法

通过对已上源码的分析,我们虽然知道了JSF异步调用的全部流程,但是还是无法解释为什么偶尔会出现不应该超时的超时(此处指服务端明明没有超时,客户端还显示超时了),通过对各个流程的排除,我们最终定位到可能和JSF异步回调后将任务添加到Callback线程池去执行CompletableFuture的complete方法有关,因为这个方法会继续执行CompletableFuture后续的阶段,我们业务代码在拿到RpcContext里面返回的CompletableFuture对象以后,一般会使用CompletableFuture的一元依赖方法ThenApply去执行一些后续处理,CompletableFuture的complete方法就是用来触发这些后续阶段去执行的。

异步调用业务代码:

下面介绍一下CompletableFuture的基础知识,每个CompletableFuture都可以被看作一个被观察者,其内部有一个Completion类型的链表成员变量stack,用来存储注册到其中的所有观察者。当被观察者执行完成后会弹栈stack属性,依次通知注册到其中的观察者,所以在这个阶段会去调用我们程序中的ThenApply方法,下图是CompletableFuture内部的关键属性。

图12 thenApply简图

如果上面的异步调用流程感觉不清晰,可以看下面的一张调用关系图

通过查看Callack线程池的默认配置,发现他的核心线程数为20,队列长度256,最大线程数200。看到这我们猜测可能是核心线程数不够用,导致一些回调任务积压在队列中没来得及执行导致了超时。由于无法通过其他方式获取当时CallBack线程池的运行状态,因此我们通过修改业务代码,在发生超时异常的时候获取Callback线程池当前的状态来验证我们的猜测。

(1)获取线程池状态代码

修改完代码上线后,系统运行一段时间出现了接口可用率降低的现象,接着我们查询日志,从日志里可以看出,在发生超时异常的时候,JSF的Callback线程池核心线程数已满,同时队列中积压了71个任务,通过这个日志就可以确定是因为JSF 回调线程池核心线程数满导致任务排队出现的超时

问题分析

1、通过上面的日志我们知道是因为异步线程池满导致的,理论上正常请求就算有些排队应该也会很快就能处理掉,但是我们排查业务代码后发现,我们有些业务在ThenApply里面做了一些耗时的操作、还有在ThenApply里面又调用了另外一个异步方法。

2、第一种情况会导致线程池的线程会被一直占用,其他任务都会在排队,这种其实还是能接受的,但是第二种情况可能会出现线程池循环引用导致死锁,原因是父任务会将异步回调放在线程池执行,父任务的子任务也会将异步回调放在线程池执行,Callback线程池核心线程大小为20,当同一时刻有20个请求到达,则Callback core thread被打满,子任务请求线程时进入阻塞队列排队,但是父任务的完成又依赖于子任务,这时由于子任务得不到线程,父任务无法完成,主线程执行get进入阻塞状态,并且永远无法恢复。

解决方案

**短期方案:**因为线程池核心线程满导致排队,所以将JSF 的回调线程池核心线程数从20调整为200,

**长期方案:**优化代码将ThenApply里面耗时的操作不放在回调线程池执行,同时优化代码逻辑,将在ThenApply方法内部再次开启异步调用的流程去除。

调整完前后的对比:

通过查看监控可以发现,优化后接口可用率一直保持在100%。

作者:京东零售 宋维飞

来源:京东云开发者社区 转载请注明来源

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1368347.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

强化学习的数学原理学习笔记 - Actor-Critic

文章目录 概览&#xff1a;RL方法分类Actor-CriticBasic actor-critic / QAC&#x1f7e6;A2C (Advantage actor-critic)Off-policy AC&#x1f7e1;重要性采样&#xff08;Importance Sampling&#xff09;Off-policy PGOff-policy AC &#x1f7e6;DPG (Deterministic AC) 本…

使用fs.renameSync(oldPath,newPath)方法,报错Error: ENOENT: no such file or directory

报错翻译&#xff1a;由于文件或目录不存在导致的。 解决方法&#xff1a;查看给定的路径&#xff0c;确保路径和文件名正确&#xff0c;并且文件或目录确实存在。

C语言--结构体详解

C语言--结构体详解 1.结构体产生原因2.结构体声明2.1 结构体的声明2.2 结构体的初始化2.3结构体自引用 3.结构体内存对齐3.1 对齐规则3.2 为什么存在内存对齐3.3 修改默认对⻬数 4. 结构体传参 1.结构体产生原因 C语言将数据类型分为了两种&#xff0c;一种是内置类型&#xf…

Spring学习 Spring事务控制

7.1.事务介绍 7.1.1.什么是事务&#xff1f; 当你需要一次执行多条SQL语句时&#xff0c;可以使用事务。通俗一点说&#xff0c;如果这几条SQL语句全部执行成功&#xff0c;则才对数据库进行一次更新&#xff0c;如果有一条SQL语句执行失败&#xff0c;则这几条SQL语句全部不…

2.SPSS数据文件的建立和管理

文章目录 数据文件的特点建立SPSS数据文件步骤 数据文件的结构变量的规则 数据的录入和保存录入数据保存文件 数据的编辑数据定位 数据文件的特点 SPSS数据库文件包括文件结构和数据两部分 SPSS数据文件中的一列数据称为一个变量。每个变量都应有一个名称&#xff0c;即&…

面试算法100:三角形中最小路径之和

题目 在一个由数字组成的三角形中&#xff0c;第1行有1个数字&#xff0c;第2行有2个数字&#xff0c;以此类推&#xff0c;第n行有n个数字。例如&#xff0c;下图是一个包含4行数字的三角形。如果每步只能前往下一行中相邻的数字&#xff0c;请计算从三角形顶部到底部的路径经…

centos7新建普通用户并设置分组和密码

sudo -i获取root权限 添加分组group1 groupadd group1 添加用户并设置分组为group1密码为password1 useradd user1 -g group1 -p password1 su user1 切换到 user1

第7章-第6节-Java中的Map集合

1、HashMap&#xff1a; 1&#xff09;、 引入 如果业务需要我们去用姓名的拼音手写字母匹配完整姓名&#xff0c;那么如果用单列数据&#xff0c;我们可能需要两个集合才能存储&#xff0c;而且两个集合之间没有关联不好操作&#xff0c;这种时候双列数据就会起很大作用 2&…

Mysql : command not found

1.Mysql : command not found 安装成功的mysql&#xff0c;并且服务已经启动&#xff0c;查看进行是可以看到的&#xff0c;但是使用命令登录操作&#xff0c;却抛出错误&#xff1a;command not found。 2.解决方案 2.1 查看/usr/bin目录下是否有mysql服务连接 ls /usr/bin…

服务网格 Service Mesh

什么是服务网格&#xff1f; 服务网格是一个软件层&#xff0c;用于处理应用程序中服务之间的所有通信。该层由容器化微服务组成。随着应用程序的扩展和微服务数量的增加&#xff0c;监控服务的性能变得越来越困难。为了管理服务之间的连接&#xff0c;服务网格提供了监控、记…

Linux 期末复习

Linux 期末复习 计算机历史 硬件基础 1&#xff0c;计算机硬件的五大部件&#xff1a;控制器、运算器、存储器、输入输出设备 2&#xff0c;cpu分为精简指令集(RISC)和复杂指令集(CISC) 3&#xff0c;硬件只认识0和1&#xff0c;最小单位是bit&#xff0c;最小存储单位是字…

应用统计学期末复习简答题

应用统计学期末复习简答题 1&#xff0e;解释众数、中位数和调和平均数。2、什么是普查&#xff1f;其有何特点和作用&#xff1f;3、什么是抽样调查&#xff1f;其有何特点和作用&#xff1f;4、什么是偏度系数&#xff0c;并解释左偏分布、对称分布和右偏分布。5、解释置信区…

竞赛保研 基于深度学习的人脸表情识别

文章目录 0 前言1 技术介绍1.1 技术概括1.2 目前表情识别实现技术 2 实现效果3 深度学习表情识别实现过程3.1 网络架构3.2 数据3.3 实现流程3.4 部分实现代码 4 最后 0 前言 &#x1f525; 优质竞赛项目系列&#xff0c;今天要分享的是 基于深度学习的人脸表情识别 该项目较…

结构型设计模式——适配器模式

适配器模式 这个更加好理解&#xff0c;就是做适配功能的类&#xff0c;例如&#xff0c;现在手机没有了圆形耳机接口&#xff0c;只有Type-C接口&#xff0c;因此你如果还想要使用圆形耳机的话需要买个圆形接口转Type-C的转换器&#xff08;适配器&#xff09;&#xff0c;这…

一分钟带你了解深度学习算法

深度学习是一种受到生物学启发的机器学习方法&#xff0c;其目标是通过构建多层神经网络来模拟人脑的工作原理。它在过去几十年来取得了巨大的进展&#xff0c;并在图像识别、语音识别、自然语言处理等领域取得了突破性的成果。 深度学习的核心思想是模仿人脑的神经网络。人脑中…

HarmonyOS应用开发学习笔记 Want概述Ability跳转

一、Want的定义与用途 Want是对象间信息传递的载体&#xff0c;可以用于应用组件间的信息传递。其使用场景之一是作为startAbility()的参数&#xff0c;包含了指定的启动目标以及启动时需携带的相关数据&#xff0c;如bundleName和abilityName字段分别指明目标Ability所在应用…

vue element ui table表格--实现列的显示与隐藏

前言 实现效果 提示&#xff1a;代码段太简单就不解释了&#xff0c;自己看代码自己更改&#xff0c;下面代码直接无脑复制更改就行 一、实现代码&#xff1f; <template><div id"app"><el-table :data"tableData" border style"w…

Spring 见解 7 基于注解的AOP控制事务

8.基于注解的AOP控制事务 8.1.拷贝上一章代码 8.2.applicationContext.xml <!-- 开启spring对注解事务的支持 --> <tx:annotation-driven transaction-manager"transactionManager"/> 8.3.service Service Transactional(readOnlytrue,propagation Pr…

学生用台灯对眼睛好吗?五款考研党台灯推荐

对于一名深耕家电多年的测评师&#xff0c;对于各品牌的台灯产品测评做了不少&#xff0c;本期我将给大家带来一期超全的热门护眼台灯深度测评。 据世界卫生组织最新研究报告称&#xff0c;我国近视患者人数多达6亿左右&#xff0c;几乎占据了中国总人口数量的一半&#xff0c;…

进程管理及计划任务

一、定义 程序&#xff1a;硬盘上躺着。执行特定任务的一串代码 进程&#xff1a;加载到内存中运行。进程是程序的副本&#xff0c;进程是有生命周期 进程的控制&#xff1a;每开启一个进程会消耗相应的硬件资源&#xff0c;内存&#xff0c;CPU&#xff0c;磁盘io 内存不足&am…