关于Dubbo的超时
Dubbo 客户端超时设置是Dubbo 客户端参数最常用设置参数。本文介绍在Dubbo超时参数配置生效原理,对应超时异常问题排查和Dubbo里两种特殊超时原理。
1. Dubbo超时参数设置方式
1.1 Dubbo 超时的配置方式
Dubbo的一般启动方式有三种,注解方式,xml方式,api方式。
<!--xml方式-->
<dubbo:reference id="demoServiceRef" timeout = "1000" interface="com.ctrip.framework.cdubbo.demo3.api.HelloBOMService" >
<dubbo:parameter key="validateServiceId" value="false"/>
</dubbo:reference>
注解的方式
@RestController
public class DubboController {
@DubboReference(timeout = 10000)
public HelloBOMService helloBOMService;
@RequestMapping("/")
使用API的方式
public static void main(String[] args) throws IOException {
ReferenceBuilder referenceBuilder = new ReferenceBuilder();
referenceBuilder.<HelloBOMService>timeout(3000);
ReferenceConfig<HelloBOMService> referenceConfig = referenceBuilder.build();
HelloBOMService someClient = referenceConfig.get();
1.2 不同配置级别的超时
Dubbo的配置分为服务端和客户端,客户端的超时还有服务级别和操作级别。
优先级为服务端> 客户端服务级别>客户端方法级别
2. Dubbo超时是如何生效
超时相关的类DubboInvoker
DefaultFuture
DubboInvoker
决定了当前这个请求需要多久之后会超时。
DefaultFuter
决定了超时之后的异常怎么产生。
2.1 超时参数的设置
直接上代码 DubboInvoker
Dubbo 有oneway机制只发送不收取于是这种请求不需要超时。
主要代码在 int timeout = calculateTimeout(invocation, methodName);
@Override
protected Result doInvoke(final Invocation invocation) throws Throwable {
....
try {
boolean isOneway = RpcUtils.isOneway(getUrl(), invocation);
int timeout = calculateTimeout(invocation, methodName);
// 1. one way 表示只发不收 所以第一个分支实际不用超时
if (isOneway) {
boolean isSent = getUrl().getMethodParameter(methodName, Constants.SENT_KEY, false);
currentClient.send(inv, isSent);
return AsyncRpcResult.newDefaultAsyncResult(invocation);
} else {
// 2.响应交给哪个线程池来执行
ExecutorService executor = getCallbackExecutor(getUrl(), inv);
// 具体的请求,相关调用链可以参考官方文档 最后会到HeaderExchangeChannel
CompletableFuture<AppResponse> appResponseFuture =
currentClient.request(inv, timeout, executor).thenApply(obj -> (AppResponse) obj);
// save for 2.6.x compatibility, for example, TraceFilter in Zipkin uses com.alibaba.xxx.FutureAdapter
FutureContext.getContext().setCompatibleFuture(appResponseFuture);
AsyncRpcResult result = new AsyncRpcResult(appResponseFuture, inv);
result.setExecutor(executor);
return result;
}
} catch (TimeoutException e) {
.... 省略各种异常
}
}
计算超时时我们发现这里两种机制一种基于 countdownTimeout
。
这个方法里面计算出来的就是当前的超时时间。默认超时是静态变量1000ms。
private int calculateTimeout(Invocation invocation, String methodName) {
// 1. RpcContext.getContext()后续介绍
Object countdown = RpcContext.getContext().get(TIME_COUNTDOWN_KEY);
int timeout = DEFAULT_TIMEOUT;
if (countdown == null) {
// 参数1 url,dubbo 这个地方的url实际是整合了服务端配置原理会在下面讲到
// 参数2 方法名,dubbo从url中拿参数的时候实际是按照先拿操作界别在按照服务级别,这个地方时获取方法界别的
// 这里第三个参数就很有意思,RpcContext 在dubbo中是类似ThreadLocal并且单次生效的配置
timeout = (int) RpcUtils.getTimeout(getUrl(), methodName, RpcContext.getContext(), DEFAULT_TIMEOUT);
if (getUrl().getParameter(ENABLE_TIMEOUT_COUNTDOWN_KEY, false)) {
invocation.setObjectAttachment(TIMEOUT_ATTACHENT_KEY, timeout); // pass timeout to remote server
}
} else {
....
}
return timeout;
}
进入RpcUtils
看其中的计算,其中context.getObjectAttachment("timeout")
允许用户为每一次请求设立不同的超时。
public static long getTimeout(URL url, String methodName, RpcContext context, long defaultTimeout) {
//1. 设置默认
long timeout = defaultTimeout;
//2. 取单次生效的超时时间
Object genericTimeout = context.getObjectAttachment("timeout");
if (genericTimeout != null) {
timeout = convertToNumber(genericTimeout, defaultTimeout);
} else if (url != null) {
// 3. 否则取配置的超时
timeout = url.getMethodPositiveParameter(methodName, "timeout", defaultTimeout);
}
return timeout;
}
Dubbo 会先查看用户有没有设置单次超时,有的话会使用单次超时,没有的话使用url中的操作界别超时,没有在寻找服务级别的超时配置,再没有使用默认的超时配置。
2.2 超时生效机制
DefaultFuter
类保存了Dubbo客户端所有发送但是未超时且未返回的请求。
Dubbo的请求和响应都带有messageId.
发送请求1的时候,服务端返回会带有1的序列号的响应,表示是请求1的响应。
DefaultFuture
创建的代码
org.apache.dubbo.remoting.exchange.support.header.HeaderExchangeChannel#request(java.lang.Object, int, java.util.concurrent.ExecutorService)·
@Override
public CompletableFuture<Object> request(Object request, int timeout, ExecutorService executor) throws RemotingException {
if (closed) {
// 不能发送不发
}
// create request.
// 有个自增的变量保证dubbo的消息的ID为自增
Request req = new Request();
req.setVersion(Version.getProtocolVersion());
req.setTwoWay(true);
req.setData(request);
// 每发一个请求就创建一个 直到有返回才删除
DefaultFuture future = DefaultFuture.newFuture(channel, req, timeout, executor);
try {
channel.send(req);
} catch (RemotingException e) {
future.cancel();
throw e;
}
return future;
}
DefaultFuture
的静态 方法都会做两个操作,new操作会把请求放到一个Map中去
timeoutCheck操作会新建一个定时检查超时的线程。
// 1. 每次new都会put 这边id是传送出去的requestId而value是返回给前面的future.
private static final Map<Long, DefaultFuture> FUTURES = new ConcurrentHashMap<>();
public static DefaultFuture newFuture(Channel channel, Request request, int timeout, ExecutorService executor) {
final DefaultFuture future = new DefaultFuture(channel, request, timeout);
// 2. 这个线程池的问题,还有一个threadLess的扩展以及线程池的仓库
future.setExecutor(executor);
// ThreadlessExecutor needs to hold the waiting future in case of circuit return.
if (executor instanceof ThreadlessExecutor) {
((ThreadlessExecutor) executor).setWaitingFuture(future);
}
// 3. timeout check 生成定时检查超时的任务
timeoutCheck(future);
return future;
}
//这东西Dubbo里面用的比较多一个线程就可以处理多个加在上面的任务
public static final Timer TIME_OUT_TIMER = new HashedWheelTimer(
new NamedThreadFactory("dubbo-future-timeout", true),
30,
TimeUnit.MILLISECONDS);
pr
ivate static void timeoutCheck(DefaultFuture future) {
TimeoutCheckTask task = new TimeoutCheckTask(future.getId());
//创建 超时的健康监测,因为上面设置的30ms的ticktock时间,感觉小于30ms的超时似乎不会生效
future.timeoutCheckTask = TIME_OUT_TIMER.newTimeout(task, future.getTimeout(), TimeUnit.MILLISECONDS);
}
private static class TimeoutCheckTask implements TimerTask {
private final Long requestID;
TimeoutCheckTask(Long requestID) {
this.requestID = requestID;
}
@Override
public void run(Timeout timeout) {
····
// 1. 假如时间到了,利用利用requestId 找到对应的请求
DefaultFuture future = DefaultFuture.getFuture(requestID);
// 请求已经返回或者已经被处理过了 不需要超时
if (future == null || future.isDone()) {
return;
}
if (future.getExecutor() != null) {
future.getExecutor().execute(() -> notifyTimeout(future));
} else {
// 生成一个带超时错误的Response并返回
notifyTimeout(future);
}
}
private void notifyTimeout(DefaultFuture future) {
// 1. create exception response. 响应包含当时的信息,
Response timeoutResponse = new Response(future.getId());
// 2. set timeout status.
timeoutResponse.setStatus(future.isSent() ? Response.SERVER_TIMEOUT : Response.CLIENT_TIMEOUT);
timeoutResponse.setErrorMessage(future.getTimeoutMessage(true));
// 3, handle response. 这一行假如是正常返回Response里面就是正常的响应
DefaultFuture.received(future.getChannel(), timeoutResponse, true);
}
}
所以这边的逻辑是每次发送的时候保存请求的map并设置定时定期检查超时,假如有请求超时就直接给一个超时的异常响应。
3. 两种特殊的超时
3.1 单次请求超时
见上面2.1 超时设置的分析
配置代码
ReferenceBuilder referenceBuilder = new ReferenceBuilder();
referenceBuilder.<HelloBOMService>timeout(3000);
ReferenceConfig<HelloBOMService> referenceConfig = referenceBuilder.build();
HelloBOMService someClient = referenceConfig.get();
//单次生效
RpcContext.getContext().setObjectAttachment(TIMEOUT_KEY,1000);
someClient.call(someRequest);
3.2 累加减少的超时分析
从上面计算超时的第二个分支进去
private int calculateTimeout(Invocation invocation, String methodName) {
Object countdown = RpcContext.getContext().get(TIME_COUNTDOWN_KEY);
int timeout = DEFAULT_TIMEOUT;
if (countdown == null) {
} else {
// 1. 发现上下文中有个定时器
TimeoutCountDown timeoutCountDown = (TimeoutCountDown) countdown;
// 2. 拿到剩余时间
timeout = (int) timeoutCountDown.timeRemaining(TimeUnit.MILLISECONDS);
// 3. 将剩余时间传递给服务端 TIMEOUT_ATTACHENT_KEY = "_TO" 这名字略不走心
invocation.setObjectAttachment(TIMEOUT_ATTACHENT_KEY, timeout);// pass timeout to remote server
}
return timeout;
}
TimeoutCountDown
的原理比较简单就是给一个超时和起始时间,然后每次都计算出现在的时间点,还剩多少超时。比如我3面前设置超时为5,我当时拿到的超时就是(5-3)=2秒。
客户端把参数传到服务端之后又做了什么呢?
对应代码org.apache.dubbo.rpc.filter.ContextFilter#invoke
@Override
public Result invoke(Invoker<?> invoker, Invocation invocation) throws RpcException {
.....
RpcContext context = RpcContext.getContext();
// 1. 这个地方其实拿着的是 客端传递过来的 TIMEOUT_ATTACHENT_KEY = "_TO"
long timeout = RpcUtils.getTimeout(invocation, -1);
if (timeout != -1) {
// 2. 这边利用客户端传递过来的超时又新建一个超时出来
context.set(TIME_COUNTDOWN_KEY, TimeoutCountDown.newCountDown(timeout, TimeUnit.MILLISECONDS));
}
.....
}
}
考虑一种场景 A->B->C 假如A设置超时5s
但是B收到请求后,处理其他逻辑用了4s 然后开始调用C
这个时候C只要1s的超时就会使得A得不到正常的结果,所以没有必要hold线程到1s以上。
于是优化了性能
3. 超时异常问题排查
我们看下超时的message会给我们何种报错信息
private String getTimeoutMessage(boolean scan) {
long nowTimestamp = System.currentTimeMillis();
// 首先是标志位sent,这个是netty的一个状态表示客户端是否把请求发送出去了。
// 错误假如是 server-side response timeout 考虑是服务端的业务处理太慢导致 没在约定的时间内返回
// Sending request timeout in client-side 考虑是客户端的请求没有发出去考虑是客户端的网络问题
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(nowTimestamp))) + ","
+ (sent > 0 ? " client elapsed: " + (sent - start)
+ " ms, server elapsed: " + (nowTimestamp - sent)
: " elapsed: " + (nowTimestamp - start)) + " ms, timeout: "
+ timeout + " ms, request: " + (logger.isDebugEnabled() ? request : getRequestWithoutData()) + ", channel: " + channel.getLocalAddress()
+ " -> " + channel.getRemoteAddress();
}
4. 扩展:客户端怎么拿到服务端的超时配置
先省略大部分的注册发现状态。我们从第一次服务端推送服务实例的url到客户端。
相关的类是XXXRegistry extends FailbackRegistry
可能是ZK或者其他的。
// 推送消实例信息会走如下的方法其中consumerUrl 对应当前发现的客户端
// urls 这个场景下是对应的服务端URL。(一般还有配置的URL)
this.doNotify(consumerUrl, listener, urls);
urls会根据协议进行分类然后传递给对应的listener。
listenerd的实现有如下几个
org.apache.dubbo.registry.integration.RegistryProtocol.OverrideListener 改写客户端url 下发动态配置
org.apache.dubbo.registry.integration.RegistryDirectory 获取服务端的urls 重新生成Invoker的列表
还有其他一些
明显我们需要找RegistryDirectory
调用链有点长直接跳一下
org.apache.dubbo.registry.integration.RegistryDirectory#toInvokers ->mergeUrl
private URL mergeUrl(URL providerUrl) {
// 1. query map 为客户端所带有的所有parameter
providerUrl = ClusterUtils.mergeUrl(providerUrl, queryMap); // Merge the consumer side parameters
// .....
return url
}
org.apache.dubbo.rpc.cluster.support.ClusterUtils#mergeUrl
public static URL mergeUrl(URL remoteUrl, Map<String, String> localMap) {
Map<String, String> map = new HashMap<String, String>();
// 1. 获取远端的属性
Map<String, String> remoteMap = remoteUrl.getParameters();
if (remoteMap != null && remoteMap.size() > 0) {
map.putAll(remoteMap);
// 移除一些不下发的属性, 下面列表的属性服务端不会下发
map.remove(THREAD_NAME_KEY);
map.remove(DEFAULT_KEY_PREFIX + THREAD_NAME_KEY);
}
if (localMap != null && localMap.size() > 0) {
//2. 本地属性 复制避免更改
Map<String, String> copyOfLocalMap = new HashMap<>(localMap);
// 3. 特殊的key version 和group DubboInvoker 还需要靠他来实现version =*
if(map.containsKey(GROUP_KEY)){
copyOfLocalMap.remove(GROUP_KEY);
}
if(map.containsKey(VERSION_KEY)){
copyOfLocalMap.remove(VERSION_KEY);
}
// 删除无用的key
copyOfLocalMap.remove(RELEASE_KEY);
// 4. 重点 客户端后放 最终覆盖了服务端下发的配置
map.putAll(copyOfLocalMap);
map.put(REMOTE_APPLICATION_KEY, remoteMap.get(APPLICATION_KEY));
}
return remoteUrl.clearParameters().addParameters(map);
}
结论先putAll服务端参数再putAll客户端参数,最终生效客户端参数。