我们经常在系统调试或者线上告警,都会涉及"接口超时",是一个线上系统即为正常的一种现象.
随便在线上搜索一下超时日志,就发现5分钟内就存在3000多条超时日志,但是接口超时的本质到底是什么,如何处理接口超时,如何快速定位接口超时的原因,还是较为困难的,今天就系统性的来分析一下"超时"。
一、超时的定义
首先引入百度百科超时的概念:
超时即当网络设备想在某个特定时间内从另一网络设备上接收信息,但是失败的情况。
从百科就可以了解到,超时的主体是"网络设备",所以我们一般讨论「超时」一般是只网络设备发生了调用异常,而实际上非网络设备,比如在同一台单体机器的内部方法调用也可能超时,比如Java的一个线程在等待另外一个线程的结果,超过了超时时间。
对于单体程序而言,这种超时概率比较小,并且由于在同一个程序或者虚拟机上,很容易就能确定其超时的具体原因,所以我们一般不把这一类超时现象称之为狭义的超时。而针对网络设备来说,大部分时间都是需要完成RPC(注RPC为远程调用)调用来实现其业务逻辑,而网络自身相对来说依赖了各种外部软硬件,诸如运营商网络、路由节点、异构的机器等,存在大量不可控的因素,同时由于RPC调用一般是长距离,比如横跨地区的机房调用,有大量的信号传输的RT消耗,综合看来,就导致了网络设备获取服务的时候存在极大的不确定性,而里面最大的不确定性就是"超时"。百度百科上的定义我觉得是不准确的,因为超时是一种失败,但是不是所有的失败都是超时。而相对来说wiki百科上就更为准确:
css
(communication) The intentional termination of an incomplete task after a time limit considered long enough for it to end normally
可以看到,wiki百科上描述的更为精确,即"超过了特定的时间后任务没有完成被中止"。实际上超时很大的一个特点,就是超过了指定的时间后,接口依然没有返回而导致的失败。
当然,为了缩小讨论范围,结合我们日常大部分都是编写和治理Web应用,我们今天就重点分析一下RPC接口的"超时"。
二、超时的本质
首先我们分析一下一个典型的RPC调用的系统图,如下所示,我们在讨论RPC调用的时候,抽象和精简一下,一定存在两个角色,客户端和服务端:
- 客户端:发起请求的一方并接受响应
- 服务端:提供服务的一方,接受请求并返回
其中我们经常所谈的RT,也就是请求耗时,计算规则如下:
ini
RT = 请求过程耗时 + 处理耗时 + 响应过程耗时
大部分情况下,处理耗时根据业务情况变化比较大,快则1毫秒,慢则几千毫秒,而请求和响应由于要走网络,固定都是至少10~100毫秒起步,其具体的值根据距离变化,比如从北京到深圳大概RT在70毫秒左右,而北京到纽约RT要150毫秒以上。
从上面的RT过程可以知道,超时可以发生在三个阶段,但是一般来说网络的耗时是不可控的,我们无法控制网络的异常导致的超时,一般都是运营商发生了异常,而业务处理时间的耗时是我们可以控制、优化和治理的。
三、超时的业务场景
先提一个问题,超时的设定都是系统固定的吗?比如HTTP的超时,比如RPC框架默认的超时。答案是否定的,一般的RPC协议在设计时就一定考虑到了超时的因素。因为协议的设计就是为了双方能够顺利"交互",而超时势必就导致"交互"中断,因此RPC协议就会对"超时"有协议上的设计。
但是,除了标准之外,业务上也要充分考虑到对"超时"的要求,不同的业务对于"超时"的容忍度是不同的,比如利用HTTP协议来实现的RPC调用,客户一般在3秒内就无法接受没有任何的返回,所以尽管HTTP的超时可以更长,但是实际上业务上的要求一般是3秒内。
还有一些复杂的业务调用,期间有一些弱依赖,比如钉钉工作台页面在加载后需要去拉一些营销类的应用数据,尽管默认的RPC超时时间是3秒,但是由于核心功能不能等待营销类的3S调用结束才返回给客户,因此我们在对营销类调用设置的超时时间只有100MS,以避免非核心依赖的RT过长影响到正常的调用体验。
从想象来说,我们一般是从客户端的角度来定义"超时",而从过程里,我们可以从请求、处理和响应三个阶段来衡量耗时。最后,我们从调用方和响应方的角度,也可以定义更具体的"超时"。
四、超时设置
客户端超时设置
先从最简单的HTTP协议开始,HTTP除了浏览器在使用之外,我们也可以通过HttpClient发起调用,可以通过设置客户端的超时时间来实现超时控制。
arduino
private static HttpClient getHttpClientWithTimeout(int seconds) {
return HttpClient.newBuilder()
.connectTimeout(Duration.ofSeconds(10))
.build();
}
如上所示,就是初始化了一个请求,超时时间是10S,10秒后服务端没有返回的话,HttpClient代理就会直接返回TimeOut错误。
除了HTTP外,我们以常见的RPC协议来进一步分析,我们一般认为超时设置是从客户端角度出发的,比如一般的RPC诸如Dubbo默认推荐3S超时时间,也就是说,如果从调用发起请求开始3秒内没有收到响应,则认为该RPC调用失败,框架层抛出TimeOutException异常供上层处理。就用阿里的RPC框架HSF来说,可以通过注解和配置的方式来约定调用方接受超时的设置,客户端超时的链路图如下所示:
我们以HSF配置的超时为例,来解释一下如何设置客户端超时时间。
- HSF泛化超时配置
如下图所示,可以利用hsfApiConsumerBean.setClientTimeout(timeout)的方式来设置客户端对于API的超时时间
scss
HSFApiConsumerBean hsfApiConsumerBean = new HSFApiConsumerBean();
// 接口名
hsfApiConsumerBean.setInterfaceName("com.alibaba.test.TestService");
// 版本号
hsfApiConsumerBean.setVersion("1.0.0");
// 分组
hsfApiConsumerBean.setGroup("HSF");
// 设置泛化配置
hsfApiConsumerBean.setGeneric("true");
// 设置超时时间(不推荐,不能动态修改)
hsfApiConsumerBean.setClientTimeout(timeout);
// 初始化
hsfApiConsumerBean.init(true);
- HSF注解超时配置
如下图所示,@HSFConsumer(clientTimeout = 60000)这个注解就表示了客户端对该API的超时时间是60S,也就是最长等待60S依然没有响应会认为是超时。
ini
/**
* 算法团队提供的AI服务,由于AIGC性能比较慢,因此超时时间设置成60S
*/
@HSFConsumer(clientTimeout = 60000)
private DingWorkTabAIgcToFrameService dingWorkTabAIgcToFrameService;
- HSF的XML超时配置
其中${lippi.foodandbeverage.service.provider.timeout}就是API超时的配置时间
ini
<bean class="com.taobao.hsf.app.spring.util.HSFSpringProviderBean" init-method="init">
<property name="serviceInterface"
value="com.dingtalk.foodandbeverage.custom.service.DingPortalFacadeService"/>
<property name="serviceName" value="dingPortalFacadeService"/>
<property name="target" ref="dingPortalFacadeService"/>
<property name="serviceVersion" value="${lippi.foodandbeverage.service.provider.version}"/>
<property name="clientTimeout" value="${lippi.foodandbeverage.service.provider.timeout}"/>
</bean>
服务端超时设置
除了客户端外,但是大家往往忽略了还可以从服务端角度的超时设置。对于一般的RPC来说,服务端也是有个代理来对来自移动端网络请求的转发,因此也可以配置超时时间,即当本服务从接收到请求到处理完之后如果已经超过了超时时间就可以直接客户端返回超时错误了。
对于HSF的服务端设置超时时间的配置如下所示:
less
@Slf4j(topic = "HSF_BIZ_LOGGER")
@HSFProvider(serviceInterface = WorkbenchAiService.class, clientTimeout = 10000)
如配置所示,服务端设置了统一10S的超时时间,所以客户端来调用这个API的超时时间都是10S,如果客户端有更短的超时时间,则客户端的RPC代理会先返回超时错误而丢弃服务端的超时错误。客户端和服务端都设置了超时时间,最终现象是以最短的时间作为本次请求的超时时间。
HSF框架都提供了默认的超时请求时间,为什么还需要服务端设置超时时间呢?这个是跟业务相关,比如某些对外提供可降级的缓存服务,如果都采用默认时间则可能会导致雪崩效应,为了保护服务端的安全稳定,可以统一设置一个更短的时间,当服务端无法处理请求的时候就直接用超时的方式返回,避免堆积大量耗时的处理任务,进而保护服务端的稳定性和高可用。
五、超时处理
如果客户端发起请求超时了怎么办?也就是说客户端需要查询某个RPC的API,但是在指定时间内没有返回结果怎么处理?可以分成两个层面来思考,底层是RPC框架的实现,上次业务通过RPC内的代理发起网络请求,RPC内的代理逻辑具备超时设置功能,也就是RPC首先感知到请求超时了,这次RPC框架会处理超时异常,一般来说,RPC框架会向上层调用方抛一个超时异常,我们这里统一称为TimeOutException。以阿里集团的RPC框架HSF为例,HSFTimeOutException的继承关系如下图所示:
右图的继承关系可知,HSFTimeOutException继承是RuntimeException,RuntimeException的特点简单说就是无需强制捕获,也就是在写业务代码来调用RPC的时候,不用显示得捕获HSFTimeOutException异常。一般来说,业务逻辑代码本身不用处理HSFTimeOutException,当RPC接口发生HSFTimeOutException异常时会往上抛,调用侧业务代码也会继续往上抛,直接会一路往调用栈上抛,最后一般由最外层的服务统一封装; |
---|
六、超时排查
对于接口超时来说,造成的现象就是上层功能不可用,根据业务的处理情况不同显示不同的逻辑,诸如"接口超时"、"系统繁忙";对于系统监控来说,就会存在大量的TimeOutException告警、成功率下跌等。一般来说目前的RPC框架在接口发生超时时都会打印出超时的API、堆栈和traceId,可以根据这些信息快速定位到是哪个API发生了超时,诸如阿里巴巴的HSF框架在发生超时后还会把耗时链路也打印出来,可以很方便的看到服务提供方哪部分立即发生了超时。比如下面这一例就是线上一个接口超时后打印的日志:
ini
33.63.7.14,lippi-workbench033063007014.center.na620,4F1E0FB17AD560A2-18B,/home/admin/logs/hsf/hsf.log,1697613740,lippi-workbenchhost,1697613740,,"01 2023-10-18 15:22:20.153 WARN [HSFBizProcessor-DEFAULT-8-thread-44:t.hsf] [lippi-workbench] [] [] [HSF-Provider] execute service com.dingtalk.workbench.service.WorkbenchFacadeService:1.0.0#searchWorkbenchApp~S cost 3043 ms, this invocation almost (maybe already) timeout
client: 33.63.197.64
invocation context:
{_eagleeye_rpc_id=0.15, _eagleeye_trace_id=213f259516976137370446986d050f, _stream=[L:/33.63.7.14:12200 - R:/33.63.197.64:59282][active], _server_invocation_eagleeye_context=com.taobao.eagleeye.RpcContext_inner@44308a37, com.taobao.hsf.plugins.eagleeye.EagleEyeServerFilter_executed=true, _get_through=true}
thread info:
0 [3,039ms (15ms), 100%] - receive request, service invoke begin.
+---8 [8ms, 0%, 0%] - invoke service : com.dingtalk.org.service.OrgService:1.0.0#getOrgInfo(java.lang.Long)
+---16 [5ms, 0%, 0%] - invoke service : com.dingtalk.org.service.OrgEmpService:1.0.0#getEmpInfoByUid(java.lang.Long,java.lang.Long)
+---21 [19ms, 1%, 1%] - invoke service : com.dingtalk.org.service.OrgDeptService:1.0.0#getSuperDeptsIdsByUid(java.lang.Long,java.lang.Long)
+---40 [21ms, 1%, 1%] - invoke service : com.dingtalk.org.service.OrgAdminService:1.0.0#getOrgEmpRole(java.lang.Long,java.lang.Long)
+---64 [1,422ms, 47%, 47%] - invoke service : com.dingtalk.search.service.OpenSearchService:1.0.0#searchWorkBenchApp(com.dingtalk.search.model.WorkBenchAppSearchParam)
+---1,486 [1,472ms, 48%, 48%] - invoke service : com.dingtalk.search.service.OpenSearchService:1.0.0#searchWorkBenchApp(com.dingtalk.search.model.WorkBenchAppSearchParam)
+---2,958 [36ms, 1%, 1%] - invoke service : com.dingtalk.microapp.service.OrgMicroAppService:1.0.0#getAllAppExcludeSenderByOrgIdForOrg(java.lang.Long)
+---2,994 [35ms, 1%, 1%] - invoke service : com.dingtalk.foodandbeverage.service.ShortcutFacadeService:1.0.0#listShortcutForUser(com.dingtalk.foodandbeverage.model.ShortcutVisibleEffectsDTO)
`---3,029 [6ms, 0%, 0%] - invoke service : com.dingtalk.microappstore.service.UnifiedMarketService:1.0.0#listAppAndMarketGoodsByGoodsCodes(java.util.List)"
从上面的耗时分析,可以很快定位到服务提供方的大量耗时用在了
更多精彩内容,关注公众号:ali老蒋,或点击加我好友深度沟通:ali老蒋 - java开发者