乐趣区

关于golang:Golang-标准库-限流器-timerate-设计与实现

情谊提醒:此篇文章大概须要浏览 12 分钟 7 秒,不足之处请多指教,感激你的浏览。订阅本站

限流器是后盾服务中非常重要的组件,在理论的业务场景中应用居多,其设计在微服务、网关、和一些后盾服务中会常常遇到。限流器的作用是用来限度其申请的速率,爱护后盾响应服务,免得服务过载导致服务不可用景象呈现。

限流器 的实现办法有很多种,例如 Token Bucket、滑动窗口法、Leaky Bucket 等。

在 Golang 库中官网给咱们提供了限流器的实现golang.org/x/time/rate,它是基于令牌桶算法(Token Bucket)设计实现的。

令牌桶算法

令牌桶设计比较简单,能够简略的了解成一个只能寄存固定数量雪糕! 的一个冰箱,每个申请能够了解成来拿雪糕的人,有且只能每一次申请拿一块,那雪糕拿完了会怎么样呢?这里会有一个固定放雪糕的工人,并且他往冰箱里放雪糕的频率都是统一的,例如他 1s 中只能往冰箱里放 10 块雪糕,这里就能够看出申请响应的频率了。

令牌桶设计概念:

  • 令牌:每次申请只有拿到 Token 令牌后,才能够持续拜访;
  • :具备固定数量的桶,每个桶中最多只能放设计好的固定数量的令牌;
  • 入桶频率:依照固定的频率往桶中放入令牌,放入令牌不能超过桶的容量。

也就是说,基于令牌桶设计算法就限度了申请的速率,达到申请响应可控的目标,特地是针对于高并发场景中突发流量申请的景象,后盾就能够轻松应答申请了,因为到后端具体服务的时候突发流量申请曾经通过了限流了。

具体设计

限流器定义

type Limiter struct {
    mu        sync.Mutex // 互斥锁(排他锁)limit     Limit      // 放入桶的频率  float64 类型
    burst     int        // 桶的大小
    tokens    float64    // 令牌 token 以后残余的数量
    last      time.Time  // 最近取走 token 的工夫
    lastEvent time.Time  // 最近限流事件的工夫
}

limit、burst 和 token 是这个限流器中外围的参数,申请并发的大小在这里实现的。

在令牌发放之后,会存储在 Reservation 预约对象中:

type Reservation struct {
    ok        bool      // 是否满足条件调配了 token
    lim       *Limiter  // 发送令牌的限流器
    tokens    int       // 发送 token 令牌的数量
    timeToAct time.Time // 满足令牌发放的工夫
    limit     Limit     // 令牌发放速度
}

生产 Token

Limiter 提供了三类办法供用户生产 Token,用户能够每次生产一个 Token,也能够一次性生产多个 Token。而每种办法代表了当 Token 有余时,各自不同的对应伎俩。

Wait、WaitN

func (lim *Limiter) Wait(ctx context.Context) (err error)
func (lim *Limiter) WaitN(ctx context.Context, n int) (err error)

其中,Wait 就是 WaitN(ctx, 1),在上面的办法介绍实现也是一样的。

应用 Wait 办法生产 Token 时,如果此时桶内 Token 数组有余 (小于 n),那么 Wait 办法将会阻塞一段时间,直至 Token 满足条件。如果短缺则间接返回。

Allow、AllowN

func (lim *Limiter) Allow() bool
func (lim *Limiter) AllowN(now time.Time, n int) bool 

AllowN 办法示意,截止到以后某一时刻,目前桶中数目是否至多为 n 个,满足则返回 true,同时从桶中生产 n 个 token。
反之返回不生产 Token,false。

通常对应这样的线上场景,如果申请速率过快,就间接丢到某些申请。

Reserve、ReserveN

官网提供的限流器有阻塞期待式的 Wait,也有直接判断形式的 Allow,还有提供了本人保护预留式的,但外围的实现都是上面的 reserveN 办法。

func (lim *Limiter) Reserve() *Reservation
func (lim *Limiter) ReserveN(now time.Time, n int) *Reservation

当调用实现后,无论 Token 是否短缺,都会返回一个 Reservation * 对象。

你能够调用该对象的 Delay() 办法,该办法返回了须要期待的工夫。如果等待时间为 0,则阐明不必期待。
必须等到等待时间完结之后,能力进行接下来的工作。

