【微服务网关——Go令牌桶限流】

1. time/rate限速器使用

  • 令牌桶限流算法
  • rate.NewLimiter(limit,burst)产生一个新的限速器
    • limit表示每秒产生token数、burst表示最多存token数
  • Allow判断当前是否可以取到token
  • Wait阻塞等待直到取到token
  • Reverse返回等待时间(预估的等待时间),再去取token
go 复制代码
package main

import (
	"context"
	"golang.org/x/time/rate"
	"log"
	"testing"
	"time"
)

func Test_RateLimiter(t *testing.T) {
	l := rate.NewLimiter(1, 5)
	log.Println(l.Limit(), l.Burst())
	for i := 0; i < 10; i++ {
		//阻塞等待直到,取到一个token
		log.Println("before Wait")
		c, _ := context.WithTimeout(context.Background(), time.Second*2)
		if err := l.Wait(c); err != nil {
			log.Println("limiter wait err:" + err.Error())
		}
		log.Println("after Wait")

		//返回需要等待多久才有新的token,这样就可以等待指定时间执行任务
		r := l.Reserve()
		log.Println("reserve Delay:", r.Delay())

		//判断当前是否可以取到token
		a := l.Allow()
		log.Println("Allow:", a)
		log.Println("======================")
	}
}

2. time/rate源码原理

  • 计算上次请求和当前请求时间差
  • 计算时间差内生成的token数+旧token数
  • 如果token为负,则计算等待时间
  • token为正,则请求后token-1
go 复制代码
type Limit float64

type Limiter struct {
	limit Limit//每秒产生的token数
	burst int//桶的总大小
	mu     sync.Mutex//锁
	tokens float64//token总数
	last time.Time//上一次更新token的时间
	lastEvent time.Time//最后一次限速的时间
}

Allow、Reverse、Wait三个方法底层调用的都是func (lim *Limiter) reserveN(now time.Time, n int, maxFutureReserve time.Duration) Reservation

go 复制代码
// reserveN 是 AllowN、ReserveN 和 WaitN 的辅助方法。
// maxFutureReserve 指定了允许的最大预订等待时间。
// reserveN 返回 Reservation(而不是 *Reservation),以避免在 AllowN 和 WaitN 中进行分配。
func (lim *Limiter) reserveN(now time.Time, n int, maxFutureReserve time.Duration) Reservation {
    // 加锁,保护临界区
    lim.mu.Lock()
    // 如果每秒产生的token数为无限,则无需预订直接返回
    if lim.limit == Inf {
        lim.mu.Unlock()
        return Reservation{
            ok:        true,        // 预订成功
            lim:       lim,         // 当前限流器
            tokens:    n,           // 预订的令牌数
            timeToAct: now,         // 立即生效
        }
    }
    // 更新当前时间、上次时间和现在可用令牌数
    now, last, tokens := lim.advance(now)
    // 计算请求n个tokens后的剩余令牌数
    tokens -= float64(n)
    // 计算等待时长
    var waitDuration time.Duration
    if tokens < 0 {
        // 如果令牌不够,需要等待的时间
        waitDuration = lim.limit.durationFromTokens(-tokens)
    }
    // 判断预订是否成功,请求的n是否小于等于桶的容量,且等待时间是否小于用户给的最大实践
    ok := n <= lim.burst && waitDuration <= maxFutureReserve
    // 准备预订结果
    r := Reservation{
        ok:    ok,            // 预订是否成功
        lim:   lim,           // 当前限流器
        limit: lim.limit,     // 当前限流器的限制
    }
    if ok {
        r.tokens = n               // 成功预订的令牌数
        r.timeToAct = now.Add(waitDuration) // 生效时间
    }

    // 更新限流器状态
    if ok {
        lim.last = now              // 更新上次预订时间
        lim.tokens = tokens         // 更新剩余令牌数
        lim.lastEvent = r.timeToAct // 更新上次事件时间
    } else {
        lim.last = last             // 未成功则恢复上次时间
    }

    // 解锁
    lim.mu.Unlock()
    return r  // 返回预订结果
}

// advance 计算并返回基于时间推移的 lim 的更新状态。
// lim 自身的状态不会被改变。
func (lim *Limiter) advance(now time.Time) (newNow time.Time, newLast time.Time, newTokens float64) {
	last := lim.last
	if now.Before(last) {
		// 如果 now 比 last 还早,则使用 now 作为 last
		last = now
	}
	// 避免 last 非常久远时导致 delta 溢出。
	// 计算多久后这个桶会自动填满
	maxElapsed := lim.limit.durationFromTokens(float64(lim.burst) - lim.tokens)
	elapsed := now.Sub(last)
	if elapsed > maxElapsed {
		// 如果实际时间间隔超过最大允许间隔,调整为最大间隔,避免由于非常大的 elapsed 造成溢出或不合理的计算。
		elapsed = maxElapsed
	}
	// 计算由于时间推移增加的令牌数
	delta := lim.limit.tokensFromDuration(elapsed)
	tokens := lim.tokens + delta
	if burst := float64(lim.burst); tokens > burst {
		// 如果计算得到的令牌数超过了 burst,则限制为 burst
		tokens = burst
	}
	// 返回更新后的时间 now, 上次时间 last 以及新的令牌数 tokens
	return now, last, tokens
}

