背景
超时 这个概念不是dubbo才有的,在其他如http、线程执行时间、mysql、redis 等都有存在,实现方式也大同小异,dubbo是通过消费者实现的超时能力。
如图在消费端会有一个
定时任务角色,根据接口的不同超时时间来通知超时事件,跑超时错误给到业务代码。
分析
调用链路
我们回顾下dubbo客户端的调用链路。
从图上可以看出,整个调用链路经过了
invoker、filter、exchange,这三个概念大家自己去前面文章了解,这里主要的是红色背景的流程DefaultFuture.newFuture(),点进去看看。
//org....exchange.support.DefaultFuture#newFuture
DefaultFuture newFuture(Channel channel, Request request, int timeout) {
final DefaultFuture future = new DefaultFuture(channel, request, timeout);
timeoutCheck(future);//1
return future;
}
void timeoutCheck(DefaultFuture future) {
TimeoutCheckTask task = new TimeoutCheckTask(future.getId());
future.timeoutCheckTask = TIME_OUT_TIMER.newTimeout(task, future.getTimeout(), TimeUnit.MILLISECONDS);//2
}
public static final Timer TIME_OUT_TIMER = new HashedWheelTimer(
new NamedThreadFactory("", true),30,TimeUnit.MILLISECONDS);//3
注释1处,将future对象传到了超时检查方法中,注释2用 future.getId() 初始化了 TimeoutCheckTask 对象,这个id来自于每个请求的唯一编号是通过AtomInteger 生成的。
private static final AtomicLong INVOKE_ID = new AtomicLong(0);
private static long newId() {return INVOKE_ID.getAndIncrement();}
超时任务
每个请求唯一递增标识,task 会被加到每30毫秒执行的定时任务中(新版本dubbo 通过时间轮实现定时任务),也能看出定时任务至少会有30ms的误差,我们到看看task
//org....exchange.support.DefaultFuture.TimeoutCheckTask#run
public void run(Timeout timeout) {
DefaultFuture future = DefaultFuture.getFuture(requestID);
if (future == null || future.isDone()) { return; }//1
Response timeoutResponse = new Response(future.getId());
timeoutResponse.setStatus(future.isSent() ? 31 : 30);//2
timeoutResponse.setErrorMessage(future.getTimeoutMessage(true));
DefaultFuture.received(future.getChannel(), timeoutResponse, true);//3
}
下面这些很关键了,注释1的分支应该执行最常见,这种case是服务端已经返回结果了(就是不超时的情况),注释2很有趣,isSent()判断请求是否已经发出,如果网络原因没发出去,那么就是客户端超时,而不是服务端处理超时,注释3我们继续跟。
org....exchange.support.DefaultFuture#received(Channel, Response, boolean)
void received(Channel channel, Response response, boolean timeout) {
try {
DefaultFuture future = FUTURES.remove(response.getId());
if (future != null) {
Timeout t = future.timeoutCheckTask;
if (!timeout) {t.cancel(); }
future.doReceived(response);//继续点进去
} else {}
} finally {
CHANNELS.remove(response.getId());
}
}
//org.apache.dubbo.remoting.exchange.support.DefaultFuture#doReceived
private void doReceived(Response res) {
if (res.getStatus() == Response.OK) {
this.complete(res.getResult());
} else if (res.getStatus() == Response.CLIENT_TIMEOUT || res.getStatus() == Response.SERVER_TIMEOUT) {
this.completeExceptionally(new TimeoutException(res.getStatus() == Response.SERVER_TIMEOUT, channel, res.getErrorMessage()));//1超时逻辑
} else {
this.completeExceptionally(new RemotingException(channel, res.getErrorMessage()));
}
}
注释1处,调用completeExceptionally() 将future 完成并包装了错误信息返回,业务等待的线程此时就会被unpark继续执行返回结果处理逻辑。
//org.apache.dubbo.rpc.proxy.InvokerInvocationHandler#invoke
public Object invoke(Object proxy, Method method, Object[] args) {
String methodName = method.getName();
//....
return invoker.invoke(new RpcInvocation(method, args)).recreate();
}
//org.apache.dubbo.rpc.AppResponse#recreate
public Object recreate() throws Throwable {
if (exception != null) {
try {
Class clazz = exception.getClass();
while (!clazz.getName().equals(Throwable.class.getName())) { clazz = clazz.getSuperclass(); }
Field stackTraceField = clazz.getDeclaredField("stackTrace");
stackTraceField.setAccessible(true);
Object stackTrace = stackTraceField.get(exception);
if (stackTrace == null) { exception.setStackTrace(new StackTraceElement[0]); }
} catch (Exception e) {
// ignore
}
throw exception;//1
}
return result;
}
注释1处,如果exception 不为空,就将错误抛出给业务,这样超时错误就处理完了,大家可以自行去调试下,如果是服务端错误是怎么给到客户端的?客户端又是怎么处理的?
总结
超时总是不可避免,我们在设计时,对于超时时间、超时策略都应该好好考虑。