漏桶、令牌桶限流的Go语言实现
限流
限流又称为流量控制(流控),通常是指限制到达系统的并发请求数。
我们生活中也会经常遇到限流的场景,比如:某景区限制每日进入景区的游客数量为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()
}
}
对于该限流中间件的注册位置,我们可以按照不同的限流策略将其添加到不同的地方,例如:
- 如果要对全站限流就可以添加成全局的中间件
- 如果是某一组路由需要限流,那么就只需添加到对应的路由组即可。
本文首发于我的个人博客:liwenzhou.com
漏桶、令牌桶限流的Go语言实现的更多相关文章
- Guava-RateLimiter实现令牌桶控制接口限流方案
一.前言 对于一个应用系统来说,我们有时会遇到极限并发的情况,即有一个TPS/QPS阀值,如果超了阀值可能会导致服务器崩溃宕机,因此我们最好进行过载保护,防止大量请求涌入击垮系统.对服务接口进行限流可 ...
- ASP.NET Core中使用漏桶算法限流
漏桶算法是限流的四大主流算法之一,其应用场景各种资料中介绍的不多,一般都是说应用在网络流量控制中.这里举两个例子: 1.目前家庭上网都会限制一个固定的带宽,比如100M.200M等,一栋楼有很多的用户 ...
- php 基于redis使用令牌桶算法 计数器 漏桶算法 实现流量控制
通常在高并发和大流量的情况下,一般限流是必须的.为了保证服务器正常的压力.那我们就聊一下几种限流的算法. 计数器计数器是一种最常用的一种方法,在一段时间间隔内,处理请求的数量固定的,超的就不做处理. ...
- 15行python代码,帮你理解令牌桶算法
本文转载自: http://www.tuicool.com/articles/aEBNRnU 在网络中传输数据时,为了防止网络拥塞,需限制流出网络的流量,使流量以比较均匀的速度向外发送,令牌桶算法 ...
- 封装RateLimiter 令牌桶算法
自定义注解封装RateLimiter.实例: @RequestMapping("/myOrder") @ExtRateLimiter(value = 10.0, timeOut = ...
- 使用Guava的RateLimiter完成简单的大流量限流
限流的一般思路: 1.随机丢弃一定规则的用户(迅速过滤掉90%的用户): 2.MQ削峰(比如设一个MQ可以容纳的最大消息量,达到这个量后MQ给予reject): 3.业务逻辑层使用RateLimite ...
- spring cloud gateway 之限流篇
转载请标明出处: https://www.fangzhipeng.com 本文出自方志朋的博客 在高并发的系统中,往往需要在系统中做限流,一方面是为了防止大量的请求使服务器过载,导致服务不可用,另一方 ...
- 最近学习了限流与RateLimiter
前言 分布式环境下应对高并发保证服务稳定几招,按照个人理解,优先级从高到低分别为缓存.限流.降级.熔断,每招都有它的作用,本文重点就讲讲限流这部分. 坦白讲,其实上面的说法也不准确,因为服务降级.熔断 ...
- 从SpringBoot构建十万博文聊聊限流特技
前言 在开发十万博客系统的的过程中,前面主要分享了爬虫.缓存穿透以及文章阅读量计数等等.爬虫的目的就是解决十万+问题:缓存穿透是为了保护后端数据库查询服务:计数服务解决了接近真实阅读数以及数据库服务的 ...
随机推荐
- leetcode刷题记录——栈和队列
题目 232.用栈实现队列 class MyQueue { private Stack<Integer> in = new Stack<>(); private Stack&l ...
- MySQL最全存储引擎、索引使用及SQL优化的实践
1 MySQL的体系结构概述 整个MySQL Server由以下组成 :Connection Pool :连接池组件Management Services & Utilities :管理服务和 ...
- Golang omitempty 的用法
原文链接:https://blog.csdn.net/skh2015java/article/details/90720692omitempty作用是在json数据结构转换时,当该字段的值为该字段类型 ...
- Centos7 yum安装Python3.6环境,超简单
原文链接:https://blog.51cto.com/wenguonideshou/2083301 配置好Python3.6和pip3安装EPEL和IUS软件源 yum install epel-r ...
- 2020.5.27 第七篇 Scrum冲刺博客
Team:银河超级无敌舰队 Project:招新通 项目冲刺集合贴:链接 目录 一.每日站立会议 1.1 会议照片 1.2 项目完成情况 二.项目燃尽图 三.签入记录 3.1 代码/文档签入记录 3. ...
- Hive SQL 优化面试题整理
Hive优化目标 在有限的资源下,执行效率更高 常见问题: 数据倾斜 map数设置 reduce数设置 其他 Hive执行 HQL --> Job --> Map/Reduce 执行计划 ...
- Jenkins匿名用户设置
最近自己安装配置jenkins,但是跑任务时,发现是匿名账户登录,可以在系统设置中点击如下: 2.勾选“启用安全”,“访问控制”>“安全域”选择“Jenkins专有用户数据库”,并勾选“允许用户 ...
- 从零开始的SpringBoot项目 ( 三 ) 项目打包( jar包篇 )
一.准备工作 1.工具:Idea , maven 2.首先得保证 pom 有 maven 插件 二.开始打包 找到最右边的Maven Projects,点击进去,选择需要打包的项目,并点击 insta ...
- 新网站seo如何优化
http://www.wocaoseo.com/thread-189-1-1.html 做seo优化也有一段时间了,有做过自己的博客,自己的论坛,也有做过公司企业的网站,有同时一个人完成8个 ...
- JavaScript闭包(内存泄漏、溢出以及内存回收),超直白解析
1 引言 变量作用域 首先我们先铺垫一个知识点--变量作用域: 变量根据作用域的不同分为两种:全局变量和局部变量. 函数内部可以使用全局变量. 函数外部不可以使用局部变量. 当函数执行完毕,本作用域内 ...