合法性验证限流
验证码
IP黑名单
容器限流
Tomcat
Tomcat设置最大线程数
Nginx
控制速率 rate
控制并发数
服务端限流
固定时间窗口算法
固定的时间(比如1分钟)划分窗口,每个窗口允许访问N次(限流)
缺点:流量分布不均,不好控制,临界值有问题
滑动时间窗口算法
细分为多个小窗口,多个小窗口构成一个打窗口进行限流。每个小窗口都有自己的计数器,时间跨度更小了,不存在临界值问题
缺点:实现起来较为复杂,每个小窗口都有计数
漏桶算法
漏桶(Leaky Bucket)算法思路很简单,水(请求)先进入到漏桶里,漏桶以一定的速度出水(接口有响应速率),当水流入速度过大会直接溢出(访问频率超过接口响应速率),然后就拒绝请求,可以看出漏桶算法能强行限制数据的传输速率。示意图如下:
示例:
可见这里有两个变量,一个是桶的大小,支持流量突发增多时可以存多少的水(burst),另一个是水桶漏洞的大小(rate),伪代码如下:
double rate; // leak rate in calls/s
double burst; // bucket size in calls
long refreshTime; // time for last water refresh
double water; // water count at refreshTime
refreshWater() {
long now = getTimeOfDay();
//水随着时间流逝,不断流走,最多就流干到0.
water = max(0, water- (now - refreshTime)*rate);
refreshTime = now;
}
bool permissionGranted() {
refreshWater();
if (water < burst) { // 水桶还没满,继续加1
water ++;
return true;
} else {
return false;
}
}
因为漏桶的漏出速率是固定的参数,所以,即使网络中不存在资源冲突(没有发生拥塞),漏桶算法也不能使流突发(burst)到端口速率。因此,漏桶算法对于存在突发特性的流量来说缺乏效率。
令牌桶算法
令牌桶算法(Token Bucket)和漏桶算法的效果一样但方向相反的算法,更加容易理解。随着时间流逝,系统会按恒定1/QPS时间间隔(如果QPS=100,则间隔是10ms)往桶里加入Token(想象和漏洞漏水相反,有个水龙头在不断的加水),如果桶已经满了,令牌就溢出了。如果桶未满,令牌可以积累。新请求来临时,会各自拿走一个Token,如果没有Token可拿了就阻塞或者拒绝服务。
令牌桶算法:一个存放固定容量令牌的桶,按照固定速率(每秒/或者可以自定义时间)往桶里添加令牌,然后每次获取一个令牌,当桶里没有令牌可取时,则拒绝服务。
令牌桶分为2个动作,动作1(固定速率往桶中存入令牌)、动作2(客户端如果想访问请求,先从桶中获取token)
流入:以固定速率从桶中流入水滴
流出:按照任意速率从桶中流出水滴
技术上使用Google开源工具包Guava提供了限流工具类RateLimiter,该类基于令牌桶算法来完成限流,非常易于使用。RateLimiter是guava提供的基于令牌桶算法的实现类,可以非常简单的完成限流特技,并且根据系统的实际情况来调整生成token的速率。 RateLimiter 是单机(单进程)的限流,是JVM级别的的限流,所有的令牌生成都是在内存中,在分布式环境下不能直接这么用。
优点:支持大的并发,有效利用网络带宽
漏桶和令牌桶的区别:
并不能说明令牌桶一定比漏洞好,她们使用场景不一样。
令牌桶算法,放在服务端,用来保护服务端(自己),主要用来对调用者频率进行限流,为的是不让自己被压垮。所以如果自己本身有处理能力的时候,如果流量突发(实际消费能力强于配置的流量限制=桶大小),那么实际处理速率可以超过配置的限制(桶大小)。
而漏桶算法,放在调用方,这是用来保护他人,也就是保护他所调用的系统。主要场景是,当调用的第三方系统本身没有保护机制,或者有流量限制的时候,我们的调用速度不能超过他的限制,由于我们不能更改第三方系统,所以只有在主调方控制。这个时候,即使流量突发,也必须舍弃。因为消费能力是第三方决定的。
限流实现
1、 RateLimiter简介(guava的令牌桶实现) – 单机版
Google开源工具包Guava提供了限流工具类RateLimiter,该类基于令牌桶算法(Token Bucket)来完成限流,非常易于使用.RateLimiter经常用于限制对一些物理资源或者逻辑资源的访问速率.它支持两种获取permits接口,一种是如果拿不到立刻返回false,一种会阻塞等待一段时间看能不能拿到。原理见《guava–RateLimiter源码分析》
RateLimiter和Java中的信号量(java.util.concurrent.Semaphore)类似,Semaphore通常用于限制并发量.
源码注释中的一个例子,比如我们有很多任务需要执行,但是我们不希望每秒超过两个任务执行,那么我们就可以使用RateLimiter:
final RateLimiter rateLimiter = RateLimiter.create(2.0);
void submitTasks(List<Runnable> tasks, Executor executor) {
for (Runnable task : tasks) {
rateLimiter.acquire(); // may wait
executor.execute(task);
}
}
另外一个例子,假如我们会产生一个数据流,然后我们想以每秒5kb的速度发送出去.我们可以每获取一个令牌(permit)就发送一个byte的数据,这样我们就可以通过一个每秒5000个令牌的RateLimiter来实现:
final RateLimiter rateLimiter = RateLimiter.create(5000.0);
void submitPacket(byte[] packet) {
rateLimiter.acquire(packet.length);
networkService.send(packet);
}
另外,我们也可以使用非阻塞的形式达到降级运行的目的,即使用非阻塞的tryAcquire()方法:
if(limiter.tryAcquire()) { //未请求到limiter则立即返回false
doSomething();
}else{
doSomethingElse();
}
AOP实现限流
1、JDK动态代理
@Aspect
public class ApiRateLimiterAspect extends ApiRateLimiterAbstractImpl {
@Pointcut("@annotation(com.jd.wx.common.api.limit.ApiRateLimiter)")
public void annotationPoint() {
}
@Around("annotationPoint()")
public Object execAnnotation(ProceedingJoinPoint jp) throws Throwable {
try {
if (jp.getSignature() instanceof MethodSignature) {
MethodSignature methodSignature = (MethodSignature) jp.getSignature();
Method method = methodSignature.getMethod();
ApiRateLimiter apiRateLimiter = method.getAnnotation(ApiRateLimiter.class);
if (!tryAcquire(apiRateLimiter, methodSignature.getMethod(), jp.getArgs())) {
return generateReturnObject(apiRateLimiter, method.getReturnType());
}
}
} catch (TryAcquireException t) {
throw t;
} catch (Throwable t) {
}
return jp.proceed();
}
}
2、cglib动态代理
public class ApiRateLimiterInterceptor extends ApiRateLimiterAbstractImpl implements MethodInterceptor {
public Object invoke(MethodInvocation invocation) throws Throwable {
try {
Method method = invocation.getMethod();
if (method != null) {
ApiRateLimiter apiRateLimiter = method.getAnnotation(ApiRateLimiter.class);
if (!tryAcquire(apiRateLimiter, method, invocation.getArguments())) {
return generateReturnObject(apiRateLimiter, method.getReturnType());
}
}
} catch (TryAcquireException t) {
throw t;
} catch (Throwable t) {
}
return invocation.proceed();
}
}
公用的tryAcquire方法:
public class ApiRateLimiterAbstractImpl extends AbstractCommonImpl {
/** 各接口限流RateLimiter集合 */
private static final ConcurrentMap<String, RateLimiter> API_RESOURCE_LIMITER_MAP = Maps.newConcurrentMap();
private static final String GLOBAL_SWITCH = "isSwitchOn";
private static final String KEY_SUFFIX_SWITCH = "switch";
private static final String KEY_SUFFIX_QPS = "qps";
private static final long DEFAULT_HOST_COUNT = 1;
protected Object generateReturnObject(ApiRateLimiter apiRateLimiter, Class<?> returnType) throws TryAcquireException {
if (apiRateLimiter.returnException()) {
throw new TryAcquireException("Your method triggers current limit");
}
Object codeValue = apiRateLimiter.codeValue();
if(apiRateLimiter.codeItemType().isAssignableFrom(Integer.class)){
codeValue = Integer.parseInt(apiRateLimiter.codeValue());
}
return generateReturnObject(returnType, apiRateLimiter.codeItem(), codeValue, apiRateLimiter.msgItem(), apiRateLimiter.msgValue(), apiRateLimiter.returnJson());
}
protected boolean tryAcquire(ApiRateLimiter apiRateLimiter, Method method, Object[] args) {
if (apiRateLimiter == null) {
return true;
}
if (method == null) {
return true;
}
try {
final String source = getSource(apiRateLimiter, args);
final String propertyName = getPropertyName(apiRateLimiter, source);
if (StringUtils.isEmpty(propertyName)){
return true;
}
final Map<String, String> duccConfigMap = getDuccPropertyByMap(apiRateLimiter.duccResource(), propertyName);
if(duccConfigMap == null){
return true;
}
final String methodName = getMethodName(apiRateLimiter, method);
final long qps = getQps(apiRateLimiter, duccConfigMap, methodName);
final boolean isSwitchOn = getIsSwitchOn(apiRateLimiter, duccConfigMap, methodName);
if (isSwitchOn && qps > 0) {
final String limiterName = propertyName + "." + methodName;
RateLimiter limiter = API_RESOURCE_LIMITER_MAP.get(limiterName);
if (limiter == null) {
limiter = RateLimiter.create(qps);
API_RESOURCE_LIMITER_MAP.put(limiterName, limiter);
}
if (limiter.getRate() != qps) {
limiter.setRate(qps);
}
return limiter.tryAcquire();
}
} catch (Throwable t) {
}
return true;
}
private String getSource(ApiRateLimiter apiRateLimiter, Object[] args) {
if (args == null){
return null;
}
if(args.length < apiRateLimiter.argumentIndex() + 1){
return null;
}
return getObjectItemValue(args[apiRateLimiter.argumentIndex()], apiRateLimiter.sourceItem());
}
private String getPropertyName(ApiRateLimiter apiRateLimiter, String source) {
if (StringUtils.isEmpty(source)){
return apiRateLimiter.duccPropertyPrefix();
}
return apiRateLimiter.duccPropertyPrefix() + "_" + source;
}
private String getMethodName(ApiRateLimiter apiRateLimiter, Method method) {
if (StringUtils.isNotEmpty(apiRateLimiter.methodName())){
return apiRateLimiter.methodName();
}
if(method == null){
return null;
}
return method.getDeclaringClass().getName() + "." + method.getName();
}
private long getHostCount(ApiRateLimiter apiRateLimiter) {
Long hostCount = getDuccPropertyByLong(apiRateLimiter.duccResource(), apiRateLimiter.duccPropertyHostCount());
if(hostCount == null){
return DEFAULT_HOST_COUNT;
}
if(hostCount.longValue() < DEFAULT_HOST_COUNT){
return DEFAULT_HOST_COUNT;
}
return hostCount.longValue();
}
private long getTotalQps(ApiRateLimiter apiRateLimiter, Map<String, String> duccConfigMap, String methodName) {
long qps = apiRateLimiter.defaultQps();
if(duccConfigMap != null){
String qpsKey = methodName + "." + KEY_SUFFIX_QPS;
String qpsStr = duccConfigMap.get(qpsKey);
if(qpsStr == null){
qpsStr = duccConfigMap.get(methodName);
}
if(qpsStr != null){
qps = Long.valueOf(qpsStr.trim());
}
}
return qps;
}
private long getQps(ApiRateLimiter apiRateLimiter, Map<String, String> duccConfigMap, String methodName) {
long qps = getTotalQps(apiRateLimiter, duccConfigMap, methodName);
if(qps <= 0){
return 0;
}
long hostCount = getHostCount(apiRateLimiter);
long oneHostQps = qps/hostCount;
if(oneHostQps <1 ){
oneHostQps = 1;
}
return oneHostQps;
}
private boolean getIsSwitchOn(ApiRateLimiter apiRateLimiter, Map<String, String> duccConfigMap, String methodName) {
if(duccConfigMap == null){
return apiRateLimiter.defaultIsSwitchOn();
}
String isSwitchOnKey = methodName + "." + KEY_SUFFIX_SWITCH;
String isSwitchOnStr = duccConfigMap.get(isSwitchOnKey);
if(isSwitchOnStr == null){
isSwitchOnStr = duccConfigMap.get(GLOBAL_SWITCH);
}
if(isSwitchOnStr == null){
return apiRateLimiter.defaultIsSwitchOn();
}else{
return BooleanUtils.toBoolean(isSwitchOnStr.trim());
}
}
}