一、限流的基本原理
它的目的是确保系统高效、稳定地运行,确保请求能够快速处理的同时,保障系统不被流量压垮。
限流通常是利用某种算法实现限流器,来达到限制流量的目的。通常,限流器中会有一个定时器,它主要用来定时更新与条件有关的资源。还有,每次请求也需要更新该资源。如果抽象成 Go 中的 interface 话,示例代码如下:
type Limiter interface{
Take() bool
Start()
}
其中 Take 方法用于每次请求时调用,判断该请求是否可以继续进行而不触发限流。Start 方法主要用于启动定时器,通常是在工厂方法中创建限流器时调用。示例代码如下:
func NewLimiter(name string, params Params) Limiter {
var l Limiter
if name == "counterLimiter" {
// 创建计数器限流器
l = newCounterLimiter(params)
} else if name == "bucketLimiter" {
// 创建漏桶限流器
l = newBucketLimiter(params)
}
if l != nil {
l.Start()
}
return l
}
限流算法有多种,这两个方法按照不同算法有不同的具体实现。
二、限流算法主要有:计数器限流、滑动窗口限流、令牌桶限流、漏桶限流。
1、计数器限流算法
计数器限流算法也叫固定窗口限流算法。
首先,选定一个时间窗口作为一个周期,假设为 5 秒;
第二步,设定 5 秒内允许通过的流量,如 1000 个请求;
第三步,每次请求,计数器都加 1;
第四步,判断计数器数值是否超过 1000 ,超过了就触发限流策略,如:拒绝或者延迟处理请求等;
最后,如果时间过了 5 秒,则重置计数为 0,开始一个新的周期。
该限流算法的优点是实现简单,缺点是面对突发流量时不够精确。面对瞬时流量时,会存在资源利用率的剧烈抖动。
2、滑动窗口限流算法
滑动窗口限流算法是对计数器限流算法的优化。它的主要原理是将计数器限流算法中的一个周期拆分成很多等分,比如将 5 秒的周期拆成 5 个 1 秒,每次统计从当前时间开始过去 5 秒内的流量,每隔 1 秒往后滑动 1 秒。
由于将周期拆分成多个小的单位,相比计数器限流算法,滑动窗口限流算法对流量的统计和控制要更精确,资源利用率抖动更小。但它还是没有彻底解决因瞬时流量导致资源使用率抖动的问题。
那么,有没有办法解决这个问题呢?有,它就是我接下来要介绍的令牌桶限流算法和漏桶限流算法。
3、令牌桶限流算法
令牌桶算法的基本原理是,使用一个定时器以恒定速度往桶里颁发令牌,桶满了则丢弃多余令牌。请看示意图:
在令牌桶算法中,一般只有拿到令牌的请求才会被处理,没拿到的将会被拒绝。这个过程就像景区的人工售票窗口售票,只有买到票了才能检票进入景区。这其中,令牌就是门票,令牌桶就是售票窗口,负责发令牌的线程就类似于售票员,处理请求的线程就是检票员。
4、漏桶限流算法
漏桶算法的原理跟令牌桶有点相似,只不过漏桶算法采用“生产者-消费者”模型。在“生产者”一端,所有请求进队列,队列满了则丢弃请求。在“消费者”一端,以恒定速度消费队列并处理请求。
举个例子:乒乓球教练将乒乓球放入到发球机中,乒乓球发球机能以固定速度发出乒乓球,球员可以以固定速度击打乒乓球。例子中的乒乓球相当于软件系统中的请求,教练相当于“生产者”,发球机相当于漏桶,而球员相当于“消费者”。
以上这几种限流算法中,流量控制效果从好到差依次是:漏桶限流 > 令牌桶限流 > 滑动窗口限流 > 计数器限流。
其中,只有漏桶算法真正实现了恒定速度处理请求,能够绝对防止突发流量超过下游系统承载能力。不过,漏桶限流也有个不足,就是需要分配内存资源缓存请求,这会增加内存的使用率。而令牌桶限流算法中的“桶”可以用一个整数表示,资源占用相对较小,这也让它成为最常用的限流算法。
正是因为这些特点,漏桶限流和令牌桶限流经常在一些大流量系统中结合使用。比如秒杀系统中就同时使用了这两种限流算法。