限流

限流又称为流量控制(流控),通常是指限制到达系统的并发请求数。

我们生活中也会经常遇到限流的场景,比如:某景区限制每日进入景区的游客数量为8万人;沙河地铁站早高峰通过站外排队逐一放行的方式限制同一时间进入车站的旅客数量等。

限流虽然会影响部分用户的使用体验,但是却能在一定程度上保障系统的稳定性,不至于崩溃(大家都没了用户体验)。

而互联网上类似需要限流的业务场景也有很多,比如电商系统的秒杀、微博上突发热点新闻、双十一购物节、12306抢票等等。这些场景下的用户请求量通常会激增,远远超过平时正常的请求量,此时如果不加任何限制很容易就会将后端服务打垮,影响服务的稳定性。

此外,一些厂商公开的API服务通常也会限制用户的请求次数,比如百度地图开放平台等会根据用户的付费情况来限制用户的请求数等。

常用的限流策略

漏桶

漏桶法限流很好理解,假设我们有一个水桶按固定的速率向下方滴落一滴水,无论有多少请求,请求的速率有多大,都按照固定的速率流出,对应到系统中就是按照固定的速率处理请求。

漏桶算法原理

漏桶法的关键点在于漏桶始终按照固定的速率运行,但是它并不能很好的处理有大量突发请求的场景,毕竟在某些场景下我们可能需要提高系统的处理效率,而不是一味的按照固定速率处理请求。

关于漏桶的实现,uber团队有一个开源的https://github.com/uber-go/ratelimit实现。 使用方法也比较简单,Take() 方法会返回漏桶下一次滴水的时间。

import (
 "fmt"
 "time"  "go.uber.org/ratelimit"
) func main() {
    rl := ratelimit.New(100) // per second     prev := time.Now()
    for i := 0; i < 10; i++ {
        now := rl.Take()
        fmt.Println(i, now.Sub(prev))
        prev = now
    }     // Output:
    // 0 0
    // 1 10ms
    // 2 10ms
    // 3 10ms
    // 4 10ms
    // 5 10ms
    // 6 10ms
    // 7 10ms
    // 8 10ms
    // 9 10ms
}

它的源码实现也比较简单,这里大致说一下关键的地方,有兴趣的同学可以自己去看一下完整的源码。

限制器是一个接口类型,其要求实现一个Take()方法:

type Limiter interface {
 // Take方法应该阻塞已确保满足 RPS
 Take() time.Time
}

实现限制器接口的结构体定义如下,这里可以重点留意下maxSlack字段,它在后面的Take()方法中的处理。

type limiter struct {
 sync.Mutex                // 锁
 last       time.Time      // 上一次的时刻
 sleepFor   time.Duration  // 需要等待的时间
 perRequest time.Duration  // 每次的时间间隔
 maxSlack   time.Duration  // 最大的富余量
 clock      Clock          // 时钟
}

limiter结构体实现Limiter接口的Take()方法内容如下:

// Take 会阻塞,确保两次请求之间的时间走完
// Take 调用平均数为 time.Second/rate.
func (t *limiter) Take() time.Time {
 t.Lock()
 defer t.Unlock()  now := t.clock.Now()  // 如果是第一次请求就直接放行
 if t.last.IsZero() {
  t.last = now
  return t.last
 }  // sleepFor 根据 perRequest 和上一次请求的时刻计算应该sleep的时间
 // 由于每次请求间隔的时间可能会超过perRequest, 所以这个数字可能为负数,并在多个请求之间累加
 t.sleepFor += t.perRequest - now.Sub(t.last)  // 我们不应该让sleepFor负的太多,因为这意味着一个服务在短时间内慢了很多随后会得到更高的RPS。
 if t.sleepFor < t.maxSlack {
  t.sleepFor = t.maxSlack
 }  // 如果 sleepFor 是正值那么就 sleep
 if t.sleepFor > 0 {
  t.clock.Sleep(t.sleepFor)
  t.last = now.Add(t.sleepFor)
  t.sleepFor = 0
 } else {
  t.last = now
 }  return t.last
}

上面的代码根据记录每次请求的间隔时间和上一次请求的时刻来计算当次请求需要阻塞的时间——sleepFor,这里需要留意的是sleepFor的值可能为负,在经过间隔时间长的两次访问之后会导致随后大量的请求被放行,所以代码中针对这个场景有专门的优化处理。maxSlack默认值可以通过创建限制器的New函数看到。

func New(rate int, opts ...Option) Limiter {
 l := &limiter{
  perRequest: time.Second / time.Duration(rate),
  maxSlack:   -10 * time.Second / time.Duration(rate),
 }
 for _, opt := range opts {
  opt(l)
 }
 if l.clock == nil {
  l.clock = clock.New()
 }
 return l
}