// tokensFromDuration 是一个单位转换函数,
// 用于将时间段转换为在该时间段内以每秒 limit 个令牌的速率
// 可积累的令牌数。
func (limit Limit) tokensFromDuration(d time.Duration) float64 {
    // 自行分离整数部分和小数部分,以尽量减少舍入误差。
    // 参考 golang.org/issues/34861。
    sec := float64(d / time.Second) * float64(limit) // 计算整秒内的令牌数
    nsec := float64(d % time.Second) * float64(limit) // 计算剩余纳秒内的令牌数
    return sec + nsec / 1e9 // 返回整秒和纳秒对应的令牌数之和
}

func (lim *Limiter) reserveN(now time.Time, n int, maxFutureReserve time.Duration) Reservation

  • AllowN(now time.Time, n int) bool
    • lim.reserveN(now, n, 0).ok
      • now表示现在
      • n表示请求n个token
      • 0表示等待时间
  • ReserveN
    • lim.reserveN(now, n, InfDuration)
      • now表示现在
      • n表示请求n个token
      • InfDuration表示无限等待
  • WaitN
go 复制代码
// WaitN 阻塞直到 lim 允许 n 个事件发生。
// 如果 n 超过了 Limiter 的 burst 大小,Context 被取消,
// 或者预期的等待时间超过了 Context 的截止时间,它会返回一个错误。
// 如果速率限制是无限的(Inf),则忽略 burst 限制。
func (lim *Limiter) WaitN(ctx context.Context, n int) (err error) {
    // 加锁以安全地获取限流器的 burst 和 limit 值
    lim.mu.Lock()
    burst := lim.burst
    limit := lim.limit
    lim.mu.Unlock()
    // 如果 n 超过了 burst 且 limit 不是 Inf,则返回错误
    if n > burst && limit != Inf {
        return fmt.Errorf("rate: Wait(n=%d) exceeds limiter's burst %d", n, lim.burst)
    }
    // 检查 Context 是否已取消
    select {
    case <-ctx.Done():
        return ctx.Err()
    default:
    }
    // 查看ctx是否设定了deadline,确定最大等待时间
    now := time.Now()
    waitLimit := InfDuration
    if deadline, ok := ctx.Deadline(); ok {
        // 计算距离截止时间的剩余时间
        waitLimit = deadline.Sub(now)
    }
    // 进行预订
    r := lim.reserveN(now, n, waitLimit)
    if !r.ok {
        // 如果预订失败且等待时间超过 Context 截止时间,返回错误
        return fmt.Errorf("rate: Wait(n=%d) would exceed context deadline", n)
    }
    // 计算需要等待的时间
    delay := r.DelayFrom(now)
    if delay == 0 {
        // 如果不需要等待,直接返回
        return nil
    }
    // 启动定时器进行等待
    t := time.NewTimer(delay)
    defer t.Stop()
    select {
    case <-t.C:
        // 拿到了令牌
        return nil
    case <-ctx.Done():
        // 在等待时 Context 被取消,取消预订,允许其他事件提前进行
        r.Cancel()
        return ctx.Err()
    }
}

3. 小结

令牌桶算法广泛应用于控制 API 请求速率、限制资源访问频率、管理任务调度等场景。通过合理设置 limit 和 burst,可以有效平衡系统负载和服务质量。该算法并不会实时去维护令牌桶中的token的数量,而是通过last和lastEvent来巧妙的计算出该段时间内容桶内令牌的状态,同时通过锁来维护了对于令牌桶的访问一致性问题。

相关推荐
深蓝电商API16 小时前
将爬虫部署到服务器:Scrapy+Scrapyd 实现定时任务与监控
服务器·爬虫·scrapy
熊思宇16 小时前
ASP.NET Core Web API 发布到 IIS 服务器
服务器·microsoft·asp.net
搬砖的青蛙16 小时前
本地web测试服务器快速域名映射工具
服务器·域名映射·ip映射
sailwon17 小时前
自己搭建远程桌面服务器——私有化部署RustDesk
运维·服务器·云计算·远程工作
Garc17 小时前
Zookeeper删除提供者服务中的指定IP节点
linux·运维·服务器
写代码的小阿帆1 天前
Java体系总结——从基础语法到微服务
java·微服务·学习方法
SUPER52661 天前
FastApi项目启动失败 got an unexpected keyword argument ‘loop_factory‘
java·服务器·前端
一只小bit1 天前
MySQL 库的操作:从创建配置到备份恢复
服务器·数据库·mysql·oracle
利刃大大1 天前
【高并发服务器】二、时间轮定时器设计与实现
服务器·项目·cpp·高并发服务器
心灵宝贝1 天前
libopenssl-1_0_0-devel-1.0.2p RPM 包安装教程(openSUSE/SLES x86_64)
linux·服务器·数据库