首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >并发限流 vs 速率限流:一次压测教会你如何选型

并发限流 vs 速率限流:一次压测教会你如何选型

原创
作者头像
不做虫子
发布2025-09-08 20:32:06
发布2025-09-08 20:32:06
2760
举报

背景

最近在做组件化,组件化其中有一点就是需要有限流能力,针对不同的租户设置不同的限流上限,避免其中一个租户请求量过高影响其他租户。

限流器是一种服务治理能力,用于限制服务的并发调用量,以保护服务的稳定性。

调研

限流一般有单节点限流、集群限流(将限流数值对集群节点数求平均值,其本质还是单节点限流)、分布式限流。本文暂时只讨论单机的。

限流器类型方面一般分为并发数限流和速率限流两种。

并发数限流

并发数限流顾名思义,就是限制并发数,不让并发数过高

适用场景

  • 资源保护。例如最多支持多少个并发数据库操作,比较实用
  • 内存/cpu密集型操作,例如图片的并发处理
  • 文件io操作,例如最多支持多少个文件同时上传等等

优点

  • 精准控制并发数
  • 资源保护
  • 无时间依赖,只关心当前的并发数

缺点

  • 需要手动管理限流资源
  • 如果请求耗时特别长的情况下,可能导致饥饿,因为一直没有空闲的并发限流资源释放
  • 无法控制每秒请求数

用法

本次使用的是gozero的限流器

代码语言:go
复制
github.com/zeromicro/go-zero/core/syncx

初始化

代码语言:go
复制
zeroLimit = syncx.NewLimit(100)

使用

代码语言:go
复制
func (s *helloServerImpl) SayHello(ctx context.Context, req *hellopb.HelloRequest) (*hellopb.HelloReply, error) {
	if !zeroLimit.TryBorrow()() {
		return nil, status.Errorf(codes.ResourceExhausted, "rate limit exceeded")
	}
	defer zeroLimit.Return() // 务必务必
	time.Sleep(200 * time.Millisecond)
	return &hellopb.HelloReply{
		Message: "Hello " + req.Name,
	}, nil
}

这里一定要注意zeroLimit.Return()

因为内部是使用信号量去实现的,通过拿资源->归还资源来控制。

代码语言:go
复制
// TryBorrow() - 非阻塞获取资源
func (l Limit) TryBorrow() bool {
    select {
    case l.pool <- lang.Placeholder:  // 向 channel 发送信号
        return true
    default:
        return false  // 资源已满,获取失败
    }
}

// Return() - 归还资源
func (l Limit) Return() error {
    select {
    case <-l.pool:  // 从 channel 接收信号,释放资源
        return nil
    default:
        return ErrLimitReturn  // 归还过多资源
    }
}

如果忘记调用Return(),后果很严重

因为造成了信号量的资源泄露,一直没有归还,导致限流器认为还有其他并发,结果就是导致后面的请求都被拒绝。

效果

速率限流

速率限流就是控制每秒请求数的,跟我们直觉上任务的限流是一致的。

适用场景

  • api限流,控制每秒请求数
  • 消费队列中消费速率的控制
  • 爬虫/数据采集,也算是控制速度

优点

  • 限流资源自动管理,代码上简单
  • 突发处理能力,一般都是令牌桶算法,桶容量允许一定的突发请求

缺点

  • 无法控制并发数
  • 突发请求可能过载,桶满了,可能瞬时处理大量请求,对应优点的第二点

用法

使用的框架为官方库

代码语言:go
复制
golang.org/x/time/rate

初始化

代码语言:go
复制
rateLimit := rate.Limit(100)
	// 创建限流器
	limiter = rate.NewLimiter(rateLimit, 100)

使用上比较简单,没有什么要特别注意的事项,不需要归还资源。

代码语言:go
复制
func (s *helloServerImpl) SayHello(ctx context.Context, req *hellopb.HelloRequest) (*hellopb.HelloReply, error) {
	// if !zeroAllow() {
	// 	return nil, status.Errorf(codes.ResourceExhausted, "rate limit exceeded")
	// }
	// defer zeroLimit.Return()
	if !limiter.Allow() {
		return nil, status.Errorf(codes.ResourceExhausted, "rate limit exceeded")
	}
	time.Sleep(200 * time.Millisecond)
	return &hellopb.HelloReply{
		Message: "Hello " + req.Name,
	}, nil
}
效果

2000个请求花费了484毫秒,我们设置的是1秒内最多100个请求,算是限流成功,成功数量有波动正常,因为要算上桶的初始量(100)+这段时间内新生成的令牌(允许突发流量的特性)

总结

两种限流器各有适用场景,需要根据实际场景选用,当然,也可以组合使用,效果更佳,但是也更复杂。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

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

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

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

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 背景
  • 调研
    • 并发数限流
      • 适用场景
      • 优点
      • 缺点
      • 用法
    • 速率限流
      • 适用场景
      • 优点
      • 缺点
      • 用法
  • 总结
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档