或者,如果不想期待,能够调用 Cancel() 办法,该办法会将 Token 偿还。

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

    // 首先判断是否放入频率是否为无穷大
    // 如果为无穷大,阐明临时不限流
    if lim.limit == Inf {lim.mu.Unlock()
        return Reservation{
            ok:        true,
            lim:       lim,
            tokens:    n,
            timeToAct: now,
        }
    }

    // 拿到截至 now 工夫时
    // 能够获取的令牌 tokens 数量及上一次拿走令牌的工夫 last
    now, last, tokens := lim.advance(now)

    // 更新 tokens 数量
    tokens -= float64(n)

    // 如果 tokens 为正数,代表以后没有 token 放入桶中
    // 阐明须要期待,计算期待的工夫
    var waitDuration time.Duration
    if tokens < 0 {waitDuration = lim.limit.durationFromTokens(-tokens)
    }

    // 计算是否满足调配条件
    // 1、须要调配的大小不超过桶的大小
    // 2、等待时间不超过设定的期待时长
    ok := n <= lim.burst && waitDuration <= maxFutureReserve

    // 预处理 reservation
    r := Reservation{
        ok:    ok,
        lim:   lim,
        limit: lim.limit,
    }
    // 若以后满足调配条件
    // 1、设置调配大小
    // 2、满足令牌发放的工夫 = 以后工夫 + 期待时长
    if ok {
        r.tokens = n
        r.timeToAct = now.Add(waitDuration)
    }

    // 更新 limiter 的值,并返回
    if ok {
        lim.last = now
        lim.tokens = tokens
        lim.lastEvent = r.timeToAct
    } else {lim.last = last}

    lim.mu.Unlock()
    return r
}

具体应用

rate 包中提供了对限流器的应用,只须要指定 limit(放入桶中的频率)、burst(桶的大小)。

func NewLimiter(r Limit, b int) *Limiter {
    return &Limiter{
        limit: r, // 放入桶的频率
        burst: b, // 桶的大小
    }
}

在这里,应用一个 http api 来简略的验证一下 time/rate 的弱小:

func main() {r := rate.Every(1 * time.Millisecond)
    limit := rate.NewLimiter(r, 10)
    http.HandleFunc("/", func(writer http.ResponseWriter, request *http.Request) {if limit.Allow() {fmt.Printf("申请胜利,以后工夫:%s\n", time.Now().Format("2006-01-02 15:04:05"))
        } else {fmt.Printf("申请胜利,然而被限流了。。。\n")
        }
    })

    _ = http.ListenAndServe(":8081", nil)
}

在这里,我把桶设置成了每一毫秒投放一次令牌,桶容量大小为 10,起一个 http 的服务,模仿后盾 API。

接下来做一个压力测试,看看成果如何:

func GetApi() {
    api := "http://localhost:8081/"
    res, err := http.Get(api)
    if err != nil {panic(err)
    }
    defer res.Body.Close()

    if res.StatusCode == http.StatusOK {fmt.Printf("get api success\n")
    }
}

func Benchmark_Main(b *testing.B) {
    for i := 0; i < b.N; i++ {GetApi()
    }
}

成果如下:

......
申请胜利,以后工夫:2020-08-24 14:26:52
申请胜利,然而被限流了。。。申请胜利,然而被限流了。。。申请胜利,然而被限流了。。。申请胜利,然而被限流了。。。申请胜利,然而被限流了。。。申请胜利,以后工夫:2020-08-24 14:26:52
申请胜利,然而被限流了。。。申请胜利,然而被限流了。。。申请胜利,然而被限流了。。。申请胜利,然而被限流了。。。......

在这里,能够看到,当应用 AllowN 办法中,只有当令牌 Token 生产进去,才能够生产令牌,持续申请,残余的则是将其申请摈弃,当然在理论的业务解决中,能够用比拟敌对的形式反馈给前端。

在这里,先有的几次申请都会胜利,是因为服务启动后,令牌桶会初始化,将令牌放入到桶中,然而随着突发流量的申请,令牌依照预约的速率生产令牌,就会呈现显著的令牌供不应求的景象。

开源文化

目前 time/rate 是一个独立的限流器开源解决方案,感兴趣的小伙伴能够给此我的项目一个 Star,谢谢。

GitHub
golang/time

参考文章

  • 限流器系列(2) — Token Bucket 令牌桶
  • Golang 限流器的应用和实现
  • Golang 规范库限流器 time/rate 应用介绍
  • https://github.com/golang/time/rate.go
退出移动版