限流器是后台服务中十分重要的组件,在实际的业务场景中使用居多,其设计在微服务、网关、和一些后台服务中会经常遇到。限流器的作用是用来限制其请求的速率,保护后台响应服务,以免服务过载导致服务不可用现象出现。
限流器的实现方法有很多种,例如 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 生产出来,才可以消费令牌,继续请求,剩余的则是将其请求抛弃,当然在实际的业务处理中,可以用比较友好的方式反馈给前端。
在这里,先有的几次请求都会成功,是因为服务启动后,令牌桶会初始化,将令牌放入到桶中,但是随着突发流量的请求,令牌按照预定的速率生产令牌,就会出现明显的令牌供不应求的现象。
到此这篇关于golang限流器time/rate设计与实现详解的文章就介绍到这了,更多相关go限流器time/rate内容请搜索代码网以前的文章或继续浏览下面的相关文章希望大家以后多多支持代码网!
发表评论