前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >限流领域的黑科技:揭秘分布式限流镇山秘籍,真的那么神奇吗?

限流领域的黑科技:揭秘分布式限流镇山秘籍,真的那么神奇吗?

作者头像
程序视点
发布2024-06-07 14:53:08
760
发布2024-06-07 14:53:08
举报
文章被收录于专栏:程序小小事

前言

单机限流指针对单一服务器的情况,通过限制单台服务器在单位时间内处理的请求数量,防止服务器过载。常见的限流算法(固定窗口限流算法滑动窗口算法、漏桶限流算法)上文已介绍,其优点在于实现简单,效率高,效果明显。

随着微服务架构的普及,系统的服务通常会部署在多台服务器上,此时就需要分布式限流来保证整个系统的稳定性。接下本文会介绍几种常见的分布式限流技术方案:

基于中心化的限流方案

方案原理

通过一个中心化的限流器来控制所有服务器的请求。实现方式:

  1. 选择一个中心化的组件,例如— Redis。
  2. 定义限流规则,例如:可以设置每秒钟允许的最大请求数(QPS),并将这个值存储在 Redis 中。
  3. 对于每个请求,服务器需要先向 Redis 请求令牌。
  4. 如果获取到令牌,说明请求可以被处理;如果没有获取到令牌,说明请求被限流,可以返回一个错误信息或者稍后重试。

代码实现

代码语言:javascript
复制
package main

import (
   "context"
   "fmt"
   "go.uber.org/atomic"
   "sync"

   "git.code.oa.com/pcg-csd/trpc-ext/redis"
)

type RedisClient interface {
   Do(ctx context.Context, cmd string, args ...interface{}) (interface{}, error)
}

// Client 数据库
type Client struct {
   client RedisClient // redis 操作
   script string      // lua脚本
}

// NewBucketClient 创建redis令牌桶
func NewBucketClient(redis RedisClient) *Client {
   helper := redis
   return &Client{
      client: helper,
      script: `
         -- 令牌桶限流脚本
         -- KEYS[1]: 桶的名称
         -- ARGV[1]: 桶的容量
         -- ARGV[2]: 令牌产生速率
         
         local bucket = KEYS[1]
         local capacity = tonumber(ARGV[1])
         local tokenRate = tonumber(ARGV[2])
         
         local redisTime = redis.call('TIME')
         local now = tonumber(redisTime[1])
         
         local tokens, lastRefill = unpack(redis.call('hmget', bucket, 'tokens', 'lastRefill'))
         tokens = tonumber(tokens)
         lastRefill = tonumber(lastRefill)
         
         if not tokens or not lastRefill then
            tokens = capacity
            lastRefill = now
         else
            local intervalsSinceLast = (now - lastRefill) * tokenRate
            tokens = math.min(capacity, tokens + intervalsSinceLast)
         end
         
         if tokens < 1 then
            return 0
         else
            redis.call('hmset', bucket, 'tokens', tokens - 1, 'lastRefill', now)
            return 1
         end
      `,
   }
}

// 获取令牌,获取成功则立即返回true,否则返回false
func (c *Client) isAllowed(ctx context.Context, key string, capacity int64, tokenRate int64) (bool, error) {
   result, err := redis.Int(c.client.Do(ctx, "eval", c.script, 1, key, capacity, tokenRate))
   if err != nil {
      fmt.Println("Redis 执行错误:", err)
      return false, err
   }
   return result == 1, nil
}

// 调用检测
func main() {
   c := NewBucketClient(redis.GetPoolByName("redis://127.0.0.1:6379"))
   gw := sync.WaitGroup{}
   gw.Add(120)
   count := atomic.Int64{}
   for i := 0; i < 120; i++ {
      go func(i int) {
         defer gw.Done()
         status, err := c.isAllowed(context.Background(), "test", 100, 10)
         if status {
            count.Add(1)
         }
         fmt.Printf("go %d status:%v error: %v\n", i, status, err)
      }(i)
   }
   gw.Wait()
   fmt.Printf("allow %d\n\n", count.Load())
}

执行结果:

存在的问题

  1. 性能瓶颈:由于所有的请求都需要经过 Redis,因此 Redis 可能成为整个系统的性能瓶颈。为了解决这个问题,可以考虑使用 Redis 集群来提高性能,或者使用更高性能的硬件。
  2. 单点故障:如果 Redis 出现故障,整个系统的限流功能将受到影响。为了解决这个问题,可以考虑使用 Redis 的主从复制或者哨兵模式来实现高可用。
  3. 网络带宽:Redis 是一个基于网络通信的内存数据库,因此网络带宽是其性能的一个关键因素。如果网络带宽有限,可能会导致请求的传输速度变慢,从而影响 Redis 的性能。

