常用限流策略——漏桶与令牌桶介绍

常用限流策略——漏桶与令牌桶介绍

限流:限制到达系统的并发请求数。会影响部分用户的体验,但在一定程度上保障系统的稳定性

两者区别

漏桶算法思路很简单,请求先进入到漏桶里,漏桶以固定的速度出水,也就是处理请求,当水加的过快,则会直接溢出,也就是拒绝请求,可以看出漏桶算法能强行限制数据的传输速率。不能应对大量的突发请求。
令牌桶算法的原理是系统会以一个恒定的速度往桶里放入令牌,而如果请求需要被处理,则需要先从桶里获取一个令牌,当桶里没有令牌可取时,则拒绝服务。

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
}
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值