令牌桶

令牌桶其实和漏桶的原理类似,令牌桶按固定的速率往桶里放入令牌,并且只要能从桶里取出令牌就能通过,令牌桶支持突发流量的快速处理。

令牌桶原理

对于从桶里取不到令牌的场景,我们可以选择等待也可以直接拒绝并返回。

对于令牌桶的Go语言实现,大家可以参照https://github.com/juju/ratelimit

这个库支持多种令牌桶模式,并且使用起来也比较简单。

创建令牌桶的方法:

// 创建指定填充速率和容量大小的令牌桶
func NewBucket(fillInterval time.Duration, capacity int64) *Bucket
// 创建指定填充速率、容量大小和每次填充的令牌数的令牌桶
func NewBucketWithQuantum(fillInterval time.Duration, capacity, quantum int64) *Bucket
// 创建填充速度为指定速率和容量大小的令牌桶
// NewBucketWithRate(0.1, 200) 表示每秒填充20个令牌
func NewBucketWithRate(rate float64, capacity int64) *Bucket

取出令牌的方法:

// 取token(非阻塞)
func (tb *Bucket) Take(count int64) time.Duration
func (tb *Bucket) TakeAvailable(count int64) int64
// 最多等maxWait时间取token
func (tb *Bucket) TakeMaxDuration(count int64, maxWait time.Duration) (time.Duration, bool) // 取token(阻塞)
func (tb *Bucket) Wait(count int64)
func (tb *Bucket) WaitMaxDuration(count int64, maxWait time.Duration) bool

虽说是令牌桶,但是我们没有必要真的去生成令牌放到桶里,我们只需要每次来取令牌的时候计算一下,当前是否有足够的令牌可以使用就可以了,具体的计算公式如下。

当前令牌数 = 上一次剩余的令牌数 + (本次取令牌的时刻-上一次取令牌的时刻)/放置令牌的时间间隔 * 每次放置的令牌数

github.com/juju/ratelimit这个库中关于令牌数计算的具体实现如下:

func (tb *Bucket) adjustavailableTokens(tick int64) {
 if tb.availableTokens >= tb.capacity {
  return
 }
 tb.availableTokens += (tick - tb.latestTick) * tb.quantum
 if tb.availableTokens > tb.capacity {
  tb.availableTokens = tb.capacity
 }
 tb.latestTick = tick
 return
}

获取令牌的TakeAvailable函数关键部分的源码如下:

func (tb *Bucket) takeAvailable(now time.Time, count int64) int64 {
 if count <= 0 {
  return 0
 }
 tb.adjustavailableTokens(tb.currentTick(now))
 if tb.availableTokens <= 0 {
  return 0
 }
 if count > tb.availableTokens {
  count = tb.availableTokens
 }
 tb.availableTokens -= count
 return count
}

大家从代码中也可以看到其实令牌桶的实现并没有很复杂。

gin框架中使用限流中间件

在gin框架构建的项目中,我们可以将限流组件定义成中间件。

这里使用令牌桶作为限流策略,编写一个限流中间件如下:

func RateLimitMiddleware(fillInterval time.Duration, cap int64) func(c *gin.Context) {
 bucket := ratelimit.NewBucket(fillInterval, cap)
 return func(c *gin.Context) {
  // 如果取不到令牌就返回响应
  if bucket.TakeAvailable(1) == 0 {
   c.String(http.StatusOK, "rate limit...")
   c.Abort()
   return
  }
  c.Next()
 }
}

对于该限流中间件的注册位置,我们可以按照不同的限流策略将其添加到不同的地方,例如:

  1. 如果要对全站限流就可以添加成全局的中间件
  2. 如果是某一组路由需要限流,那么就只需添加到对应的路由组即可。

本文首发于我的个人博客:liwenzhou.com

 

