其他
RPC 的超时设置,一不小心就是线上事故!
The following article is from IT人的职场进阶 Author 骆俊武
(给ImportNew加星标,提高Java技能)
作者:骆俊武
从一次RPC接口超时引发的线上事故说起 超时的实现原理是什么? 设置超时时间到底是为了解决什么问题? 应该如何合理的设置超时时间?
APP端发起一个HTTP请求到业务网关 业务网关RPC调用推荐服务,获取推荐商品list 如果第2步调用失败,则服务降级,改成RPC调用商品排序服务,获取热销商品list进行托底 如果第3步调用失败,则再次降级,直接获取Redis缓存中的热销商品list
将业务网关调用推荐服务的超时时间改成了800ms(推荐服务的TP99大约为540ms),超时重试次数改成了2次 将业务网关调用商品排序服务的超时时间改成了600ms(商品排序服务的TP99大约为400ms),超时重试次数也改成了2次
public class TimeoutFilter implements Filter {
public TimeoutFilter() {
}
public Result invoke(...) throws RpcException {// 执行真正的逻辑调用,并统计耗时
long start = System.currentTimeMillis();
Result result = invoker.invoke(invocation);
long elapsed = System.currentTimeMillis() - start;
// 判断是否超时
if (invoker.getUrl() != null && elapsed > timeout) {// 打印warn日志
logger.warn("invoke time out...");
}
return result;
}
}
public class TimeoutFilter implements Filter
public class FailoverClusterInvoker {
public Result doInvoke(...) {
...
// 循环调用设定的重试次数
for (int i = 0; i < retryTimes; ++i) {
...
try {
Result result = invoker.invoke(invocation);
return result;
} catch (RpcException e) {
// 如果是业务异常,终止重试
if (e.isBiz()) {
throw e;
}
le = e;
} catch (Throwable e) {
le = new RpcException(...);
} finally {
...
}
}
throw new RpcException("...");
}
}
FailoverCluster是集群容错的缺省模式,当调用失败后会切换成调用其他服务器。再看下doInvoke方法,当调用失败时,会先判断是否是业务异常,如果是则终止重试,否则会一直重试直到达到重试次数。
继续跟踪invoker的invoke方法,可以看到在请求发出后通过Future的get方法获取结果,源码如下:
public Object get(int timeout) {
if (timeout <= 0) {
timeout = 1000;
}
if (!isDone()) {
long start = System.currentTimeMillis();
this.lock.lock();
try {
// 循环判断
while(!isDone()) {
// 放弃锁,进入等待状态
done.await((long)timeout, TimeUnit.MILLISECONDS);
// 判断是否已经返回结果或者已经超时
long elapsed = System.currentTimeMillis() - start;
if (isDone() || elapsed > (long)timeout) {
break;
}
}
} catch (InterruptedException var8) {
throw new RuntimeException(var8);
} finally {
this.lock.unlock();
}
if (!isDone()) {
// 如果未返回结果,则抛出超时异常
throw new TimeoutException(...);
}
}
return returnFromResponse();
}
进入方法后开始计时,如果在设定的超时时间内没有获得返回结果,则抛出TimeoutException。因此,消费端的超时逻辑同时受到超时时间和超时次数两个参数的控制,像网络异常、响应超时等都会一直重试,直到达到重试次数。
设置调用方的超时时间之前,先了解清楚依赖服务的TP99响应时间是多少(如果依赖服务性能波动大,也可以看TP95),调用方的超时时间可以在此基础上加50% 如果RPC框架支持多粒度的超时设置,则:全局超时时间应该要略大于接口级别最长的耗时时间,每个接口的超时时间应该要略大于方法级别最长的耗时时间,每个方法的超时时间应该要略大于实际的方法执行时间 区分是可重试服务还是不可重试服务,如果接口没实现幂等则不允许设置重试次数。注意:读接口是天然幂等的,写接口则可以使用业务单据ID或者在调用方生成唯一ID传递给服务端,通过此ID进行防重避免引入脏数据 如果RPC框架支持服务端的超时设置,同样基于前面3条规则依次进行设置,这样能避免客户端不设置的情况下配置是合理的,减少隐患 如果从业务角度来看,服务可用性要求不用那么高(比如偏内部的应用系统),则可以不用设置超时重试次数,直接人工重试即可,这样能减少接口实现的复杂度,反而更利于后期维护 重试次数设置越大,服务可用性越高,业务损失也能进一步降低,但是性能隐患也会更大,这个需要综合考虑设置成几次(一般是2次,最多3次) 如果调用方是高QPS服务,则必须考虑服务方超时情况下的降级和熔断策略。(比如超过10%的请求出错,则停止重试机制直接熔断,改成调用其他服务、异步MQ机制、或者使用调用方的缓存数据)
看完本文有收获?请转发分享给更多人
关注「ImportNew」,提升Java技能
好文章,我在看❤️