基于负载均衡的分布式限流方案

方案原理

可以看到中心化限流方案的存在较高的单点故障风险,且带宽瓶颈比较严重。在这个基础上本文结合本地缓存单机限流和负载均衡设计了一个新的分布式限流方案。具体方案如下:

  1. 使用负载均衡器或分布式服务发现(北极星即可做到),将请求均匀地分发到每个机器上。这确保了每个机器都能处理一部分请求。
  2. 在每个机器上维护本机的限流状态,实现本地缓存单机限流的逻辑。使用令牌桶限流算法,在每个机器上独立地进行限流控制。每秒钟处理的请求数、令牌桶的令牌数量等。根据本地限流状态,对到达的请求进行限流判断。
  3. 准备相应的动态调整方案,可以根据每个机器的实际负载情况,动态地调整限流参数。例如,如果一个机器的 CPU 或内存使用率过高,你可以降低这个机器的限流阈值,减少这个机器的请求处理量。反之,如果一个机器的资源使用率较低,提高这个机器的限流阈值,增加这个机器的请求处理量。

存在的问题

  1. 本地缓存:这个方案对本地缓存的要求较高,需要自己根据业务逻辑抉择本地缓存的淘汰策略和缓存容量限制等风险点。
  2. 限流精度:本地缓存单机限流的精度受限于每个服务实例的资源和配置。这可能导致限流策略无法精确地适应整个系统的流量变化,无法灵活地调整限流规则。
  3. 请求负载均衡器的单点故障。
  4. 动态扩缩容的适应性:当系统需要动态扩展或缩容时,该方案可能需要额外的配置和和调整,以确保新加入或移除的服务实例能够正确地参与限流和请求均衡。

基于分布式协调服务的限流

方案原理

使用 ZooKeeper 或者 etcd 等分布式协调服务来实现限流。每台服务器都会向分布式协调服务申请令牌,只有获取到令牌的请求才能被处理。基本方案:

  1. 初始化令牌桶:在 ZooKeeper 中创建一个节点,节点的数据代表令牌的数量。初始时,将数据设置为令牌桶的容量。
  2. 申请令牌:当一个请求到达时,服务器首先向 ZooKeeper 申请一个令牌。这可以通过获取节点的分布式锁,然后将节点的数据减1实现。如果操作成功,说明申请到了令牌,请求可以被处理;如果操作失败,说明令牌已经用完,请求需要被拒绝或者等待。
  3. 释放令牌:当一个请求处理完毕时,服务器需要向 ZooKeeper 释放一个令牌。这可以通过获取节点的分布式锁,然后将节点的数据加1实现。
  4. 补充令牌:可以设置一个定时任务,定期向 ZooKeeper 中的令牌桶补充令牌。补充的频率和数量可以根据系统的负载情况动态调整。

存在的问题

这个方案的优点是可以实现精确的全局限流,并且可以避免单点故障。但是,这个方案的缺点是实现复杂,且对 ZooKeeper 的性能有较高的要求。如果 ZooKeeper 无法处理大量的令牌申请和释放操作,可能会成为系统的瓶颈。

小结

总之,没有最好的方案,只有合适的方案。在选择合适的限流方案时,我们需要考虑多种因素,包括系统的需求、现有的技术栈、系统的负载情况以及底层系统的性能等。理解每种方案的工作原理和特性,以便在实际应用中做出最佳的选择。

限流是保证系统稳定和高效运行的重要手段,但它并不是唯一的解决方案。我们还需要考虑其他的系统设计和优化手段,例如负载均衡、缓存、异步处理等(面对爆量,扩容永远是最好的方式,除了贵!)。这些手段相互配合,才能构建出一个既能应对高并发请求,又能保证服务质量的系统。

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2024-06-02,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 程序视点 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 前言
相关产品与服务
云数据库 Redis
腾讯云数据库 Redis(TencentDB for Redis)是腾讯云打造的兼容 Redis 协议的缓存和存储服务。丰富的数据结构能帮助您完成不同类型的业务场景开发。支持主从热备,提供自动容灾切换、数据备份、故障迁移、实例监控、在线扩容、数据回档等全套的数据库服务。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档