对外提供的api接口都要考虑限流,否则会出现因流量暴增而导致的系统运行缓慢或宕机的问题。
有了负载均衡算法来平衡各个服务器之间的请求数量,但即使是这样,我们的服务资源肯定也是有上限的。比如你开发了一个类似微博或者论坛类的网站,如果有人写了一个脚本,比如一分钟发送10万条微博,那么该系统就会崩溃。那么我们的系统就需要对每一个用户在特定的一个时间段内能够发出请求的数量做出一定的限制来防止这些破坏。 那么问题来了,如果使用多个用户执行脚本,就会破坏了我们所限制的每一个用户请求数的限制,因此我们的系统就要对用户ip也要进行一定的防御,就是说在特定的一个ip当中所能发的请求数量也是在特定时间内有一个限制。 那么问题又来了在全球范围内的多个ip地址对服务进行一个攻击。这样对ip层的这种限制也就失效了【DDOS】
可以在客户端、LB中、服务器端做限流操作。其中全局性的限流相对较为复杂。而在server端的限流会相对简单一些。
限流是指对系统的请求流量进行限制,以便于保障系统的可靠性和稳定性,防止系统被过度调用导致崩溃或性能下降。常见的限流方法主要有以下几种:
固定窗口计数器算法
固定窗口计数器算法(Fixed Window Counting)是一种简单的限流算法,它将请求拦截在一定的时间窗口中。在一定时间内,只允许请求一定数量的次数,超过临界值就拒绝请求。这种算法简单易懂,但是其缺点是:如果窗口长度过短,则会导致瞬间流量过大;如果窗口长度过长,则无法应对短时间内大量请求的情况。
伪码演示,只供演示
private long timeStamp = System.currentTimeMillis();
private int reqCount; // 请求次数
private int limitNum = 100; // 每秒限流的最大请求数
private long interval = 1000L; // 时间窗口时长,单位ms
/**
返回true表示限流,false代表通过
*/
public synchronized Boolean limit() {
long now = System.currentTimeMillis();
if(now < timeStamp + interval) { // 在当前时间窗口内
// 判断当前时间窗口请求数加1是否超过每秒限流的最大请求数。
if(reqCount + 1 > limitNum) {
return true;
}
reqCount++;
return false;
} else { // 开启新的时间窗口
timeStamp = now;
// 重置计数器
reqCount = 1;
return false;
}
}
缺点
- 精度问题:比如我们限流100次,在前1s的后半段接收了60个请求,后1s的前半段接收了60个请求,整体就是120个请求。同样是不符合要求的。
滑动窗口计数器算法
滑动窗口计数器算法(Sliding Window Counting)是一种改进版的固定窗口计数器算法。它引入 “滑动窗口”的概念,即将整个时间段平均分成若干个时间段,以每个时间段的请求频率作为限制条件。这样,就可以平滑处理请求,有效地应对瞬间流量过大的情况。滑动窗口计数器算法应用比较广泛,例如在nginx中的limit req模块,就是使用滑动窗口算法进行限流的。
- 解决了计数器精度太低的问题【总的来说,就是把规定时间划分成更小的格子,即精度更高一些】
在上图中,整个红色的矩形框表示一个时间窗口,在我们的例子中,一个时间窗口就是一秒钟。然后我们将时间窗口进行划分,比如图中,我们就将滑动窗口划分成了10格,所以每格代表的是100毫秒。每过100毫秒,我们的时间窗口就会往右滑动一格。我们会根据请求发生的时间找到对应的事件窗格,所以在窗格里维护一个计数器counter,并让其加1。
伪码演示,存在并发问题,只供理解思想
// 服务在最近1秒内的访问次数,可以放在redis中,实现分布式系统的访问次数。
private int reqCount;
// 使用LinkedList来记录滑动窗口的10个格子
private LinkedList<Integer> slots = new LinkedList<>();
// 每秒限流的最大请求数
private int limitNum = 100;
// 滑动时间窗口里的每个格子的时间长度,单位ms
private long windowLength = 100L;
// 滑动事件窗口里的格子数量
private int windowNum = 10;
/**
返回true代表限流,false代表通过
*/
public synchronized Boolean limit() {
if(((reqCount + 1)) > limitNum) {
return true;
}
// 将最后一个窗口中记录的请求次数+1
slots.set(slots.size() - 1, slots.peekLast() + 1);
// 总的请求次数+1
reqCount++;
return false;
}
// 构造方法完成初始化
public SlidingTimeWindowLimiter() {
// 先往格子中放一个0
slots.addLast(0);
new Thread(() -> {
while(true) {
// 睡眠100ms之后再添加一个格子
try {
Thread.sleep(windowLength);
} catch(InterruptedException e) {
e.printStackTrace();
}
slots.addLast(0);
// 窗格的数量大于了执行的数量10
if(slots.size() > windowNum) {
// 更新总的请求次数
reqCount = reqCount - slots.peekFirst();
slots.removeFirst();
System.out.println("滑动格子:" + reqCount);
}
}
}).start();
}
缺点
-
精度问题:滑动窗口限流算法的精度取决于时间窗口的大小和滑动步长。如果时间窗口太小,就会导致限流不够严格,影响系统的吞吐量;如果时间窗口太大,就会导致限流不够严格,无法有效地保护系统。同样,如果滑动补偿太小,会导致统计数据冗余,造成计算资源的浪费;如果滑动步长太大,会导致统计数据不准确,无法及时发现请求的异常情况。
精度应该结合业务场景做具体的设置。
漏桶算法
漏桶算法(Leaky Bucket)是一种常用的限流算法。它的实现思想是:将请求看作水流,将请求的处理看作水流通过漏桶的处理,如果水流过快,则会漏掉一部分。漏桶算法可以应对恒定的请求速率,但其缺点是无法应对突发性持续的大流量需求。
伪码演示,只供理解思想
private long timeStamp = System.currentTimeMillis();
// 桶的容量
private long capacity = 100;
// 水漏出的速度(每秒系统能处理的请求数)
private long rate = 10;
// 当前水量(当前累计请求数)
private long water = 20;
/**
返回true代表限流,false代表通过
*/
public synchronized Boolean limit() {
long now - System.currentTimeMillis();
// 先执行漏水,计算剩余水量(计算剩余请求数量),不能为0
water = Math.max(0, water - ((now - timeStamp) / 1000) * rate);
timeStamp = now;
if((water + 1) <= capacity) {
// 水还未满,加水
water++;
System.out.println("加水" + water);
return false;
} else {
// 水满,拒绝加水
return true;
}
}
缺点
- 突发流量问题:漏桶限流算法无法有效地控制突发流量。当系统遭受到突发流量时,漏桶会瞬间被填满,从而拒绝大量的请求。这会对系统的性能造成影响,特别是在高并发场景下。
- 精度问题:漏桶限流算法的精度取决于漏桶的容量和流出速率。如果漏桶容量太小或流出速率太慢,就会导致限流过于严格,影响系统的吞吐量;如果漏桶容量太大或流出速率太快,就会导致限流不够严格,无法有效地保护系统。这也会给系统带来很大的负担,特别是在请求量较大的情况下。
令牌桶算法
令牌桶算法(Token Bucket):它模拟了一个桶,每秒钟往桶里面放入一定数量的令牌。一个请求只要获取到一个令牌,就可以执行请求。当桶满的时候,多余的令牌会被丢弃。这种算法针对请求去拿令牌是没有速度限制的,因此,可以应对瞬时流量的过载情况。
伪码演示,只供理解思想
private long timeStamp = System.currentTimeMillis();
// 桶的容量
private long capacity = 100;
// 令牌放入速度
private long rate = 10;
// 当前令牌数量
private long tokens = 20;
/**
返回true代表限流,false代表通过
*/
public synchronized Boolean limit() {
long now = System.currentTimeMillis();
// 先放下令牌,因为请求过来存在时间差
tokens = Math.min(capacity, tokens + (now - timeStamp) * rate);
timeStamp = now;
if(tokens < 1) {
// 若不到1个令牌,则拒绝
return true;
} else {
// 还有令牌,领取令牌
tokens--;
return false;
}
}
本文讲的单机的限流,是JVM级别的的限流,所有的令牌生成都是在内存中,在分布式环境下不能直接这么用,可用使redis限流。
- 本文只供个人学习,无其它用途。
- 参考了一些公开视频和博客。