高并发系统--限流算法
在开发高并发系统时,有三把利器用来保护系统:缓存、降级和限流。通过限流,我们可以很好地控制系统的qps,从而达到保护系统的目的。主要算法有:计数器算法,滑动窗口算法,漏桶算法,令牌桶算法
1.计数器算法
规定接口的最大频率,例如一分钟最多100次接口调用,利用一个计数器来不断计数,统计一分钟内调用次数是否超过100,如果没有超过则本次请求可以响应,如果超过则阻塞丢弃本次请求,计数器清零,重新开始计数。
public class Counter {
public long timeStamp = System.currentTimeMillis();
public int reqCount = 0;
public final int limit = 100;
public final long interval = 1000;
public boolean grant(){
long now = System.currentTimeMillis();
if(now < timeStamp+interval){
reqCount++;
return reqCount<=limit;
}else{
timeStamp = now;
reqCount = 1;
return false;
}
}
}
问题:如果在判断时间间隔前后进行大量请求,则会突破限流频率给系统造成损害,假设有一个恶意用户,他在0:59时,瞬间发送了100个请求,并且1:00又瞬间发送了100个请求,那么其实这个用户在 1秒里面,瞬间发送了200个请求。我们刚才规定的是1分钟最多100个请求,也就是每秒钟最多1.7个请求,用户通过在时间窗口的重置节点处突发请求, 可以瞬间超过我们的速率限制。用户有可能通过算法的这个漏洞,瞬间压垮我们的应用。
2.滑动窗口算法
设置一个1秒钟的滑动窗口,窗口中有10个格子,每个格子100毫秒,每100毫秒移动一次,每次移动都需要记录当前服务请求的次数。内存中需要保存10次的次数。可以用数据结构LinkedList来实现。格子每次移动的时候判断一次,当前访问次数和LinkedList中最后一个相差是否超过100,如果超过就需要限流。
public class RollingWindow {
Long counter = 0L;
LinkedList<Long> ll = new LinkedList<Long>();
public void doCheck() throws Exception{
while(true){
ll.addLast(counter);
if(ll.size()>10){
ll.removeFirst();
}
if(ll.peekLast()-ll.peekFirst()>100){
// to do limit
}
Thread.sleep(100);
}
}
}
计数器算法其实就是滑动窗口算法。它没有对时间窗口做进一步地划分,只有1格。当滑动窗口的格子划分的越多,那么滑动窗口的滚动就越平滑,限流的统计就会越精确。
3.漏桶算法
漏桶算法,又称leaky bucket。首先,我们有一个固定容量的桶,有水流进来,也有水流出去。对于流进来的水来说,我们无法预计一共有多 少水会流进来,也无法预计水流的速度。但是对于流出去的水来说,这个桶可以固定水流出的速率。而且,当桶满了之后,多余的水将会溢出。
public class LeakyBucket {
public long timeStamp = System.currentTimeMillis();
public int capacity;//桶大小
public int rate;//漏水速率
public int water;//水量
public boolean grant(){
long now = System.currentTimeMillis();
water = (int)Math.max(0, water-(now-timeStamp)*rate);
timeStamp = now;
if(water+1<capacity){
water += 1;
return true;
}else{
return false;
}
}
}
因为漏桶的漏出速率是固定的参数,所以,即使网络中不存在资源冲突(没有发生拥塞),漏桶算法也不能使流突发(burst)到端口速率.因此,漏桶算法对于存在突发特性的流量来说缺乏效率.
4.令牌桶算法
令牌桶算法,又称token bucket。首先,我们有一个固定容量的桶,桶里存放着令牌(token)。桶一开始是空的,token以 一个固定的速率r往桶里填充,直到达到桶的容量,多余的令牌将会被丢弃。每当一个请求过来时,就会尝试从桶里移除一个令牌,如果没有令牌的话,请求无法通过。
public class TokenBucket {
public long timeStamp = System.currentTimeMillis();
public int capacity;
public int rate;
public int tokens;
public boolean grant(){
long now = System.currentTimeMillis();
tokens = (int)Math.min(capacity, tokens+(now-timeStamp)*rate);
timeStamp = now;
if(tokens<1){
return false;
}else{
tokens -= 1;
return true;
}
}
}
令牌桶的一个好处是可以方便的改变速度. 一旦需要提高速率,则按需提高放入桶中的令牌的速率. 一般会定时(比如100毫秒)往桶中增加一定数量的令牌, 有些变种算法则实时的计算应该增加的令牌的数量。漏桶算法和令牌桶算法最明显的区别是令牌桶算法允许流量一定程度的突发。因为默认的令牌桶算法,取走token是不需要耗费时间的,也就是说,假设桶内有100个token时,那么可以瞬间允许100个请求通过。令牌桶算法由于实现简单,且允许某些流量的突发,对用户友好,所以被业界采用地较多。当然我们需要具体情况具体分析,只有最合适的算法,没有最优的算法。
Google开源工具包Guava提供了限流工具类RateLimiter,该类基于令牌桶算法(Token Bucket)来完成限流,非常易于使用.RateLimiter经常用于限制对一些物理资源或者逻辑资源的访问速率.它支持两种获取permits接口,一种是如果拿不到立刻返回false,一种会阻塞等待一段时间看能不能拿到。
对于Nginx接入层限流可以使用Nginx自带了两个模块:连接数限流模块ngx_http_limit_conn_module和漏桶算法实现的请求限流模块ngx_http_limit_req_module。
1. ngx_http_limit_conn_module
服务器流量异常,负载过大等等。对于大流量恶意的攻击访问,会带来带宽的浪费,服务器压力,影响业务,往往考虑对同一个ip的连接数,并发数进行限制。ngx_http_limit_conn_module 模块来实现该需求。该模块可以根据定义的键来限制每个键值的连接数,如同一个IP来源的连接数。并不是所有的连接都会被该模块计数,只有那些正在被处理的请求(这些请求的头信息已被完全读入)所在的连接才会被计数。
2. ngx_http_limit_req_module
该模块可以通过定义的键值来限制请求处理的频率。特别的,可以限制来自单个IP地址的请求处理频率。 限制的方法是使用了漏斗算法,每秒固定处理请求数,推迟过多请求。如果请求的频率超过了限制域配置的值,请求处理会被延迟或被丢弃,所以所有的请求都是以定义的频率被处理的。
参考链接:
https://www.cnblogs.com/haoxinyue/p/6792309.html