先推荐大家阅读dubbo官网源码解读服务调用流程一节,传送门:
dubbo.apache.org/zh-cn/docs/…
一、调用模式
Dubbo同步调用还是异步调用的逻辑是在DubboInvoker中,Dubbo 实现同步和异步调用比较关键的一点就在于由谁调用 ResponseFuture 的 get方法。
- 同步调用模式下,由框架自身调用 ResponseFuture 的 get 方法。
- 异步调用模式下,则由用户调用ResponseFuture的 get 方法。
ResponseFuture是一个接口,ResponseFuture的默认实现是DefaultFuture,当服务消费者还未接收到调用结果时,用户线程调用 get 方法会被阻塞住。
- 同步调用模式下,框架获得 DefaultFuture 对象后,会立即调用 get 方法进行等待。
- 异步模式下则是将该对象封装到 FutureAdapter 实例中,并将 FutureAdapter 实例设置到 RpcContext 中,供用户使用。FutureAdapter 是一个适配器,用于将 Dubbo 中的 ResponseFuture 与 JDK 中的 Future 进行适配。这样当用户线程调用 Future 的 get 方法时,经过 FutureAdapter 适配,最终会调用 ResponseFuture 实现类对象的 get 方法,也就是 DefaultFuture 的 get 方法。dubbo2.7.0使用了CompletableFuture,同时会将其设置到异步上下文中。
一般情况下,服务调用方会并发调用多个服务,每个用户线程发送请求后,会调用不同 DefaultFuture 对象的 get 方法进行等待。 一段时间后,服务调用方的线程池会收到多个响应对象。这个时候要考虑一个问题,如何将每个响应对象传递给相应的 DefaultFuture 对象,且不出错。答案是通过调用编号。DefaultFuture 被创建时,会要求传入一个 Request 对象。此时 DefaultFuture 可从 Request 对象中获取调用编号,并将 <调用编号, DefaultFuture 对象> 映射关系存入到静态 Map 中,即 FUTURES。线程池中的线程在收到 Response 对象后,会根据 Response 对象中的调用编号到 FUTURES 集合中取出相应的 DefaultFuture 对象,然后再将 Response 对象设置到 DefaultFuture 对象中。最后再唤醒用户线程,这样用户线程即可从 DefaultFuture 对象中获取调用结果了。
二、超时异常
DefaultFuture中的sent变量在客户端向服务端发送请求成功后会写入,以表明消息发送完成。
-->NettyChannel#send
-->io.netty.channel.ChannelOutboundInvoker#writeAndFlush
-->NettyClientHandler#write
-->.DefaultFuture#sent
1.客户端超时
如果客户端没有成功发送消息,服务端不会返回响应(Response),DefaultFuture中的sent变量也没有被写入,在DefaultFuture#getTimeoutMessage会根据sent是否大于0,输出客户端超时异常。
2.服务端超时
如果客户端成功发送消息,服务端返回响应(Response),DefaultFuture中的sent变量被写入,在DefaultFuture#getTimeoutMessage会根据sent是否大于0,服务端超时异常。
private String getTimeoutMessage(boolean scan) {
long nowTimestamp = System.currentTimeMillis();
return (sent > 0 ? "Waiting server-side response timeout" : "Sending request timeout in client-side")
+ (scan ? " by scan timer" : "") + ". start time: "
+ (new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS").format(new Date(start))) + ", end time: "
+ (new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS").format(new Date())) + ","
+ (sent > 0 ? " client elapsed: " + (sent - start)
+ " ms, server elapsed: " + (nowTimestamp - sent)
: " elapsed: " + (nowTimestamp - start)) + " ms, timeout: "
+ timeout + " ms, request: " + request + ", channel: " + channel.getLocalAddress()
+ " -> " + channel.getRemoteAddress();
}
3.服务端超时或者客户端超时dubbo如何构造 Response
当发生超时异常的时候是没有Response返回的,dubbo的客户端在创建DefaultFuture的时候会创建一个TimeoutCheckTask的延时任务,当超时时间到达后就会执行。这段代码不难理解。
public static DefaultFuture newFuture(Channel channel, Request request, int timeout) {
final DefaultFuture future = new DefaultFuture(channel, request, timeout);
// timeout check
timeoutCheck(future);
return future;
}
private static class TimeoutCheckTask implements TimerTask {
private DefaultFuture future;
TimeoutCheckTask(DefaultFuture future) {
this.future = future;
}
@Override
public void run(Timeout timeout) {
if (future == null || future.isDone()) {
return;
}
// create exception response.
Response timeoutResponse = new Response(future.getId());
// set timeout status.
timeoutResponse.setStatus(future.isSent() ? Response.SERVER_TIMEOUT : Response.CLIENT_TIMEOUT);
timeoutResponse.setErrorMessage(future.getTimeoutMessage(true));
// handle response.
DefaultFuture.received(future.getChannel(), timeoutResponse);
}
}
private Object returnFromResponse() throws RemotingException {
Response res = response;
if (res == null) {
throw new IllegalStateException("response cannot be null");
}
// 如果响应正常则返回调用结果
if (res.getStatus() == Response.OK) {
return res.getResult();
}
// 抛出超时异常
if (res.getStatus() == Response.CLIENT_TIMEOUT || res.getStatus() == Response.SERVER_TIMEOUT) {
throw new TimeoutException(res.getStatus() == Response.SERVER_TIMEOUT, channel, res.getErrorMessage());
}
// 消费方调用异常抛出RemotingException
throw new RemotingException(channel, res.getErrorMessage());
}
使用延时任务的方式会在调用超时的时候也会使RPC调用流程完整,而不至于一直停留在!isDone()状态,相对来说这种方式可能更好一些。
三、对超时问题的一些理解。
- 首先要根据业务设置合适超时时间,所有的服务应当设置一样的超时时间。
- 并不是所有的超时异常都需要重试,有的业务场景应当由用户手动发起请求,一定要争对业务场景做合适的选择。
- 发生超时异常并不意味这服务端处理失败(所以合适的超时时间尤为重要),这时可以通过查询接口主动拉取信息。
- 发生超时前端应当设置遮罩层(有的人手贱没有返回一直点),否则可能引发雪崩。