漏桶、令牌桶限流的Go语言实现的更多相关文章

  1. 15行python代码,帮你理解令牌桶算法

    本文转载自: http://www.tuicool.com/articles/aEBNRnU   在网络中传输数据时,为了防止网络拥塞,需限制流出网络的流量,使流量以比较均匀的速度向外发送,令牌桶算法 ...

  2. 封装RateLimiter 令牌桶算法

    自定义注解封装RateLimiter.实例: @RequestMapping("/myOrder") @ExtRateLimiter(value = 10.0, timeOut = ...

  3. spring cloud gateway 之限流篇

    转载请标明出处: https://www.fangzhipeng.com 本文出自方志朋的博客 在高并发的系统中,往往需要在系统中做限流,一方面是为了防止大量的请求使服务器过载,导致服务不可用,另一方 ...

  4. 最近学习了限流与RateLimiter

    前言 分布式环境下应对高并发保证服务稳定几招,按照个人理解,优先级从高到低分别为缓存.限流.降级.熔断,每招都有它的作用,本文重点就讲讲限流这部分. 坦白讲,其实上面的说法也不准确,因为服务降级.熔断 ...

  5. 从SpringBoot构建十万博文聊聊限流特技

    前言 在开发十万博客系统的的过程中,前面主要分享了爬虫.缓存穿透以及文章阅读量计数等等.爬虫的目的就是解决十万+问题:缓存穿透是为了保护后端数据库查询服务:计数服务解决了接近真实阅读数以及数据库服务的 ...

  6. SpringBoot项目的限流

    开发访问量比较大的系统是,爬虫的目的就是解决访问量大的问题:缓存穿透是为了保护后端数据库查询服务:计数服务解决了接近真实访问量以及数据库服务的压力. 架构图 限流 就拿十万博客来说,如果存在热点文章, ...

  7. 微服务架构spring cloud - gateway网关限流

    1.算法 在高并发的应用中,限流是一个绕不开的话题.限流可以保障我们的 API 服务对所有用户的可用性,也可以防止网络攻击. 一般开发高并发系统常见的限流有:限制总并发数(比如数据库连接池.线程池). ...

  8. OpenResty实现限流的几种方式

      在开发 api 网关的时,做过一些简单的限流,比如说静态拦截和动态拦截:静态拦截说白了就是限流某一个接口在一定时间窗口的请求数.用户可以在系统上给他们的接口配置一个每秒最大调用量,如果超过这个限制 ...

  9. 服务熔断、降级、限流、异步RPC -- HyStrix

    背景 伴随着业务复杂性的提高,系统的不断拆分,一个面向用户端的API,其内部的RPC调用层层嵌套,调用链条可能会非常长.这会造成以下几个问题: API接口可用性降低 引用Hystrix官方的一个例子, ...

  10. Zuul【限流】

    在项目中,大部分都会使用到hyrtrix做熔断机制,通过某个预定的阈值来对异常流量进行降级处理,除了做服务降级以外,还可以对服务进行限流,分流,排队等. 当然,zuul也能做到限流策略,最简单的方式就 ...

随机推荐

  1. 使用Flexible实现手淘H5页面的终端适配

    拿到设计师给的设计图之后,剩下的事情是前端开发人员的事了.而手淘经过多年的摸索和实战,总结了一套移动端适配的方案--flexible方案. 这种方案具体在实际开发中如何使用,暂时先卖个关子,在继续详细 ...

  2. ios之VFL的补充(二)

    [self.contentView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"V:[languageI ...

  3. Unity3D安装多版本

    今天我也遇到这个版本更换问题.  老外也遇到了的!哈哈. 错误提示是: Fatal error!type = =kMetaAssetType & pathName.find("lib ...

  4. POJ 2992 求组合数的因子个数

    求C(n,k)的因子个数 C(n,k) = (n*(n-1)*...*(n-k+1))/(1*2*...*k) = p1^k1 * p2^k2 * ... * pt^kt 这里只要计算出分子中素数因子 ...

  5. c#创建、保存excel正常执行

    源地址:http://blog.csdn.net/hui_shixu/article/details/5785029

  6. MySQL高效分页解决方案集

    一,最常见MYSQL最基本的分页方式: select * from content order by id desc limit 0, 10 在中小数据量的情况下,这样的SQL足够用了,唯一需要注意的 ...

  7. delete 用法

    1.对象属性的删除 function fun(){ this.name = 'mm'; } var obj = new fun(); console.log(obj.name);//mm delete ...

  8. HDU 5256 - 序列变换 ,树状数组+离散化 ,二分法

    Problem Description 我们有一个数列A1,A2...An,你现在要求修改数量最少的元素,使得这个数列严格递增.其中无论是修改前还是修改后,每个元素都必须是整数.请输出最少需要修改多少 ...

  9. 关于ios 推送功能的终极解决

    刚刚做了一个使用推送功能的应用 遇到了一些问题整的很郁闷 搞了两天总算是弄明白了 特此分享给大家 本帖 主要是针对产品发布版本的一些问题 综合了网上一些资料根据自己实践写的 不过测试也可以看看 首先要 ...

  10. python之路第四篇(基础篇)

    一.冒泡算法实现: 方法一: li = [13,33,12,80,66,1] print li for m in range(4): num1 = li[m] num2 = li[m+1] if nu ...