既然我们已经使用springboot+dubbo 那么也不放继续深入下去了【springcloud确实各种特性很垂涎哈!】
对于大量的请求我们存在一些限流需求——秒杀活动【比如锁定库存接口==》目前使用http请求】后续存在改造需求
因此来看一下我们如何实现限流吧
http限流
没有上springcloud的zuul之前我们使用nginx/openresty作为反向代理
那么做一些业务无关的限制请求自然在nginx这一层完成
其实存在一些第三方比较好用的网关过滤等【包括限流 验签等等】典型的包括kong orange等
我们直接使用nginx的limitzone来完成。
limit_req_zone $binary_remote_addr zone=req_one:10m rate=1r/s;
The ngx_http_limit_req_module module (0.7.21) is used to limit the request processing rate per a defined key, in particular, the processing rate of requests coming from a single IP address. The limitation is done using the “leaky bucket” method.
很明显这个表示使用ip进行限流 zone名称为req_one 分配了10m 空间使用漏桶算法 每秒钟允许1个请求
当我们使用的时候只需要使用刚才的名称即可
limit_req zone=req_three burst=20;
这边burst表示可以瞬间超过20个请求 由于没有noDelay参数因此需要排队 如果超过这20个那么直接返回503
dubbo
dubbo提供了多个和请求相关的filter 我们可以看到
ActiveLimitFilter ExecuteLimitFilter TPSLimiterFilter
三个侧重点各有不同
ActiveLimitFilter
@Activate(group = Constants.CONSUMER, value = Constants.ACTIVES_KEY)
很明显这是作用在客户端的
主要作用如下
控制客户端同样的方法可同时运行的次数【即该方法的并发度】
public Result invoke(Invoker<?> invoker, Invocation invocation) throws RpcException {
URL url = invoker.getUrl();
String methodName = invocation.getMethodName();
int max = invoker.getUrl().getMethodParameter(methodName, Constants.ACTIVES_KEY, 0);
RpcStatus count = RpcStatus.getStatus(invoker.getUrl(), invocation.getMethodName());
if (max > 0) {
long timeout = invoker.getUrl().getMethodParameter(invocation.getMethodName(), Constants.TIMEOUT_KEY, 0);
long start = System.currentTimeMillis();
long remain = timeout;
int active = count.getActive();
if (active >= max) {
synchronized (count) {
while ((active = count.getActive()) >= max) {
try {
count.wait(remain);
} catch (InterruptedException e) {
}
long elapsed = System.currentTimeMillis() - start;
remain = timeout - elapsed;
if (remain <= 0) {
throw new RpcException("Waiting concurrent invoke timeout in client-side for service: "
+ invoker.getInterface().getName() + ", method: "
+ invocation.getMethodName() + ", elapsed: " + elapsed
+ ", timeout: " + timeout + ". concurrent invokes: " + active
+ ". max concurrent invoke limit: " + max);
}
}
}
}
}
try {
long begin = System.currentTimeMillis();
RpcStatus.beginCount(url, methodName);
try {
Result result = invoker.invoke(invocation);
RpcStatus.endCount(url, methodName, System.currentTimeMillis() - begin, true);
return result;
} catch (RuntimeException t) {
RpcStatus.endCount(url, methodName, System.currentTimeMillis() - begin, false);
throw t;
}
} finally {
if(max>0){
synchronized (count) {
count.notify();
}
}
}
}
很明显当超过了指定的active值之后该请求将等待前面的请求完成【何时结束呢?依赖于该方法的timeout 如果没有设置timeout的话可能就是多个请求一直被阻塞然后等待随机唤醒吧……】
因此要搭配timeout一起使用噢!
ExecuteLimitFilter
@Activate(group = Constants.PROVIDER, value = Constants.EXECUTES_KEY)
很明显这是指在服务端的限制了
这个就简单了
public Result invoke(Invoker<?> invoker, Invocation invocation) throws RpcException {
URL url = invoker.getUrl();
String methodName = invocation.getMethodName();
int max = url.getMethodParameter(methodName, Constants.EXECUTES_KEY, 0);
if (max > 0) {
RpcStatus count = RpcStatus.getStatus(url, invocation.getMethodName());
if (count.getActive() >= max) {
throw new RpcException(“Failed to invoke method " + invocation.getMethodName() + " in provider " + url + “, cause: The service using threads greater than <dubbo:service executes=”” + max + “” /> limited.");
}
}
long begin = System.currentTimeMillis();
boolean isException = false;
RpcStatus.beginCount(url, methodName);
try {
Result result = invoker.invoke(invocation);
return result;
} catch (Throwable t) {
isException = true;
if(t instanceof RuntimeException) {
throw (RuntimeException) t;
}
else {
throw new RpcException(“unexpected exception when ExecuteLimitFilter”, t);
}
}
finally {
RpcStatus.endCount(url, methodName, System.currentTimeMillis() - begin, isException);
}
}
一旦超出指定的数目直接报错 其实是指在服务端的并行度【需要注意这些都是指的是在单台服务上而不是整个服务集群】
TpsLimitFilter
@Activate(group = Constants.PROVIDER, value = Constants.TPS_LIMIT_RATE_KEY)
同样作用在服务端 目的在于控制一段时间类中的请求数
public class DefaultTPSLimiter implements TPSLimiter {
private final ConcurrentMap<String, StatItem> stats
= new ConcurrentHashMap<String, StatItem>();
public boolean isAllowable(URL url, Invocation invocation) {
int rate = url.getParameter(Constants.TPS_LIMIT_RATE_KEY, -1);
long interval = url.getParameter(Constants.TPS_LIMIT_INTERVAL_KEY,
Constants.DEFAULT_TPS_LIMIT_INTERVAL);
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);
}
return statItem.isAllowable(url, invocation);
} else {
StatItem statItem = stats.get(serviceKey);
if (statItem != null) {
stats.remove(serviceKey);
}
}
return true;
}
}
默认情况下取得tps.interval字段表示请求间隔 如果无法找到则使用60s 根据tps字段表示允许调用次数
class StatItem {
private String name;
private long lastResetTime;
private long interval;
private AtomicInteger 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 = new AtomicInteger(rate);
}
public boolean isAllowable(URL url, Invocation invocation) {
long now = System.currentTimeMillis();
if (now > lastResetTime + interval) {
token.set(rate);
lastResetTime = now;
}
int value = token.get();
boolean flag = false;
while (value > 0 && !flag) {
flag = token.compareAndSet(value, value - 1);
value = token.get();
}
return flag;
}
long getLastResetTime() {
return lastResetTime;
}
int getToken() {
return token.get();
}
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();
}
使用AtomicInteger表示允许调用的次数 每次调用减少1次当结果小于0之后返回不允许调用
总结
在没有sc之前我们使用这些办法控制我们方法的调用频率【这个都比较基础 如果需要深度定制自然需要再次加入业务 比如某个客户调用多少次 不同的客户次数不同等等逻辑还是需要额外开发!】