一:超时设置
- DUBBO有很多地方可以配置超时时间,可以配置在消费者,可以配置在生产者,可以配置为方法级别,可以配置为接口级别,还可以配置为全局级别,DUBBO官方文档介绍这些配置优先级如下:
第一优先级:方法级 > 接口级 > 全局级
第二优先级:消费者 > 生产者
从源码层面对超时机制进行分析,我们首先分析优先级如何生效,然后再分析超时机制在消费者和生产者分别如何实现。
二:配置优先级
1:消费者 > 生产者
配置生产者接口级别超时时间1000毫秒
<beans>
<dubbo:registry address="zookeeper://127.0.0.1:2181" />
<dubbo:protocol name="dubbo" port="20880" />
<dubbo:service timeout="1000" interface="com.itxpz.dubbo.demo.provider.HelloService" ref="helloService" />
</beans>
配置消费者接口级别超时时间2000毫秒
<beans>
<dubbo:application name="xpz-consumer" />
<dubbo:registry address="zookeeper://127.0.0.1:2181" />
<dubbo:reference timeout="2000" id="helloService" interface="com.itxpz.dubbo.demo.provider.HelloService" />
</beans>
生产者首先注册服务信息至注册中心,消费者从注册中心订阅服务信息,在获取到生产者服务信息后,会将这些配置与消费者配置进行融合,核心在消费者订阅信息后会将服务信息转化为Invokers这一段代码
RegistryDirectory实体类中的toInvokers方法:
/**
* Turn urls into invokers, and if url has been refer, will not re-reference.
*
* @param urls
* @return invokers
*/
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;
}
Set<String> keys = new HashSet<String>();
String queryProtocols = this.queryMap.get(Constants.PROTOCOL_KEY);
for (URL providerUrl : urls) {
// If protocol is configured at the reference side, only the matching protocol is selected
if (queryProtocols != null && queryProtocols.length() > 0) {
boolean accept = false;
String[] acceptProtocols = queryProtocols.split(",");
for (String acceptProtocol : acceptProtocols) {
if (providerUrl.getProtocol().equals(acceptProtocol)) {
accept = true;
break;
}
}
if (!accept) {
continue;
}
}
if (Constants.EMPTY_PROTOCOL.equals(providerUrl.getProtocol())) {
continue;
}
if (!ExtensionLoader.getExtensionLoader(Protocol.class).hasExtension(providerUrl.getProtocol())) {
logger.error(new IllegalStateException("Unsupported protocol " + providerUrl.getProtocol() + " in notified url: " + providerUrl + " from registry " + getUrl().getAddress() + " to consumer " + NetUtils.getLocalHost()
+ ", supported protocol: " + ExtensionLoader.getExtensionLoader(Protocol.class).getSupportedExtensions()));
continue;
}
// 本文只分析消费者和生产者参数融合
URL url = mergeUrl(providerUrl);
String key = url.toFullString(); // The parameter urls are sorted
if (keys.contains(key)) { // Repeated url
continue;
}
keys.add(key);
// Cache key is url that does not merge with consumer side parameters, regardless of how the consumer combines parameters, if the server url changes, then refer again
Map<String, Invoker<T>> localUrlInvokerMap = this.urlInvokerMap; // local reference
Invoker<T> invoker = localUrlInvokerMap == null ? null : localUrlInvokerMap.get(key);
if (invoker == null) { // Not in the cache, refer again
try {
boolean enabled = true;
if (url.hasParameter(Constants.DISABLED_KEY)) {
enabled = !url.getParameter(Constants.DISABLED_KEY, false);
} else {
enabled = url.getParameter(Constants.ENABLED_KEY, true);
}
if (enabled) {
invoker = new InvokerDelegate<T>(protocol.refer(serviceType, url), url, providerUrl);
}
} catch (Throwable t) {
logger.error("Failed to refer invoker for interface:" + serviceType + ",url:(" + url + ")" + t.getMessage(), t);
}
if (invoker != null) { // Put new invoker in cache
newUrlInvokerMap.put(key, invoker);
}
} else {
newUrlInvokerMap.put(key, invoker);
}
}
keys.clear();
return newUrlInvokerMap;
}
分析消费者和生产者参数融合代码,将生产者和消费者的配置进行整合成一个URL:
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.alibaba.dubbo.rpc.cluster.support;
import com.alibaba.dubbo.common.Constants;
import com.alibaba.dubbo.common.URL;
import java.util.HashMap;
import java.util.Map;
/**
* ClusterUtils
*
*/
public class ClusterUtils {
private ClusterUtils() {
}
public static URL mergeUrl(URL remoteUrl, Map<String, String> localMap) {
Map<String, String> map = new HashMap<String, String>();
Map<String, String> remoteMap = remoteUrl.getParameters();
if (remoteMap != null && remoteMap.size() > 0) {
map.putAll(remoteMap);
// Remove configurations from provider, some items should be affected by provider.
map.remove(Constants.THREAD_NAME_KEY);
map.remove(Constants.DEFAULT_KEY_PREFIX + Constants.THREAD_NAME_KEY);
map.remove(Constants.THREADPOOL_KEY);
map.remove(Constants.DEFAULT_KEY_PREFIX + Constants.THREADPOOL_KEY);
map.remove(Constants.CORE_THREADS_KEY);
map.remove(Constants.DEFAULT_KEY_PREFIX + Constants.CORE_THREADS_KEY);
map.remove(Constants.THREADS_KEY);
map.remove(Constants.DEFAULT_KEY_PREFIX + Constants.THREADS_KEY);
map.remove(Constants.QUEUES_KEY);
map.remove(Constants.DEFAULT_KEY_PREFIX + Constants.QUEUES_KEY);
map.remove(Constants.ALIVE_KEY);
map.remove(Constants.DEFAULT_KEY_PREFIX + Constants.ALIVE_KEY);
map.remove(Constants.TRANSPORTER_KEY);
map.remove(Constants.DEFAULT_KEY_PREFIX + Constants.TRANSPORTER_KEY);
}
// 消费者配置不为空则全部赋值至结果对象
if (localMap != null && localMap.size() > 0) {
map.putAll(localMap);
}
if (remoteMap != null && remoteMap.size() > 0) {
// Use version passed from provider side
String dubbo = remoteMap.get(Constants.DUBBO_VERSION_KEY);
if (dubbo != null && dubbo.length() > 0) {
map.put(Constants.DUBBO_VERSION_KEY, dubbo);
}
String defaultVersion = remoteMap.get(Constants.DEFAULT_KEY_PREFIX + Constants.VERSION_KEY);
if (defaultVersion != null && defaultVersion.length() > 0) {
map.put(Constants.DEFAULT_KEY_PREFIX + Constants.VERSION_KEY, defaultVersion);
}
String defaultGroup = remoteMap.get(Constants.DEFAULT_KEY_PREFIX + Constants.GROUP_KEY);
if (defaultGroup != null && defaultGroup.length() > 0) {
map.put(Constants.DEFAULT_KEY_PREFIX + Constants.GROUP_KEY, defaultGroup);
}
String version = remoteMap.get(Constants.VERSION_KEY);
if (version != null && version.length() > 0) {
map.put(Constants.VERSION_KEY, version);
}
String group = remoteMap.get(Constants.GROUP_KEY);
if (group != null && group.length() > 0) {
map.put(Constants.GROUP_KEY, group);
}
String methods = remoteMap.get(Constants.METHODS_KEY);
if (methods != null && methods.length() > 0) {
map.put(Constants.METHODS_KEY, methods);
}
// Reserve timestamp of provider url.
String remoteTimestamp = remoteMap.get(Constants.TIMESTAMP_KEY);
if (remoteTimestamp != null && remoteTimestamp.length() > 0) {
map.put(Constants.REMOTE_TIMESTAMP_KEY, remoteMap.get(Constants.TIMESTAMP_KEY));
}
// Combine filters and listeners on Provider and Consumer
String remoteFilter = remoteMap.get(Constants.REFERENCE_FILTER_KEY);
String localFilter = localMap.get(Constants.REFERENCE_FILTER_KEY);
if (remoteFilter != null && remoteFilter.length() > 0
&& localFilter != null && localFilter.length() > 0) {
localMap.put(Constants.REFERENCE_FILTER_KEY, remoteFilter + "," + localFilter);
}
String remoteListener = remoteMap.get(Constants.INVOKER_LISTENER_KEY);
String localListener = localMap.get(Constants.INVOKER_LISTENER_KEY);
if (remoteListener != null && remoteListener.length() > 0
&& localListener != null && localListener.length() > 0) {
localMap.put(Constants.INVOKER_LISTENER_KEY, remoteListener + "," + localListener);
}
}
URL result = remoteUrl.clearParameters().addParameters(map);
return result;
}
}
2: 方法级 > 接口级
- 配置消费者接口级别超时时间1000毫秒
<beans>
<dubbo:application name="xpz-consumer" />
<dubbo:registry address="zookeeper://127.0.0.1:2181" />
<dubbo:reference timeout="1000" id="helloService" interface="com.itxpz.dubbo.demo.provider.HelloService" />
</beans>
- 配置生产者方法级别超时时间2000毫秒
<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="2000" />
</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有两个配置,这两个配置优先级在消费者发起远程调用时体现
DubboInvoker实体类:
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);
}
}
}
getMethodParameter方法:
public static final String DEFAULT_KEY_PREFIX = "default.";
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;
}
// 获取方法的超时时间
public String getMethodParameter(String method, String key) {
String value = parameters.get(method + "." + key);
if (value == null || value.length() == 0) {
return getParameter(key);
}
return value;
}
public String getParameter(String key) {
// 获取接口的超时时间
String value = parameters.get(key);
if (value == null || value.length() == 0) {
// 获取默认的超时时间(全局超时时间,如果没有设置,默认是1000,也就是1秒)
value = parameters.get(Constants.DEFAULT_KEY_PREFIX + key);
}
return value;
}
三:消费者超时机制
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);
}
}
}
四:生产者超时机制
生产者超时机制体现在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;
}
}