一. 概述
版本:2.7.8
解決問題
- 對服務方法或服務接口中所有服務方法併發調用請求進行限制
調用時機
- 消費端通過 org.apache.dubbo.rpc.filter.ActiveLimitFilter 進行限制
- 消費端在使用 DubboInvoker 的 invoke() 方法真正向遠端發起RPC請求前,先調用 ActiveLimitFilter 的 invoke() 方法限制
二. 源碼分析
RpcStatus類
源碼說明:
- 該類表示提供端服務接口(包括接口中所有服務方法)、消費端服務接口(包括接口中所有服務方法)的當前調用次數、總數、失敗數、調用間隔等狀態信息
- 代碼中有詳細註釋,重點關注beginCount方法、endCount方法
- SERVICE_STATISTICS/METHOD_STATISTICS是靜態變量,相當於緩存
package org.apache.dubbo.rpc;
public class RpcStatus {
/**
* key 爲服務接口(url.toIdentityString()),value 爲該服務接口中所有方法的 RpcStatus 狀態
* 靜態變量:所有服務接口共用(緩存)
*/
private static final ConcurrentMap<String, RpcStatus> SERVICE_STATISTICS = new ConcurrentHashMap<>();
/**
* key 爲服務接口(url.toIdentityString()),value 爲 ConcurrentMap<String,RpcStatus> 對象,其中key爲具體的服務方法,value爲服務方法的 RpcStatus 狀態
* 靜態變量:所有服務接口共用(緩存)
*/
private static final ConcurrentMap<String, ConcurrentMap<String, RpcStatus>> METHOD_STATISTICS = new ConcurrentHashMap<>();
private final ConcurrentMap<String, Object> values = new ConcurrentHashMap<>();
// 服務方法正在執行中的數量
private final AtomicInteger active = new AtomicInteger();
// 服務方法調用的總數量
private final AtomicLong total = new AtomicLong();
// 服務方法調用失敗的數量
private final AtomicInteger failed = new AtomicInteger();
private final AtomicLong totalElapsed = new AtomicLong();
private final AtomicLong failedElapsed = new AtomicLong();
private final AtomicLong maxElapsed = new AtomicLong();
private final AtomicLong failedMaxElapsed = new AtomicLong();
private final AtomicLong succeededMaxElapsed = new AtomicLong();
/**
* 根據 URL 返回服務接口的 RpcStatus 狀態
*/
public static RpcStatus getStatus(URL url) {
String uri = url.toIdentityString();
return SERVICE_STATISTICS.computeIfAbsent(uri, key -> new RpcStatus());
}
/**
* 根據 URL 返回接口中服務方法的 RpcStatus 狀態
*/
public static RpcStatus getStatus(URL url, String methodName) {
String uri = url.toIdentityString();
ConcurrentMap<String, RpcStatus> map = METHOD_STATISTICS.computeIfAbsent(uri, k -> new ConcurrentHashMap<>());
return map.computeIfAbsent(methodName, k -> new RpcStatus());
}
/**
* 服務方法執行前判斷是否滿足併發控制要求
*
* @param url
* @param methodName 服務方法
* @param max 併發控制的數量
* @return false:併發數已達到,掛起當前線程,等待喚醒
* @return true: 併發數未達到,可以執行
*/
public static boolean beginCount(URL url, String methodName, int max) {
max = (max <= 0) ? Integer.MAX_VALUE : max;
RpcStatus appStatus = getStatus(url);
RpcStatus methodStatus = getStatus(url, methodName);
if (methodStatus.active.get() == Integer.MAX_VALUE) {
return false;
}
for (int i; ; ) {
// 服務方法正在調用中的數量
i = methodStatus.active.get();
// 超過配置的併發數,返回false
if (i + 1 > max) {
return false;
}
// 併發數未達到,返回true
if (methodStatus.active.compareAndSet(i, i + 1)) {
break;
}
}
// 接口所有服務方法正在執行中的數量加1
appStatus.active.incrementAndGet();
return true;
}
/**
* 服務方法執行後減少相關參數值(包括併發控制次數)
*
* @param url
* @param methodName
* @param elapsed
* @param succeeded
*/
public static void endCount(URL url, String methodName, long elapsed, boolean succeeded) {
// 服務接口
endCount(getStatus(url), elapsed, succeeded);
// 服務方法
endCount(getStatus(url, methodName), elapsed, succeeded);
}
private static void endCount(RpcStatus status, long elapsed, boolean succeeded) {
status.active.decrementAndGet();
status.total.incrementAndGet();
status.totalElapsed.addAndGet(elapsed);
if (status.maxElapsed.get() < elapsed) {
status.maxElapsed.set(elapsed);
}
if (succeeded) {
if (status.succeededMaxElapsed.get() < elapsed) {
status.succeededMaxElapsed.set(elapsed);
}
} else {
status.failed.incrementAndGet();
status.failedElapsed.addAndGet(elapsed);
if (status.failedMaxElapsed.get() < elapsed) {
status.failedMaxElapsed.set(elapsed);
}
}
}
}
ActiveLimitFilter 過濾器
源碼說明:
- 是消費端並且URL有指定key(actives) Filter才生效(@Activate(group = CONSUMER, value = ACTIVES_KEY))
- invoke:調用 RpcStatus.beginCount 判斷是否滿足併發控制條件
- RpcStatus.beginCount返回false:則讓當前線程掛起,之後會在timeout時間後被喚醒,並拋出RpcException異常。
也可能在其它消費端正常執行完或異常後調用 notifyFinish() 方法喚醒。
如果超過timeout時間還沒被喚醒,則當前線程會自動被喚醒,然後拋出RpcException異常。- onResponse/onError:方法執行完成或異常調用RpcStatus.endCount減去相應數量
- onResponse/onError:會調用notifyFinish通知掛起的消費者
package org.apache.dubbo.rpc.filter;
@Activate(group = CONSUMER, value = ACTIVES_KEY)
public class ActiveLimitFilter implements Filter, Filter.Listener {
private static final String ACTIVELIMIT_FILTER_START_TIME = "activelimit_filter_start_time";
@Override
public Result invoke(Invoker<?> invoker, Invocation invocation) throws RpcException {
URL url = invoker.getUrl();
String methodName = invocation.getMethodName();
// 併發調用次數
int max = invoker.getUrl().getMethodParameter(methodName, ACTIVES_KEY, 0);
final RpcStatus rpcStatus = RpcStatus.getStatus(invoker.getUrl(), invocation.getMethodName());
// 判斷是不是超過併發限制
// RpcStatus 根據 URL 和調用方法名獲取對應方法的RPC狀態對象
// RpcStatus.beginCount 返回false :則讓當前線程掛起,之後會在 timeout 時間後被喚醒,並拋出 RpcException 異常。
// 也可能在其它消費端正常執行完或異常後調用 notifyFinish() 方法喚醒。
if (!RpcStatus.beginCount(url, methodName, max)) {
long timeout = invoker.getUrl().getMethodParameter(invocation.getMethodName(), TIMEOUT_KEY, 0);
long start = System.currentTimeMillis();
long remain = timeout;
// 超過併發限制則阻塞當前線程timeout時間,並重試
synchronized (rpcStatus) {
while (!RpcStatus.beginCount(url, methodName, max)) {
try {
rpcStatus.wait(remain);
} catch (InterruptedException e) {
// ignore
}
long elapsed = System.currentTimeMillis() - start;
remain = timeout - elapsed;
if (remain <= 0) {
throw new RpcException();
}
}
}
}
invocation.put(ACTIVELIMIT_FILTER_START_TIME, System.currentTimeMillis());
return invoker.invoke(invocation);
}
/**
* 服務方法正常返回
* @param appResponse
* @param invoker
* @param invocation
*/
@Override
public void onResponse(Result appResponse, Invoker<?> invoker, Invocation invocation) {
String methodName = invocation.getMethodName();
URL url = invoker.getUrl();
int max = invoker.getUrl().getMethodParameter(methodName, ACTIVES_KEY, 0);
RpcStatus.endCount(url, methodName, getElapsed(invocation), true);
notifyFinish(RpcStatus.getStatus(url, methodName), max);
}
/**
* 服務方法異常
* @param t
* @param invoker
* @param invocation
*/
@Override
public void onError(Throwable t, Invoker<?> invoker, Invocation invocation) {
String methodName = invocation.getMethodName();
URL url = invoker.getUrl();
int max = invoker.getUrl().getMethodParameter(methodName, ACTIVES_KEY, 0);
if (t instanceof RpcException) {
RpcException rpcException = (RpcException) t;
if (rpcException.isLimitExceed()) {
return;
}
}
RpcStatus.endCount(url, methodName, getElapsed(invocation), false);
notifyFinish(RpcStatus.getStatus(url, methodName), max);
}
private long getElapsed(Invocation invocation) {
Object beginTime = invocation.get(ACTIVELIMIT_FILTER_START_TIME);
return beginTime != null ? System.currentTimeMillis() - (Long) beginTime : 0;
}
/**
* 服務方法正常返回或異常後:會調用 notifyAll 通知掛起的消費者
* @param rpcStatus
* @param max
*/
private void notifyFinish(final RpcStatus rpcStatus, int max) {
if (max > 0) {
synchronized (rpcStatus) {
rpcStatus.notifyAll();
}
}
}
}
三. 使用
public static void main(String[] args) {
// 1.創建服務引用對象實例
ReferenceConfig<GreetingService> referenceConfig = new ReferenceConfig<>();
// 2.設置應用程序信息
referenceConfig.setApplication(new ApplicationConfig("dubbo-consumer"));
// 3.設置服務註冊中心
referenceConfig.setRegistry(new RegistryConfig("ZKAddress"));
// 4.設置服務接口和超時時間
referenceConfig.setInterface(GreetingService.class);
referenceConfig.setTimeout(5000);
// 5. 服務接口所有方法
referenceConfig.setActives(10);
// 6. 指定服務方法
final List<MethodConfig> methodList = new ArrayList<MethodConfig>();
MethodConfig methodConfig = new MethodConfig();
methodConfig.setActives(10);
methodConfig.setName("sayHello");
methodList.add(methodConfig);
referenceConfig.setMethods(methodList);
// 7.設置服務分組與版本
referenceConfig.setVersion("1.0.0");
referenceConfig.setGroup("dubbo");
// 8.引用服務
GreetingService greetingService = referenceConfig.get();
}