常用限流策略——漏桶与令牌桶介绍
限流:限制到达系统的并发请求数。会影响部分用户的体验,但在一定程度上保障系统的稳定性
两者区别
漏桶算法思路很简单,请求先进入到漏桶里,漏桶以固定的速度出水,也就是处理请求,当水加的过快,则会直接溢出,也就是拒绝请求,可以看出漏桶算法能强行限制数据的传输速率。不能应对大量的突发请求。
令牌桶算法的原理是系统会以一个恒定的速度往桶里放入令牌,而如果请求需要被处理,则需要先从桶里获取一个令牌,当桶里没有令牌可取时,则拒绝服务。
15.1 漏桶源码
func rateLimit1() func(ctx *gin.Context) {
//创建一个限流器
//ratelimit1.New(rate int, opts ...Option) rate 每秒能够通过的请求数
rl := ratelimit1.New(100)
return func(ctx *gin.Context) {
//桶中有水滴时,取走;桶中没有时,返回需要等待的时间
//rl.Take() 返回一个时间 表示该请求获取到令牌的时间
/**
rl.Take().Sub(time.Now()) > 0 说明 需要等待一段时间才能获取到令牌
*/
if rl.Take().Sub(time.Now()) > 0 {
time.Sleep(rl.Take().Sub(time.Now()))
ctx.String(http.StatusOK, "rate limit...")
ctx.Abort()
return
}
//rl.Take().Sub(time.Now()) <= 0 当前时间可以获取到令牌 放行
ctx.Next()
}
}
"go.uber.org/ratelimit" 源码
New方法里面调用了newAtomicBased方法
func newAtomicBased(rate int, opts ...Option) *atomicLimiter {
config := buildConfig(opts)
//生成令牌的时间间隔
perRequest := config.per / time.Duration(rate)
l := &atomicLimiter{
perRequest: perRequest,
maxSlack: -1 * time.Duration(config.slack) * perRequest,
clock: config.clock,
}
//初始化state 里面有上次请求的时间 和 请求需要等待的时间
initialState := state{
last: time.Time{},
sleepFor: 0,
}
atomic.StorePointer(&l.state, unsafe.Pointer(&initialState))
return l
}
//buildConfig
//初始化config结构体 再根据选项 给结构体中的字段赋值
func buildConfig(opts []Option) config {
c := config{
clock: clock.New(),
slack: 10,
per: time.Second,
}
for _, opt := range opts {
opt.apply(&c)
}
return c
}
Take方法
func (t *atomicLimiter) Take() time.Time {
var (
newState state
taken bool
interval time.Duration
)
//taken 默认为false
for !taken {
now := t.clock.Now()
previousStatePointer := atomic.LoadPointer(&t.state)
oldState := (*state)(previousStatePointer)
newState = state{
last: now,
sleepFor: oldState.sleepFor,
}
//如果last == 0 表示这是第一个请求 直接执行 然后返回
if oldState.last.IsZero() {
taken = atomic.CompareAndSwapPointer(&t.state, previousStatePointer, unsafe.Pointer(&newState))
continue
}
newState.sleepFor += t.perRequest - now.Sub(oldState.last)
if newState.sleepFor < t.maxSlack {
newState.sleepFor = t.maxSlack
}
if newState.sleepFor > 0 {
newState.last = newState.last.Add(newState.sleepFor)
interval, newState.sleepFor = newState.sleepFor, 0
}
//用newState 替换t.state
taken = atomic.CompareAndSwapPointer(&t.state, previousStatePointer, unsafe.Pointer(&newState))
}
t.clock.Sleep(interval)
return newState.last
}
func (t *atomicLimiter) Take() time.Time {
var (
newState state
taken bool
interval time.Duration
)
for !taken {
//获取当前时间
now := t.clock.Now()
//获取*atomicLimiter.state
previousStatePointer := atomic.LoadPointer(&t.state)
oldState := (*state)(previousStatePointer)
//
newState = state{
last: now,
sleepFor: oldState.sleepFor,
}
//如果last == 0 表示为初始化状态 上一次访问时间为0
//设置 上一次访问时间 为 当前时间 直接返回
if oldState.last.IsZero() {
taken = atomic.CompareAndSwapPointer(&t.state, previousStatePointer, unsafe.Pointer(&newState))
continue
}
//计算 需要等待的时间 (不发请求 水就不会从桶里落下 水按照指定速率加入桶中 多长时间之后 下一滴水 才能落下来)
//例子 10s + 100s + (2022年8月29日00:00:00 - 2021年8月29日00:00:00)
//10s 上一次 发出请求后10s 等到了令牌(初始化时 需要等待的时间为0)
//100s 生成令牌的时间间隔
//两时间相减 当前时间 - 上一次访问该请求的时间
//上文具体例子 得到的值 可能为一个负值
//需要等待的时间 = 上一次记录的需要等待的时间 + 生成令牌的时间间隔 - (当前时间 - 上一次访问时间)
newState.sleepFor += t.perRequest - now.Sub(oldState.last)
//如果 需要等待的时间 < 最大富余量(默认配置时为负值 十个请求间隔的大小 ratelimit包 默认允许最大瞬时请求为10) 说明请求量很小 距离上一次访问时间已经很久了
/**
当需要等待的时间为负值时 表示请求到来时,不用等待,就能取到令牌 当大量请求出现时 由于无需等待 服务器会顶不住
所以 这种情况下 将需要等待的时间 修改成 最大富余量 增大需要等待的时间 以应对大量突发请求
*/
if newState.sleepFor < t.maxSlack {
newState.sleepFor = t.maxSlack
}
//如果 需要等待的时间 > 0 说明请求量很大 需要等待一段时间才能返回
if newState.sleepFor > 0 {
//修改last 加上需要等待的时间
newState.last = newState.last.Add(newState.sleepFor)
//给interval赋sleepFor的值 将sleepFor置为0
interval, newState.sleepFor = newState.sleepFor, 0
}
taken = atomic.CompareAndSwapPointer(&t.state, previousStatePointer, unsafe.Pointer(&newState))
}
//时钟休眠 指定的时间 达到等待xx时间 获取到令牌的效果
t.clock.Sleep(interval)
return newState.last
}
举例说明 为什么要使用最大松弛量
具体例子:
有三个请求re1,re2,re3 令牌生成的时间间隔为10ms
re1先到
re1完成15ms后 re2到
res完成5ms后 re3到
计算所有请求消耗的总时间
漏桶算法的限速逻辑一:
sleepFor = t.perRequest - now.Sub(t.last)
if sleepFor > 0 {
t.clock.Sleep(sleepFor)
t.last = now.Add(sleepFor)
} else {
t.last = now
}
计算:
re1先到 last == 0 无需等待
re2 15ms到 sleepFor = 10 - 15 = -5 < 0 直接执行
re3 5ms后 sleepFor = 10 - 5 = 5 > 0 令牌还要5ms才能生成 需要再等待5ms后再执行
所有请求消耗的总时间 = 15ms + 5+5 = 25ms
分析
根据漏桶算法,请求之间应间隔10ms 上文中三个请求预期消耗20ms(两次生成令牌的间隔时间) 但实际上消耗25ms
没有实现漏桶算法的目标
漏桶算法的限速逻辑二:
t.sleepFor += t.perRequest - now.Sub(t.last)
//此前请求多余出来的时间 无法完全抵消此次需要等待的时间 所以需要sleep相应的时间 并将t.sleepFor置为0
if t.sleepFor > 0 {
t.clock.Sleep(t.sleepFor)
t.last = now.Add(t.sleepFor)
t.sleepFor = 0
} else {
t.last = now
}
t.sleepFor += t.perRequest - now.Sub(t.last)
//啥意思
/**
两请求之间 间隔很长时间 以至于超过perRequest的时间 将差值 匀给后面的请求 判断限流时使用
例如re2在re1后15ms到达,相当于 多等了5ms 将这5ms 匀给re3使用
*/
计算
re1先到 last == 0 无需等待
re2 15ms到 sleepFor += 10 - 15 = 0 -5 = -5 < 0 直接执行 此时t.sleepFor = -5
re3 5ms后 sleepFor += 10 - 5 = -5 + 5 = 0 直接执行 此时t.sleepFor = 0
所有请求消耗的总时间 = 15ms + 5 = 20ms
分析
这其中存在着问题
当两次请求的时间间隔 很长 以至于
t.sleepFor += t.perRequest - now.Sub(t.last) 的结果为很大负数时
即使后面大量的请求瞬时到达,也无法抵消完这些时间 这些请求都直接执行 这样就失去了限流的意义
为了防止这种情况,reatelimit引入了maxSlack(最大松弛量) 默认情况为 十个请求的间隔大小(slack默认为10) 可以理解为ratelimit包设计的 默认允许的最大瞬时请求为10
maxSlack: -1 * time.Duration(config.slack) * perRequest
//当sleepFor < maxSlack时 sleepFor = maxSlack
if newState.sleepFor < t.maxSlack {
newState.sleepFor = t.maxSlack
}
15.2 令牌桶源码
func rateLimit2() func(ctx *gin.Context) {
tokenBucket := ratelimit2.NewBucket(time.Second, 20)
return func(ctx *gin.Context) {
available := tokenBucket.TakeAvailable(1)
if available <= 0 {
ctx.String(http.StatusOK, "rate limit...")
ctx.Abort()
return
}
ctx.Next()
}
}
NewBucket(fillInterval time.Duration, capacity int64)
->
NewBucketWithClock(fillInterval, capacity, nil)
->
NewBucketWithQuantumAndClock(fillInterval, capacity, 1, clock)
初始化令牌桶
func NewBucketWithQuantumAndClock(fillInterval time.Duration, capacity, quantum int64, clock Clock) *Bucket {
if clock == nil {
clock = realClock{}
}
if fillInterval <= 0 {
panic("token bucket fill interval is not > 0")
}
if capacity <= 0 {
panic("token bucket capacity is not > 0")
}
if quantum <= 0 {
panic("token bucket quantum is not > 0")
}
//结构体初始化
return &Bucket{
clock: clock,
startTime: clock.Now(),
latestTick: 0, //从程序运行到上一次访问的时候,一共产生了多少次计数(如果quantum等于1的话 ,就是一共产生的令牌数量)
fillInterval: fillInterval,//产生令牌的时间间隔
capacity: capacity, //令牌桶容量
quantum: quantum, //每次时间间隔 产生令牌的个数
availableTokens: capacity, //可用令牌数量
}
}
取令牌
func (tb *Bucket) takeAvailable(now time.Time, count int64) int64 {
if count <= 0 {
return 0
}
//计算修改可用token数量
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 {
//计算 从开始运行到 当前时间 一共跳变了多少次 次数*quantum = 令牌数
return int64(now.Sub(tb.startTime) / tb.fillInterval)
}
func (tb *Bucket) adjustavailableTokens(tick int64) {
//lastTick 截止上次请求 产生的跳变次数
lastTick := tb.latestTick
//截止本次请求 产生的跳变次数
tb.latestTick = tick
//可用令牌数 >= 桶的容量 返回
if tb.availableTokens >= tb.capacity {
return
}
//可用令牌数量 += (本次请求计算的跳变次数 - 上次请求计算的跳变次数)*quantum
tb.availableTokens += (tick - lastTick) * tb.quantum
//可用令牌数 >= 桶的容量 则 可用令牌数 = 桶的容量
if tb.availableTokens > tb.capacity {
tb.availableTokens = tb.capacity
}
return
}