1 文章概述
DUBBO有很多地方可以配置超时时间,可以配置在消费者,可以配置在生产者,可以配置为方法级别,可以配置为接口级别,还可以配置为全局级别,DUBBO官方文档介绍这些配置优先级如下:
第一优先级:方法级 > 接口级 > 全局级
第二优先级:消费者 > 生产者
本文从源码层面对超时机制进行分析,我们首先分析优先级如何生效,然后再分析超时机制在消费者和生产者分别如何实现。
2 配置优先级
2.1 消费者 > 生产者
配置生产者接口级别超时时间888毫秒
<beans>
<dubbo:registry address="zookeeper://127.0.0.1:2181" />
<dubbo:protocol name="dubbo" port="20880" />
<dubbo:service timeout="888" interface="com.itxpz.dubbo.demo.provider.HelloService" ref="helloService" />
</beans>
配置消费者接口级别超时时间999毫秒
<beans>
<dubbo:application name="xpz-consumer" />
<dubbo:registry address="zookeeper://127.0.0.1:2181" />
<dubbo:reference timeout="999" id="helloService" interface="com.itxpz.dubbo.demo.provider.HelloService" />
</beans>
生产者首先注册服务信息至注册中心,消费者从注册中心订阅服务信息,在获取到生产者服务信息后,会将这些配置与消费者配置进行融合,核心在消费者订阅信息后会将服务信息转化为Invokers这一段代码
public class RegistryDirectory<T> extends AbstractDirectory<T> implements NotifyListener {
private Map<String, Invoker<T>> toInvokers(List<URL> urls) {
Map<String, Invoker<T>> newUrlInvokerMap = new HashMap<String, Invoker<T>>();
if (urls == null || urls.isEmpty()) {
return newUrlInvokerMap;
}
for (URL providerUrl : urls) {
// providerUrl是从注册中心订阅的生产者配置
// providerUrl=dubbo://x.x.x.x:20880/com.itxpz.dubbo.demo.provider.HelloService?anyhost=true&application=xpz-provider&dubbo=2.0.2&generic=false&interface=com.itxpz.dubbo.demo.provider.HelloService&methods=sayHello&pid=16736&release=2.7.0&side=provider&timeout=888
// mergeUrl方法进行多维度参数融合
// 本文只分析消费者和生产者参数融合
URL url = mergeUrl(providerUrl);
}
}
}
分析消费者和生产者参数融合代码
public class ClusterUtils {
public static URL mergeUrl(URL remoteUrl, Map<String, String> localMap) {
// 消费者参数localMap = {side=consumer, register.ip=x.x.x.x, methods=sayHello, release=2.7.0, qos.port=55555, dubbo=2.0.2, pid=16904, interface=com.itxpz.dubbo.demo.provider.HelloService, qos.enable=true, timeout=999, application=xpz-consumer, qos.accept.foreign.ip=false, timestamp=123}
// 生产者参数remoteMap = {side=provider, methods=sayHello, release=2.7.0, dubbo=2.0.2, pid=16736, interface=com.itxpz.dubbo.demo.provider.HelloService, generic=false, timeout=888, application=xpz-provider, anyhost=true, timestamp=123}
Map<String, String> remoteMap = remoteUrl.getParameters();
Map<String, String> map = new HashMap<String, String>();
// 消费者配置不为空则全部赋值至结果对象
if (localMap != null && localMap.size() > 0) {
String remoteGroup = map.get(Constants.GROUP_KEY);
map.put(Constants.GROUP_KEY, remoteGroup);
map.putAll(localMap);
}
// 生产者配置不为空则设置一些信息
if (remoteMap != null && remoteMap.size() > 0) {
// 省略代码
}
// 超时时间已经从888毫秒变为999毫秒
// dubbo://x.x.x.x:20880/com.itxpz.dubbo.demo.provider.HelloService?anyhost=true&application=xpz-consumer&dubbo=2.0.2&generic=false&group=&interface=com.itxpz.dubbo.demo.provider.HelloService&methods=sayHello&pid=16284&qos.accept.foreign.ip=false&qos.enable=true&qos.port=55555&release=2.7.0&remote.application=xpz-provider&side=consumer&timeout=999
URL result = remoteUrl.clearParameters().addParameters(map);
return result;
}
}
2.2 方法级 > 接口级
配置消费者接口级别超时时间999毫秒
<beans>
<dubbo:application name="xpz-consumer" />
<dubbo:registry address="zookeeper://127.0.0.1:2181" />
<dubbo:reference timeout="999" id="helloService" interface="com.itxpz.dubbo.demo.provider.HelloService" />
</beans>
配置生产者方法级别超时时间1111毫秒
<beans>
<dubbo:registry address="zookeeper://127.0.0.1:2181" />
<dubbo:protocol name="dubbo" port="20880" />
<dubbo:service interface="com.itxpz.dubbo.demo.provider.HelloService" ref="helloService">
<dubbo:method name="sayHello" timeout="1111" />
</dubbo:service>
</beans>
首先观察经过参数融合后URL
public class ClusterUtils {
public static URL mergeUrl(URL remoteUrl, Map<String, String> localMap) {
// 消费者参数localMap = {side=consumer, register.ip=x.x.x.x, methods=sayHello, release=2.7.0, qos.port=55555, dubbo=2.0.2, pid=15436, interface=com.itxpz.dubbo.demo.provider.HelloService, qos.enable=true, timeout=999, application=xpz-consumer, qos.accept.foreign.ip=false, timestamp=123}
// 生产者参数remoteMap = {side=provider, methods=sayHello, release=2.7.0 dubbo=2.0.2, pid=16260,interface = com.itxpz.dubbo.demo.provider.HelloService, sayHello.timeout = 1111, generic = false, application = xpz - provider, anyhost = true, timestamp = 123}
Map<String, String> remoteMap = remoteUrl.getParameters();
Map<String, String> map = new HashMap<String, String>();
// 消费者配置不为空则全部赋值至结果对象
if (localMap != null && localMap.size() > 0) {
String remoteGroup = map.get(Constants.GROUP_KEY);
map.put(Constants.GROUP_KEY, remoteGroup);
map.putAll(localMap);
}
// 生产者配置不为空则设置一些信息
if (remoteMap != null && remoteMap.size() > 0) {
// 省略代码
}
// 我们看到两个配置sayHello.timeout=1111、timeout=999
// dubbo://x.x.x.x:20880/com.itxpz.dubbo.demo.provider.HelloService?anyhost=true&application=xpz-consumer&dubbo=2.0.2&generic=false&group=&interface=com.itxpz.dubbo.demo.provider.HelloService&methods=sayHello&pid=5456&qos.accept.foreign.ip=false&qos.enable=true&qos.port=55555&release=2.7.0&remote.application=xpz-provider&sayHello.timeout=1111&side=consumer&timeout=999
URL result = remoteUrl.clearParameters().addParameters(map);
return result;
}
}
我们看到timeout有两个配置,这两个配置优先级在消费者发起远程调用时体现
public class DubboInvoker<T> extends AbstractInvoker<T> {
@Override
protected Result doInvoke(final Invocation invocation) throws Throwable {
RpcInvocation inv = (RpcInvocation) invocation;
final String methodName = RpcUtils.getMethodName(invocation);
inv.setAttachment(Constants.PATH_KEY, getUrl().getPath());
inv.setAttachment(Constants.VERSION_KEY, version);
ExchangeClient currentClient;
if (clients.length == 1) {
currentClient = clients[0];
} else {
currentClient = clients[index.getAndIncrement() % clients.length];
}
try {
boolean isAsync = RpcUtils.isAsync(getUrl(), invocation);
boolean isAsyncFuture = RpcUtils.isReturnTypeFuture(inv);
boolean isOneway = RpcUtils.isOneway(getUrl(), invocation);
// 获取超时时间方法体现优先级
// getUrl() = dubbo://x.x.x.x:20880/com.itxpz.dubbo.demo.provider.HelloService?anyhost=true&application=xpz-consumer&dubbo=2.0.2&generic=false&group=&interface=com.itxpz.dubbo.demo.provider.HelloService&methods=sayHello&pid=5456&qos.accept.foreign.ip=false&qos.enable=true&qos.port=55555&release=2.7.0&remote.application=xpz-provider&sayHello.timeout=1111&side=consumer&timeout=999
int timeout = getUrl().getMethodParameter(methodName, Constants.TIMEOUT_KEY, Constants.DEFAULT_TIMEOUT);
if (isOneway) {
boolean isSent = getUrl().getMethodParameter(methodName, Constants.SENT_KEY, false);
currentClient.send(inv, isSent);
RpcContext.getContext().setFuture(null);
return new RpcResult();
} else if (isAsync) {
ResponseFuture future = currentClient.request(inv, timeout);
FutureAdapter<Object> futureAdapter = new FutureAdapter<>(future);
RpcContext.getContext().setFuture(futureAdapter);
Result result;
if (isAsyncFuture) {
result = new AsyncRpcResult(futureAdapter, futureAdapter.getResultFuture(), false);
} else {
result = new SimpleAsyncRpcResult(futureAdapter, futureAdapter.getResultFuture(), false);
}
return result;
} else {
RpcContext.getContext().setFuture(null);
// currentClient.request方法发起远程调用
// get方法进行超时判断
return (Result) currentClient.request(inv, timeout).get();
}
} catch (TimeoutException e) {
throw new RpcException(RpcException.TIMEOUT_EXCEPTION, "Invoke remote method timeout. method: " + invocation.getMethodName() + ", provider: " + getUrl() + ", cause: " + e.getMessage(), e);
} catch (RemotingException e) {
throw new RpcException(RpcException.NETWORK_EXCEPTION, "Failed to invoke remote method: " + invocation.getMethodName() + ", provider: " + getUrl() + ", cause: " + e.getMessage(), e);
}
}
}
public int getMethodParameter(String method, String key, int defaultValue) {
// 获取sayHello.timeout属性不为空则直接返回
// sayHello.timeout正是由方法级别生成优先级最高
String methodKey = method + "." + key;
Number n = getNumbers().get(methodKey);
if (n != null) {
return n.intValue();
}
// 获取timeout属性如果为空则返回默认值
String value = getMethodParameter(method, key);
if (StringUtils.isEmpty(value)) {
return defaultValue;
}
int i = Integer.parseInt(value);
getNumbers().put(methodKey, i);
return i;
}
3 消费者超时机制
public class DubboInvoker<T> extends AbstractInvoker<T> {
@Override
protected Result doInvoke(final Invocation invocation) throws Throwable {
try {
// get方法进行超时判断
// currentClient.request方法发起远程调用
return (Result) currentClient.request(inv, timeout).get();
} catch (TimeoutException e) {
throw new RpcException(RpcException.TIMEOUT_EXCEPTION, "Invoke remote method timeout. method: " + invocation.getMethodName() + ", provider: " + getUrl() + ", cause: " + e.getMessage(), e);
} catch (RemotingException e) {
throw new RpcException(RpcException.NETWORK_EXCEPTION, "Failed to invoke remote method: " + invocation.getMethodName() + ", provider: " + getUrl() + ", cause: " + e.getMessage(), e);
}
}
}
DefaultFuture尝试接收响应结果,如果阻塞达到超时时间响应结果还是为空,那么消费者会抛出超时异常
public class DefaultFuture implements ResponseFuture {
@Override
public Object get(int timeout) throws RemotingException {
if (timeout <= 0) {
timeout = Constants.DEFAULT_TIMEOUT;
}
// 如果response对象为空
if (!isDone()) {
long start = System.currentTimeMillis();
lock.lock();
try {
while (!isDone()) {
// 放弃锁并使当前线程等待,直到发出信号或中断它,或者达到超时时间
done.await(timeout, TimeUnit.MILLISECONDS);
if (isDone()) {
break;
}
if(System.currentTimeMillis() - start > timeout) {
break;
}
}
} catch (InterruptedException e) {
throw new RuntimeException(e);
} finally {
lock.unlock();
}
// 如果response对象仍然为空则抛出超时异常
if (!isDone()) {
throw new TimeoutException(sent > 0, channel, getTimeoutMessage(false));
}
}
return returnFromResponse();
}
@Override
public boolean isDone() {
return response != null;
}
private void doReceived(Response res) {
lock.lock();
try {
// 接收到服务器响应赋值response
response = res;
if (done != null) {
// 唤醒get方法中处于等待的代码块
done.signal();
}
} finally {
lock.unlock();
}
if (callback != null) {
invokeCallback(callback);
}
}
}
4 生产者超时机制
生产者超时机制体现在TimeoutFilter过滤器,需要注意生产者超时只记录一条日志,流程继续进行,不会抛出异常或者中断
@Activate(group = Constants.PROVIDER)
public class TimeoutFilter implements Filter {
private static final Logger logger = LoggerFactory.getLogger(TimeoutFilter.class);
@Override
public Result invoke(Invoker<?> invoker, Invocation invocation) throws RpcException {
long start = System.currentTimeMillis();
Result result = invoker.invoke(invocation);
long elapsed = System.currentTimeMillis() - start;
// 只读取生产者配置
int timeout = invoker.getUrl().getMethodParameter(invocation.getMethodName(), "timeout", Integer.MAX_VALUE);
// 如果超时只记录一条日志流程继续进行
if (invoker.getUrl() != null && elapsed > timeout ) {
if (logger.isWarnEnabled()) {
logger.warn("invoke time out method: " + invocation.getMethodName() + " arguments: " + Arrays.toString(invocation.getArguments()) + " , url is " + invoker.getUrl() + ", invoke elapsed " + elapsed + " ms.");
}
}
return result;
}
}
5 合理设置超时时间
我们设想这样一种场景:业务系统调用订单中心服务查询订单信息,由于业务系统没有合理设置超时时间,用户长时间得不到响应会反复查询订单信息,所以无论上游系统还是下游系统都可能因为流量激增导致系统崩溃,这就是系统雪崩。
消费者需要了解生产者服务大概率响应时间,设置消费者超时时间略长于大概率响应时间。如果无需同步响应可以采用Failback集群容错策略或者异步调用。消费者和生产者都需要做好限流、降级、熔断策略保护系统,防止出现系统雪崩这类严重问题。