常用的四种限流算法
参考我的另一篇博客:常用的四种限流算法图解
TPSLimiter
TPSLimiter用于服务提供者,提供限流功能
判断在配置的时间间隔内是否允许对服务提供者方法的特定调用,主要由调用间隔和速率
源码分析:
主要涉及三个类:
- TPSLimiter
- DefaultTPSLimiter
- StatItem
/**
* 限制服务或特定方法的TPS(每秒事务数)。
* Service或method url可以定义<b>tps</b>或<b>tps.interval</b>来控制。默认使用{@link DefaultTPSLimiter}
* 作为它的限制检查器。如果提供者服务方法配置为<b>tps</b>(可选的<b>tp.interval</b>),
* 则调用计数超过配置的<b>tps</b>值(默认值为-1意味着无限),那么调用将得到RpcException。
* */
@Activate(group = CommonConstants.PROVIDER, value = TPS_LIMIT_RATE_KEY)
public class TpsLimitFilter implements Filter {
private final TPSLimiter tpsLimiter = new DefaultTPSLimiter();
@Override
public Result invoke(Invoker<?> invoker, Invocation invocation) throws RpcException {
// tps默认实现计数器限流算法
if (!tpsLimiter.isAllowable(invoker.getUrl(), invocation)) {
throw new RpcException(
"Failed to invoke service " +
invoker.getInterface().getName() +
"." +
invocation.getMethodName() +
" because exceed max service tps.");
}
return invoker.invoke(invocation);
}
}
关键代码在tpsLimiter.isAllowable()
,它调用了DefaultTPSLimiter
/**
* DefaultTPSLimiter是tps过滤器的默认实现。
* 它是一个存储tps信息的基于内存的实现。它在内部使用
*
* @see org.apache.dubbo.rpc.filter.TpsLimitFilter
*/
public class DefaultTPSLimiter implements TPSLimiter {
/**
* 存储每个服务的key和每个服务限制的请求数
*/
private final ConcurrentMap<String, StatItem> stats = new ConcurrentHashMap<String, StatItem>();
@Override
public boolean isAllowable(URL url, Invocation invocation) {
// 最大请求数
int rate = url.getParameter(TPS_LIMIT_RATE_KEY, -1);
// tps间隔是60s
long interval = url.getParameter(TPS_LIMIT_INTERVAL_KEY, DEFAULT_TPS_LIMIT_INTERVAL);
// 服务唯一key
String serviceKey = url.getServiceKey();
if (rate > 0) {
// 获取计数器
StatItem statItem = stats.get(serviceKey);
if (statItem == null) {
stats.putIfAbsent(serviceKey, new StatItem(serviceKey, rate, interval));
statItem = stats.get(serviceKey);
} else {
// rate或间隔有修改,则重新构建
if (statItem.getRate() != rate || statItem.getInterval() != interval) {
stats.put(serviceKey, new StatItem(serviceKey, rate, interval));
statItem = stats.get(serviceKey);
}
}
// 是否允许
return statItem.isAllowable();
}
// 默认不限速
else {
StatItem statItem = stats.get(serviceKey);
if (statItem != null) {
stats.remove(serviceKey);
}
}
return true;
}
}
通过StatItem对象来维护每个serverKey的限流:
/**
* 计数器限流算法
* <p>
* 判断在配置的时间间隔内是否允许对服务提供者方法的特定调用。
* 作为一种状态,它包含键名(例如方法),最后一次调用时间、间隔和速率计数。
*/
class StatItem {
private String name;
private long lastResetTime;
private long interval;
private LongAdder token;
private int rate;
StatItem(String name, int rate, long interval) {
this.name = name;
this.rate = rate;
this.interval = interval;
this.lastResetTime = System.currentTimeMillis();
this.token = buildLongAdder(rate);
}
/**
* 判断是否还有剩余token,并把token数减1
* @return
*/
public boolean isAllowable() {
long now = System.currentTimeMillis();
// 计数间隔时间到,重置计数器token
if (now > lastResetTime + interval) {
// 重新设置token的个数
token = buildLongAdder(rate);
// 更新最后重置时间
lastResetTime = now;
}
// 如果token小于0则不允许
if (token.sum() < 0) {
return false;
}
// 将token数-1
token.decrement();
return true;
}
public long getInterval() {
return interval;
}
public int getRate() {
return rate;
}
long getLastResetTime() {
return lastResetTime;
}
long getToken() {
return token.sum();
}
@Override
public String toString() {
return new StringBuilder(32).append("StatItem ")
.append("[name=").append(name).append(", ")
.append("rate = ").append(rate).append(", ")
.append("interval = ").append(interval).append("]")
.toString();
}
private LongAdder buildLongAdder(int rate) {
LongAdder adder = new LongAdder();
adder.add(rate);
return adder;
}
}
真正的限流是在StateItem实现,通过调用isAllowable()方法来判断此serverKey是否允许调用
- 使用ConcurrentHashMap来存储StatItem,其key为URL中的serviceKey
- isAllowable方法从URL中读取tps参数,默认为-1,小于0则从ConcurrentHashMap中移除,大于0则创建或者获取StatItem,调用StatItem的isAllowable(重置或递减token并返回结果)
- StatItem定义了LongAdder类型的token,其isAllowable方法会判断是否需要重置token,如果需要则使用buildLongAdder重置token,不需要的话则在token.sum() < 0时返回false,如果大于等于0则递减token
ExecuteLimitFilter(服务端限流)
在服务提供者中通过executes
统一配置开启,表示每个服务的每个方法最大可并行执行的请求数
ExecuteLimitFilter通过信号量来实现对服务端的并发数控制
代码实现:
/**
* 提供者的每个方法每个服务的最大并行执行请求计数。
* 如果配置的最大执行被设置为10,并且如果调用已经是10的请求,那么它将抛出异常。
* 它继续同样的行为直到它小于10。
*/
@Activate(group = CommonConstants.PROVIDER, value = EXECUTES_KEY) // 提供者 并且配置 executes
public class ExecuteLimitFilter implements Filter, Filter.Listener {
private static final String EXECUTE_LIMIT_FILTER_START_TIME = "execute_limit_filter_start_time";
@Override
public Result invoke(Invoker<?> invoker, Invocation invocation) throws RpcException {
URL url = invoker.getUrl();
String methodName = invocation.getMethodName();
// 最大请求数
int max = url.getMethodParameter(methodName, EXECUTES_KEY, 0);
// note 判断是否可以通过,不通过则抛出RpcException
if (!RpcStatus.beginCount(url, methodName, max)) {
throw new RpcException(RpcException.LIMIT_EXCEEDED_EXCEPTION,
"Failed to invoke method " + invocation.getMethodName() + " in provider " +
url + ", cause: The service using threads greater than <dubbo:service executes=\"" + max +
"\" /> limited.");
}
// 之后会取出该值来计算执行耗时
invocation.put(EXECUTE_LIMIT_FILTER_START_TIME, System.currentTimeMillis());
try {
// 继续调用
return invoker.invoke(invocation);
} catch (Throwable t) {
if (t instanceof RuntimeException) {
throw (RuntimeException) t;
} else {
throw new RpcException("unexpected exception when ExecuteLimitFilter", t);
}
}
}
@Override
public void onResponse(Result appResponse, Invoker<?> invoker, Invocation invocation) {
RpcStatus.endCount(invoker.getUrl(), invocation.getMethodName(), getElapsed(invocation), true);
}
@Override
public void onError(Throwable t, Invoker<?> invoker, Invocation invocation) {
if (t instanceof RpcException) {
RpcException rpcException = (RpcException) t;
if (rpcException.isLimitExceed()) {
return;
}
}
RpcStatus.endCount(invoker.getUrl(), invocation.getMethodName(), getElapsed(invocation), false);
}
/**
* getElapsed⽅法取出 execute_limit_filter_start_time 值,计算执⾏耗时
*
* @param invocation
* @return
*/
private long getElapsed(Invocation invocation) {
Object beginTime = invocation.get(EXECUTE_LIMIT_FILTER_START_TIME);
return beginTime != null ? System.currentTimeMillis() - (Long) beginTime : 0;
}
}
关键代码在RpcStatus.beginCount(url, methodName, max)
,进入RpcStatus看看
public class RpcStatus {
private static final ConcurrentMap<String, RpcStatus> SERVICE_STATISTICS = new ConcurrentHashMap<String,
RpcStatus>();
...
public static void beginCount(URL url, String methodName) {
beginCount(url, methodName, Integer.MAX_VALUE);
}
/**
* @param url
*/
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);
// 最大活跃数==最大数直接短路返回false
if (methodStatus.active.get() == Integer.MAX_VALUE) {
return false;
}
for (int i; ; ) {
i = methodStatus.active.get();
// 活跃数+1>配置的最大活跃数返回false
if (i == Integer.MAX_VALUE || i + 1 > max) {
return false;
}
if (methodStatus.active.compareAndSet(i, i + 1)) {
break;
}
}
// 活跃数+1,活跃数在限流和负载均衡均有用
appStatus.active.incrementAndGet();
return true;
}
/**
*
* @param url
* @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);
}
}
}
...
}
ExecuteLimitFilter限流流程:
- 首先会先去获得提供者服务每个方法最大可并行执行请求数
- 调用
beginCount()
方法判断是否能够获得一个信号量 - 如果返回false,则抛出RpcException异常
- 如果返回true,则调用服务
- 服务结束会调用
endCount()
释放信号量
ActiveLimitFilter(客户端限流)
ActiveLimitFilter限制客户端对服务或服务方法的并发客户端调用
配置示例:
<dubbo:reference id="demoService" check="false" interface="org.apache.dubbo.demo.DemoService" "actives"="2"/>
在上面的例子中,最多允许2个并发调用。
如果不止配置的(在本例2中)试图调用远程方法,那么剩余的调用将等待配置超时(默认为0秒),直到调用被dubbo杀死。
源码解析:
基本过程与服务端类似
@Activate(group = CONSUMER, value = ACTIVES_KEY) // 消费端 并且配置 actives 才生效
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();
// 获取方法 actives 属性值 默认是0 ,这actives 就是"每服务消费者每服务每方法最大并发调用数"
int max = invoker.getUrl().getMethodParameter(methodName, ACTIVES_KEY, 0);
// 获取对应url 对应method的一个RpcStatus
final RpcStatus rpcStatus = RpcStatus.getStatus(invoker.getUrl(), invocation.getMethodName());
// 开始计数(如果超过最大活跃数或等于上限返回false)
if (!RpcStatus.beginCount(url, methodName, max)) {
// 获取超时时间
long timeout = invoker.getUrl().getMethodParameter(invocation.getMethodName(), TIMEOUT_KEY, 0);
long start = System.currentTimeMillis();
long remain = 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(RpcException.LIMIT_EXCEEDED_EXCEPTION,
"Waiting concurrent invoke timeout in client-side for service: " +
invoker.getInterface().getName() + ", method: " + invocation.getMethodName() +
", elapsed: " + elapsed + ", timeout: " + timeout + ". concurrent invokes: " +
rpcStatus.getActive() + ". max concurrent invoke limit: " + max);
}
}
}
}
// 开始时间统计
invocation.put(ACTIVELIMIT_FILTER_START_TIME, System.currentTimeMillis());
// 执行
return invoker.invoke(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);
}
@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;
}
private void notifyFinish(final RpcStatus rpcStatus, int max) {
if (max > 0) {
synchronized (rpcStatus) {
// 执行完成唤醒等待线程
rpcStatus.notifyAll();
}
}
}
}
RpcStatus使用的与服务端是一样的
流程基本与服务端类似,不同的是在客户端有超时判断
- 获取每服务消费者中每个方法最大并发调用数
- 获取对应url 对应method的一个RpcStatus
- 调用
beginCount()
如果超过最大活跃数或等于上限,则返回false进入超时判断 - 如果超过超时时间则抛出RpcException
- 如果调用
beginCount()
没有超过上限,则调用服务 - 服务调用完成后会回调
onError()
或onResponse()
方法,方法中会唤醒正在等待的服务调用请求线程
总结
限流对象 | 限流算法 | 限速应用方 |
---|---|---|
TpsLimitFilter | 计数器 | 服务提供方(服务端) |
ExecuteLimitFilter | 信号量的方式 | 服务提供方(服务端) |
ActiveLimitFilter | 信号量的方式 | 服务消费方(客户端) |
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/17842.html