使用 Go 语言中的Token桶算法构建速率限制器

本文介绍了如何使用 Go 语言和Token桶算法构建一个速率限制器,并将其与 Redis 集成以实现生产级别。文章详细讲解了速率限制的概念、Token Bucket 算法的原理和实现,以及如何使用 Redis 解决并发、无状态、多服务器同步和水平扩展等关键问题。

在暴露公共端点的系统中——无论是 Web3 API、交易平台、你的服务器,还是任何高吞吐量后端——速率限制都是一个至关重要的组件。它可以保护你的基础设施免受滥用,确保客户端之间的公平使用,并防御拒绝服务式行为。

在这篇文章中,我将介绍我们如何使用 GoToken桶算法构建一个速率限制器,然后通过集成 Redis 将其进一步提升到生产级别。你将看到我们如何使用诸如 TTL、原子操作和 Lua 脚本等 Redis 功能来应对诸如并发、无状态、多服务器同步和水平扩展等关键挑战。

无论你是在构建去中心化应用后端还是扩展 API 服务器,这种方法都应该为你提供一个干净且高性能的流量控制基础。

什么是速率限制?

速率限制限制了用户或系统在给定时间内可以执行的操作数量。它对于以下方面很有用:

  • 防止滥用或暴力攻击。
  • 保护第三方服务(例如,RPC 节点)。
  • 确保公平使用系统资源。

速率限制算法的类型

根据用例和问题陈述,业界使用了几种流行的速率限制算法。

  1. 固定窗口计数器(Fixed Window Counter)
  2. 滑动窗口日志(Sliding Window Log)
  3. 滑动窗口计数器(Sliding Window Counter)
  4. 漏桶(Leaky Bucket)
  5. Token桶(Token Bucket)(为我们的系统选择)

📖 要更深入地了解上述应用程序和工作原理,请参阅这篇必读的速率限制概述文章。

为什么选择Token桶?

我们选择 Token桶算法 的原因如下:

  • 它允许高达定义的限制的流量突发
  • 易于实现

Token桶的工作原理

  • 一个桶会保存固定数量的Token(比如 5 个)。
  • 每个请求“花费”1 个Token。
  • Token会随着时间的推移而补充(例如,每 2 秒补充 5 个Token)。
  • 如果桶是空的,则请求被拒绝。

一个简单的带内存存储的速率限制器——

type TokenBucket struct {
    Tokens       int
    Capacity     int
    RefillRate   int
    LastRefill   time.Time
    RefillPeriod time.Duration
    mu           sync.Mutex
}

func NewTokenBucket(capacity, refillRate int, refillPeriod time.Duration) *TokenBucket {
    return &TokenBucket{
        Capacity:     capacity,
        Tokens:       capacity,
        RefillRate:   refillRate,
        RefillPeriod: refillPeriod,
        LastRefill:   time.Now(),
    }
}

此结构会为每个用户初始化一个桶。它支持:

  • 使用 sync.Mutex并发安全
  • 基于经过时间的自动补充
type RateLimiter struct {
    Users    map[string]*TokenBucket
    Capacity int
    Window   time.Duration
    mu       sync.Mutex
}

func (rl *RateLimiter) CheckStatus(user string) bool {
    rl.mu.Lock()
    bucket, exists := rl.Users[user]
    if !exists {
        bucket = NewTokenBucket(rl.Capacity, rl.Capacity, rl.Window)
        rl.Users[user] = bucket
    }
    rl.mu.Unlock()

    bucket.mu.Lock()
    defer bucket.mu.Unlock()

    now := time.Now()
    if now.Sub(bucket.LastRefill) >= bucket.RefillPeriod {
        bucket.Tokens = bucket.Capacity
        bucket.LastRefill = now
    }

    return bucket.Tokens > 0
}

func (rl *RateLimiter) IncrementTokenUsed(user string) {
    rl.mu.Lock()
    bucket, _ := rl.Users[user]
    rl.mu.Unlock()

    bucket.mu.Lock()
    defer bucket.mu.Unlock()

    now := time.Now()
    if now.Sub(bucket.LastRefill) >= bucket.RefillPeriod {
        bucket.Tokens = bucket.Capacity
        bucket.LastRefill = now
    }

    if bucket.Tokens > 0 {
        bucket.Tokens--
    }
}

RateLimiter 结构有助于维护每个用户的Token桶。它包括诸如检查Token可用性(checkTokenStatus)和更新使用情况(incrementToken)之类的核心功能。这是一个基本的内存中实现,也通过锁支持并发。根据用例,你可以轻松地扩展此模型以支持用于不同 API 密钥或端点的多个速率限制器。

但是,这种内存方法存在一些关键限制:

  • 内存使用:内存占用会随着每个新用户而不断增长。
  • 无状态性:如果服务器重新启动,所有用户数据都将丢失。
  • 缺少多服务器同步:它无法跨多个实例进行扩展。
  • 竞争条件:由于Token状态在本地存储,因此多个服务器无法安全地共享用户速率限制。

引入 Redis 以实现可扩展性

为了克服这些问题,我们引入了 Redis

  • 过期处理:我们使用 Redis 的键过期来自动清理不活动用户——例如,在不活动 30 分钟后删除键。
  • 状态持久性:由于 Redis 在服务器外部,因此我们不再在服务器重新启动时丢失用户状态。
  • 多服务器同步:Redis 充当中心化缓存,允许所有应用程序实例访问共享的速率限制状态。
  • 竞争条件缓解:在 Redis 中使用 Lua 脚本 使我们能够执行原子操作,从而避免分布式系统中的不一致。(虽然这会增加少量延迟。)

奖励:

为了进一步提高可扩展性并减少延迟,我们还可以将负载均衡器(配置了 IP 哈希)放置在服务器前面。这可确保来自给定用户的请求始终命中同一台服务器,从而可以使用本地 Redis 集群而不是单个中心化 Redis——从而获得更好的性能和水平扩展。

你可以尝试许多组件来找到最适合你系统的组件。在 Redis 中,你可以实现 Redis 集群 以进行横向扩展并提高可用性——但是在使用 Lua 脚本 时必须小心,因为它们在集群环境中未得到完全支持。你也可以尝试使用 Redlock 算法,尽管它增加了复杂性并引入了一些延迟。或者,你可以考虑使用 API 网关,它通常带有内置的速率限制功能。

如果需要,你还可以集成诸如 Prometheus 和 Grafana 之类的监控工具来跟踪指标并为任何滥用行为设置警报——就像 BinanceMEXC 等平台所做的那样,以防止用户压垮他们的系统。

  • 原文链接: blog.blockmagnates.com/b...
  • 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
点赞 0
收藏 0
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论
blockmagnates
blockmagnates
The New Crypto Publication on The Block