dubbo-是怎么实现超时的?

2,092 阅读3分钟

背景

超时 这个概念不是dubbo才有的,在其他如http、线程执行时间、mysql、redis 等都有存在,实现方式也大同小异,dubbo是通过消费者实现的超时能力。 dubbo 超时 如图在消费端会有一个定时任务角色,根据接口的不同超时时间来通知超时事件,跑超时错误给到业务代码。

分析

调用链路

我们回顾下dubbo客户端的调用链路。 dubbo客户端调用链路 从图上可以看出,整个调用链路经过了 invokerfilterexchange,这三个概念大家自己去前面文章了解,这里主要的是红色背景的流程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 不为空,就将错误抛出给业务,这样超时错误就处理完了,大家可以自行去调试下,如果是服务端错误是怎么给到客户端的?客户端又是怎么处理的?

总结

超时总是不可避免,我们在设计时,对于超时时间超时策略都应该好好考虑。