限流是避免系统在过载的情况出现问题。
常见的有:数据的数据库连接池,dubbo的线程池,nginx的并发连接数配置等等
限流的策略:
限流的目的是对并发访问的速度控制,一旦到达限定的阈值,就会触发限流行为,常见的限流行为有:
拒绝服务:流量突增,拒绝掉流量最大的来源,eg根据ip,访问量统计,直接拒绝掉请求
服务降级:关闭或者后端服务降级处理,关闭不重要的服务。不返回全量数据,只返回部分数据。
特权请求:资源不够时,分配给重要的用户,eg:服务vip用户的权利更大些,尽量优先处理。非vip用户优先限流。
延时处理:使用队列减少峰值压力,缓存大量的请求。如果队列任务超时,也要返回错误。应对短时的峰刺请求。达到削峰填谷效果。
弹性伸缩:使用自动化运维,对服务自动化伸缩,需要对应用的监控系统。eg:k8s中自动扩容,缩容服务器的部署数量。
如果是数据库的压力过大,弹性伸缩应用作用不大,以限流为主。
限流的实现方式:
- 令牌桶:Token Bucket
- 漏桶算法:Leaky Bucket
- 固定窗口计数器:Fixed Window Counter
- 滑动日志:Sliding Logs
- 滑动窗口计数器:Sliding Window Counter
固定窗口计数器:Fixed Window Counter
使用N秒的时间窗口,(eg:60秒)如果计数器超过阈值则丢弃盖青青。
主要的问题是临界问题:在窗口边界附近发生的突发流量可能会请求处理量翻倍,因为该算法,允许短时间内当前窗口和下一个窗口进行请求。
eg:上图上一个时间窗口末尾算瞬间请求100个,下一个时间窗口请求100个,2秒内请求数量和为200个。
滑动日志:Sliding Logs
滑动日志算法,跟踪每个请求,并记录时间戳日志,这些日志通常存储在按时间排序的hashset或表中,时间戳超规格阈值的日志将被丢弃。新的请求进来是,计数日志的总和确认请求量,是否超规定阈值。
优点:
不受固定窗口边界条件影响。速率限制将被精确执行,每个客户的滑动日志都被跟踪,所以不会需要固定窗口的问题。
缺点:每个请求都存储的日志和计算可能会消耗大量资源,计算还可能压垮服务器。不能很好的扩展处理大量突发流量或dDos拒绝服务攻击。
滑动窗口计数器:Sliding Window Counter
滑动窗口算法结合了固定窗口和滑动日志算法。
窗口时间被分解为更小的桶——每个桶的大小取决于速率限制阈值。每个桶存储与桶范围相对应的请求计数, 它不断地随时间移动,平滑突发流量。
eg:“ 5 个 请求/分钟”速率限制器的滑动窗口计数器,使用秒作为窗口。
如上图:
1.user_1发出了一个新请求,请求时间为9:01:01
,作为其哈希键,并将计数器设置为1
2.用户没有进行任何其他的请求,直到_:_:02
-第二窗口,里,请求了5次
后续没有请求,直到9:03:01 之前的超过1分钟,之前的被丢弃
令牌桶:Token Bucket
漏桶算法:Leaky Bucket
队列算法:FIFO,优先级队列
请求速度波动的,处理速度均速。像 FIFO,先高优先级,再处理低优先级
低队列被饿死,有带权重的队列。下图:三个队列的权重分布是 3:2:1,权重 3 的这个队列上处理 3 个请求后,再去权重 2 的队列上处理 2 个请求,最后去 1 的队列上处理 1 个请求,反复。
队列满,触发限流:用队列长度来控制流量,配置上难操作。过长导致没满就挂掉了。不能做 push ,pull 方式会好一些
常见算法代码:
固定窗口计数器:Fixed Window Counter
/**
* 固定窗口时间算法
* @return
*/
boolean fixedWindowsTryAcquire() {
long currentTime = System.currentTimeMillis(); //获取系统当前时间
if (currentTime - lastRequestTime > windowUnit) { //检查是否在时间窗口内
counter = 0; // 计数器清0
lastRequestTime = currentTime; //开启新的时间窗口
}
if (counter < threshold) { // 小于阀值
counter++; //计数器加1
return true;
}
return false;
}
滑动窗口计数器:Sliding Window Counter
/**
* 单位时间划分的小周期(单位时间是1分钟,10s一个小格子窗口,一共6个格子)
*/
private int SUB_CYCLE = 10;
/**
* 每分钟限流请求数
*/
private int thresholdPerMin = 100;
/**
* 计数器, k-为当前窗口的开始时间值秒,value为当前窗口的计数
*/
private final TreeMap<Long, Integer> counters = new TreeMap<>();
/**
* 滑动窗口时间算法实现
*/
boolean slidingWindowsTryAcquire() {
long currentWindowTime = LocalDateTime.now().toEpochSecond(ZoneOffset.UTC) / SUB_CYCLE * SUB_CYCLE; //获取当前时间在哪个小周期窗口
int currentWindowNum = countCurrentWindow(currentWindowTime); //当前窗口总请求数
//超过阀值限流
if (currentWindowNum >= thresholdPerMin) {
return false;
}
//计数器+1
counters.get(currentWindowTime)++;
return true;
}
/**
* 统计当前窗口的请求数
*/
private int countCurrentWindow(long currentWindowTime) {
//计算窗口开始位置
long startTime = currentWindowTime - SUB_CYCLE* (60s/SUB_CYCLE-1);
int count = 0;
//遍历存储的计数器
Iterator<Map.Entry<Long, Integer>> iterator = counters.entrySet().iterator();
while (iterator.hasNext()) {
Map.Entry<Long, Integer> entry = iterator.next();
// 删除无效过期的子窗口计数器
if (entry.getKey() < startTime) {
iterator.remove();
} else {
//累加当前窗口的所有计数器之和
count =count + entry.getValue();
}
}
return count;
}
漏桶算法:Leaky Bucket
/**
* 每秒处理数(出水率)
*/
private long rate;
/**
* 当前剩余水量
*/
private long currentWater;
/**
* 最后刷新时间
*/
private long refreshTime;
/**
* 桶容量
*/
private long capacity;
/**
* 漏桶算法
* @return
*/
boolean leakybucketLimitTryAcquire() {
long currentTime = System.currentTimeMillis(); //获取系统当前时间
long outWater = (currentTime - refreshTime) / 1000 * rate; //流出的水量 =(当前时间-上次刷新时间)* 出水率
long currentWater = Math.max(0, currentWater - outWater); // 当前水量 = 之前的桶内水量-流出的水量
refreshTime = currentTime; // 刷新时间
// 当前剩余水量还是小于桶的容量,则请求放行
if (currentWater < capacity) {
currentWater++;
return true;
}
// 当前剩余水量大于等于桶的容量,限流
return false;
}
令牌桶:Token Bucket
/**
* 每秒处理数(放入令牌数量)
*/
private long putTokenRate;
/**
* 最后刷新时间
*/
private long refreshTime;
/**
* 令牌桶容量
*/
private long capacity;
/**
* 当前桶内令牌数
*/
private long currentToken = 0L;
/**
* 漏桶算法
* @return
*/
boolean tokenBucketTryAcquire() {
long currentTime = System.currentTimeMillis(); //获取系统当前时间
long generateToken = (currentTime - refreshTime) / 1000 * putTokenRate; //生成的令牌 =(当前时间-上次刷新时间)* 放入令牌的速率
currentToken = Math.min(capacity, generateToken + currentToken); // 当前令牌数量 = 之前的桶内令牌数量+放入的令牌数量
refreshTime = currentTime; // 刷新时间
//桶里面还有令牌,请求正常处理
if (currentToken > 0) {
currentToken--; //令牌数量-1
return true;
}
return false;
}