常见限流算法
tbghg

参考文章

介绍

限流,也称流量控制。是指系统在面临高并发,或者大流量请求的情况下,限制新的请求对系统的访问,从而保证系统的稳定性

常见限流算法有:

  1. 固定窗口/计数器
  2. 滑动窗口
  3. 限流桶
  4. 令牌桶

常见限流算法

固定窗口/计数器

维护一个计数器,设置一个过期时间,例如:1min访问次数不能包括100次,有用户访问时,访问次数+1,超过阈值时拒绝该请求,每隔1min重制一次计数器。

但当用户第59s发送了100个请求和第61s发送了100个请求,此时因为计数器重制,所以会将这些请求放行

优点:实现简单

可以通过redis实现:设置过期时间,每次访问后值+1,同时判断下访问次数是否超过域值

滑动窗口

image

整个红色的矩形框表示一个时间窗口,举例的话,一分钟作为一个时间窗口,把它分割成6份,每份10s,过10s就把窗口向右移动一格,每个格子都有自己单独的计时器,当滑动窗口的格子划分的越多,那么滑动窗口的滚动就越平滑,限流的统计就会越精确

这样看的话,第一种方案其实是将1min作为一个窗口,即计数器是滑动窗口的一种特殊情况,所以也被称为固定窗口。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
package limiter
// 代码摘自 https://juejin.cn/post/7056068978862456846

import (
"errors"
"sync"
"time"
)

// SlidingWindowLimiter 滑动窗口限流器
type SlidingWindowLimiter struct {
limit int // 窗口请求上限
window int64 // 窗口时间大小
smallWindow int64 // 小窗口时间大小
smallWindows int64 // 小窗口数量
counters map[int64]int // 小窗口计数器
mutex sync.Mutex // 避免并发问题
}

// NewSlidingWindowLimiter 创建滑动窗口限流器
func NewSlidingWindowLimiter(limit int, window, smallWindow time.Duration) (*SlidingWindowLimiter, error) {
// 窗口时间必须能够被小窗口时间整除
if window%smallWindow != 0 {
return nil, errors.New("window cannot be split by integers")
}

return &SlidingWindowLimiter{
limit: limit,
window: int64(window),
smallWindow: int64(smallWindow),
smallWindows: int64(window / smallWindow),
counters: make(map[int64]int),
}, nil
}

func (l *SlidingWindowLimiter) TryAcquire() bool {
l.mutex.Lock()
defer l.mutex.Unlock()

// 获取当前小窗口值
currentSmallWindow := time.Now().UnixNano() / l.smallWindow * l.smallWindow
// 获取起始小窗口值
startSmallWindow := currentSmallWindow - l.smallWindow*(l.smallWindows-1)

// 计算当前窗口的请求总数
var count int
for smallWindow, counter := range l.counters {
if smallWindow < startSmallWindow {
delete(l.counters, smallWindow)
} else {
count += counter
}
}

// 若到达窗口请求上限,请求失败
if count >= l.limit {
return false
}
// 若没到窗口请求上限,当前小窗口计数器+1,请求成功
l.counters[currentSmallWindow]++
return true
}

漏桶

假设我们有一个水桶按固定的速率向下方滴落一滴水,无论有多少请求,请求的速率有多大,都按照固定的速率流出,对应到系统中就是按照固定的速率处理请求。如果桶的容量满了,就达到限流的阀值,就会丢弃水滴(拒绝请求)

关于漏桶的实现,uber团队有一个开源的 https://github.com/uber-go/ratelimit 实现。
使用方法也比较简单,Take() 方法会返回漏桶下一次滴水的时间。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
package main

import (
"fmt"
"time"

"go.uber.org/ratelimit"
)

func main() {
// 限制每秒100次 -> 每隔10ms会放行一个请求
rl := ratelimit.New(100) // per second

prev := time.Now()
for i := 0; i < 10; i++ {
now := rl.Take()
fmt.Println(i, now.Sub(prev))
prev = now
}

// Output:
// 0 0
// 1 10ms
// 2 10ms
// 3 10ms
// 4 10ms
// 5 10ms
// 6 10ms
// 7 10ms
// 8 10ms
// 9 10ms
}

之后写一下这个库的详细介绍,在此不多赘述

令牌桶

令牌桶其实和漏桶的原理类似,令牌桶按固定的速率往桶里放入令牌,并且只要能从桶里取出令牌就能通过,令牌桶支持突发流量的快速处理。

对于从桶里取不到令牌的场景,我们可以选择等待也可以直接拒绝并返回。

实现时,我们不需要真正的每隔一段时间去发放令牌,记录下上次访问时间、上次访问令牌剩余数量,(当前时间-上次访问时间)*令牌生成速度+剩余令牌数得出当前令牌数

可以参考:https://github.com/juju/ratelimit ,部分代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
// 调整令牌数量
func (tb *Bucket) adjustavailableTokens(tick int64) {
lastTick := tb.latestTick
tb.latestTick = tick
if tb.availableTokens >= tb.capacity {
return
}
tb.availableTokens += (tick - lastTick) * tb.quantum
if tb.availableTokens > tb.capacity {
tb.availableTokens = tb.capacity
}
return
}

// 获取令牌,没有的话返回0,不阻塞(Take方法是会阻塞的)
func (tb *Bucket) takeAvailable(now time.Time, count int64) int64 {
if count <= 0 {
return 0
}
tb.adjustavailableTokens(tb.currentTick(now))
if tb.availableTokens <= 0 {
return 0
}
if count > tb.availableTokens {
count = tb.availableTokens
}
tb.availableTokens -= count
return count
}

func (tb *Bucket) currentTick(now time.Time) int64 {
return int64(now.Sub(tb.startTime) / tb.fillInterval)
}

令牌桶算法能满足绝大部分服务器限流的需要, 是被广泛使用的限流算法, 不过其也有一些缺点:

  • 令牌桶是没有优先级的,无法让重要的请求先通过
  • 如果对服务限流进行缩容和扩容,需要人为手动去修改,运维成本比较大
  • 令牌桶只能对局部服务端的限流, 无法掌控全局资源

限流的具体使用

  • 网关限流:可以在nginx处做限流,参考文章 Nginx限流应用 & 漏桶/令牌桶算法原理
  • 服务端限流:以gin框架为例,编写一个限流中间件,加入到需要限流的路由中即可
  • 自适应限流:结合应用的 Load、CPU 使用率、总体平均 RT(请求成功的响应耗时)、入口 QPS 和并发线程数等几个维度的监控指标,通过自适应的流控策略,让系统的入口流量和系统的负载达到一个平衡,让系统尽可能跑在最大吞吐量的同时保证系统整体的稳定性。如Sentinel
 评论
评论插件加载失败
正在加载评论插件