开源限流组件分析(二):uber-go/ratelimit
文章目录
- 本系列
- 漏桶限流算法
- uber的漏桶算法
- 使用
- mutex版本
- 数据结构
- 获取令牌
- 松弛量
- atomic版本
- 数据结构
- 获取令牌
- 测试漏桶的松弛量
- 总结
本系列
- 开源限流组件分析(一):juju/ratelimit
- 开源限流组件分析(二):uber-go/ratelimit(本文)
- 开源限流组件分析(三):golang-time/rate
漏桶限流算法
漏桶限流算法思路很简单,水(数据或者请求)先进入到漏桶里,漏桶以一定的速度出水,当水流入速度过大会直接溢出,漏桶算法能强行限制请求的处理速度
相比于令牌桶中只要桶内还有剩余令牌,调用方就可以马上消费令牌的策略。漏桶相对来说更加严格,调用方只能按照预定的间隔顺序消费令牌
例如:假设漏桶出水的间隔是10ms,上次在0ms时刻出水,那么下次是10ms时刻,再下次是20ms时刻
uber的漏桶算法
uber对标准的漏桶算法做了一些优化,加了一些松弛量,这么做了后就能应对突发流量,达到和令牌桶一样的效果
关于什么是松弛量,在介绍获取令牌流程时再详细分析
阅读的源码:https://github.com/uber-go/ratelimit,版本:v0.3.1
使用
下面展示一个没有使用松弛量的漏桶使用示例:
func Example_default() {// 参数100:代表每秒放行100个请求,即每10ms放行一个请求rl := ratelimit.New(100)prev := time.Now()for i := 0; i < 10; i++ {// Take获取令牌,返回获取令牌成功的时间now := rl.Take()if i > 0 {// 打印每次放行间隔fmt.Println(i, now.Sub(prev))}prev = now}
}
打印结果:
1 10ms
2 10ms
3 10ms
4 10ms
5 10ms
6 10ms
7 10ms
8 10ms
9 10ms
可以看出每次放行间隔10ms,符合预期
uber的漏桶提供了mutex版本
和atomic版本
两种实现,主要区别在于怎么控制并发,以及怎么记录松弛量
mutex版本
数据结构
type mutexLimiter struct {sync.Mutex// 上次请求放行的时刻last time.Time// 桶中积累的松弛量sleepFor time.Duration// 每个请求之间的间隔perRequest time.Duration// 最大松弛量maxSlack time.Durationclock Clock
}
初始化桶:
func newMutexBased(rate int, opts ...Option) *mutexLimiter {config := buildConfig(opts)perRequest := config.per / time.Duration(rate)l := &mutexLimiter{perRequest: perRequest,// config.slack默认值=10, 例如当perRequest=10ms时// maxSlack就是 -10 * 100ms = -1smaxSlack: -1 * time.Duration(config.slack) * perRequest,clock: config.clock,}return l
}func buildConfig(opts []Option) config {c := config{clock: clock.New(),// 最大松弛量默认是perRequest的10倍slack: 10,per: time.Second,}for _, opt := range opts {opt.apply(&c)}return c
}
主要设置两个变量:
-
每次放行时间间隔
perRequest
:根据参数rate计算,rate表示每单位时间per能放行多少请求- 例如当per=1s,rate=100时,每次放行时间间隔perRequest=10ms
-
最大松弛量
maxSlack
:config.slack默认值=10- 例如当perRequest=10ms时,maxSlack就是
-10 * 100ms = -1s
- 例如当perRequest=10ms时,maxSlack就是
获取令牌
要实现标准的漏桶算法,其实比较简单:
记录上次放行时间last
,当本次请求到来时, 如果此刻的时间与last
相比并没有达到 perRequest 规定的间隔大小, sleep 一段时间即可
对应代码为(删除了松弛相关代码):
func (t *mutexLimiter) Take() time.Time {t.Lock()defer t.Unlock()now := t.clock.Now()// 如果第一次请求,直接放行if t.last.IsZero() {t.last = nowreturn t.last}// 当前时间距离上次放行时间的间隔,sleepFor := t.perRequest - now.Sub(t.last)// 如果比规定的间隔小,需要sleepif sleepFor > 0 {t.clock.Sleep(t.sleepFor)t.last = now.Add(t.sleepFor)t.sleepFor = 0return t.last
}
松弛量
如果不引入松弛量,按照标准漏桶算法的流程:
假设现在有三个请求,req1
、req2
和 req3
,获取令牌间隔perRequest规定为 10ms
-
req1
先到来 -
当
req1
完成之后20ms
,req2
才到来,此时距离上次放行有20ms
,大于10ms,可以对req2
放行 -
当
req2
放行后5ms
,req3
到来,此时距离上次放行才5ms
,不足 规定的间隔10ms
,因此还需要等待5ms
才能继续放行req3
这种策略有什么问题?无法应对任何突发流量,没有弹性,定死了相邻两次请求放行的间隔必须大于等于规定的值
于是引入了松弛量的概念:当较长时间(超过perRequest)没有请求到来时,会在桶中积累一些松弛量,这样接下来的请求可以先消耗松弛量,而不会被漏洞的获取令牌间隔限制。直到把松弛量耗尽为止
当然松弛量也不是无限积累,这样当很长时间没有请求后,就跟没有限流没区别了。因此桶中规定了最多积累多少松弛量maxSlack
加上松弛量后代获取令牌代码如下:
-
计算
perRequest - now.Sub(t.last)
- 如果大于0,说明当前时间距离上次放行时间不足规定间隔,需要等待,或者需要消耗松弛量
- 如果小于0,说明当前时间距离上次放行时间超过了规定间隔,那么当前请求一定可以放行,并且可以在桶中积累一些松弛量,给下次请求使用
- 将结果累加到t.sleepFor
-
假设累加的松弛值超过了maxSlack,修正为maxSlack,默认为10倍的规定间隔
-
如果
t.sleepFor > 0
,说明本次请求应该等待的时间,在使用完桶中的松弛量后还不够,需要sleep这段时间,结束后将sleepFor置为0 -
否则
t.sleepFor <= 0
,说明桶中的存储松弛余量可以满足本次请求,本次请求放行
func (t *mutexLimiter) Take() time.Time {t.Lock()defer t.Unlock()now := t.clock.Now()// 如果第一次请求,直接放行if t.last.IsZero() {t.last = nowreturn t.last}// 假设perRequest=10ms,now.Sub(t.last)=5ms,那么需要睡5ms// 假设perRequest=10ms,now.Sub(t.last)=15ms,说明距离上传请求的时间超过了漏桶的限流间隔10ms,那么sleepFor变为-5t.sleepFor += t.perRequest - now.Sub(t.last)// 默认最多松弛 10倍的perRequest// 如果累加的松弛值超过了maxSlack,修正为maxSlackif t.sleepFor < t.maxSlack {t.sleepFor = t.maxSlack}// 需要sleepif t.sleepFor > 0 {t.clock.Sleep(t.sleepFor)t.last = now.Add(t.sleepFor)t.sleepFor = 0// 如果sleepFor <= 0,说明还有存储的松弛余量,可以放行} else {t.last = now}return t.last
}
atomic版本
数据结构
type atomicInt64Limiter struct {// 上次在哪个时刻发放许可state int64// 每次请求间隔perRequest time.Duration// 最大松弛量maxSlack time.Durationclock Clock
}func newAtomicInt64Based(rate int, opts ...Option) *atomicInt64Limiter {config := buildConfig(opts)perRequest := config.per / time.Duration(rate)l := &atomicInt64Limiter{perRequest: perRequest,// 最大松弛量:10倍的perRequestmaxSlack: time.Duration(config.slack) * perRequest,clock: config.clock,}atomic.StoreInt64(&l.state, 0)return l
}
获取令牌
atomic版本的漏桶不是用一个变量存储桶中的松弛量,而是记录了上次在哪个时刻发放许可timeOfNextPermissionIssue
,那松弛量用什么表示呢?当前时间now减去上次发放许可时间timeOfNextPermissionIssue就代表松弛量
怎么理解?这个值代表应该在什么时刻发放令牌:
- 如果该值 比now大,说明需要sleep一段时间等待时间流逝,本次才能获取到令牌
- 如果该值 比now小,说明应该在更早的时间就可以获取令牌,本次请求不需要等待
每次请求时会在timeOfNextPermissionIssue的基础上加上perRequest,并更新timeOfNextPermissionIssue,表示本次发放许可的时间比上次前进了perRequest时间
如果较长时间没有请求,那天然就积累了一些松弛量,因为上次发放许可时间就会比较小,即便加上本次的消耗perRequest也没有now大,就可以直接放行
-
如果上次发放许可时间,加上固定间隔perRequest小于now,说明本次可以放行,并更新上次发放许可时间 += perRequest
- 当然timeOfNextPermissionIssue不能比now小太多,否则限流就没意义了。因此规定该值最多比now小10倍的perRequest,表示松弛量最多为perRequest的10倍
-
否则不能放行,需要等待时间流逝到timeOfNextPermissionIssue + perRequest为止
func (t *atomicInt64Limiter) Take() time.Time {var (newTimeOfNextPermissionIssue int64now int64)for {now = t.clock.Now().UnixNano()// 上次在哪个时刻发放许可timeOfNextPermissionIssue := atomic.LoadInt64(&t.state)switch {// 第一次请求,或者不是第一次请求,且(没有松弛量,且now 比 上次发放许可时间 多了超过perRequest )case timeOfNextPermissionIssue == 0 || (t.maxSlack == 0 && now-timeOfNextPermissionIssue > int64(t.perRequest)):// 本次放行,本次在now时刻发放许可newTimeOfNextPermissionIssue = now// 可以有松弛量,且 当前时间 - 上次发放许可时间 > 11倍perRequestcase t.maxSlack > 0 && now-timeOfNextPermissionIssue > int64(t.maxSlack)+int64(t.perRequest):// 本次在 now - 最大松弛量 时刻发放许可newTimeOfNextPermissionIssue = now - int64(t.maxSlack)/**1.没有松弛量,且当前时间距离 上次发放许可时间 超过了 不到perRequest,本次应该在上次发放许可时间 + perRequest 再发放2.有松弛量,且 当前时间 - 上次发放许可时间 <= 11倍perRequest,那本次应该在上次发放许可时间 + perRequest 再发放*/default:// 更新上次发放许可时间,+= perRequestnewTimeOfNextPermissionIssue = timeOfNextPermissionIssue + int64(t.perRequest)}if atomic.CompareAndSwapInt64(&t.state, timeOfNextPermissionIssue, newTimeOfNextPermissionIssue) {break}}sleepDuration := time.Duration(newTimeOfNextPermissionIssue - now)// 没有余量了,需要sleepif sleepDuration > 0 {t.clock.Sleep(sleepDuration)// 返回放行时的时间return time.Unix(0, newTimeOfNextPermissionIssue)}return time.Unix(0, now)
}
测试漏桶的松弛量
下面对漏桶的松弛量进行测试:如果先积累一些松弛量,就会起到因对突发流量的效果,例如让时间先走45ms
func Example_default() {// 每10ms放行一个请求, 最大松弛量=100msrl := ratelimit.New(100)// 在0ms发放许可rl.Take()// 此时时间来到45mstime.Sleep(time.Millisecond * 45)prev := time.Now()for i := 0; i < 10; i++ {now := rl.Take()if i > 0 {fmt.Println(i, now.Sub(prev))}prev = now}
}
打印结果:
1 1µs
2 103µs
3 4µs
4 4.79ms
5 10ms
6 10ms
7 10ms
8 10ms
9 10ms
可以看出:前4次都没等,直接获取到令牌,第4次等了大约5ms,之后每次放行间隔都是10ms
执行到for循环时,上次发放许可时间10ms,当前时刻45ms
拆解for循环中的每次take中的漏桶状态:
- i=0, 下次应该在
10ms
发放许可,当前时刻45ms,放行 - i=1, 下次应该在
20ms
发放许可,当前时刻45ms,放行 - i=2, 下次应该在
30ms
发放许可,当前时刻45ms,放行 - i=3, 下次应该在
40ms
发放许可,当前时刻45ms,放行 - i=4, 下次应该在
50ms
发放许可,当前时刻45ms,更新timeOfNextPermissionIssue=50,需要sleep5ms
- i=5, 下次应该在
60ms
发放许可,当前50ms,需要sleep10ms
,更新timeOfNextPermissionIssue=60
如下图所示:
总结
加上松弛量后,这个漏桶和标准的令牌桶区别就不大了:
- 都能应对一定的突发流量
- 当桶中没有松弛量(没有令牌时),都按照固定时间间隔放行请求
这个库有个问题是:对桶中等待中的请求数没有限制,这样当并发量非常大时,会导致请求一直在桶中堆积
因此工程中要使用的话,最好对源码进行改造:当等待中的请求数超过阈值就快速返回失败
相关文章:

开源限流组件分析(二):uber-go/ratelimit
文章目录 本系列漏桶限流算法uber的漏桶算法使用mutex版本数据结构获取令牌松弛量 atomic版本数据结构获取令牌测试漏桶的松弛量 总结 本系列 开源限流组件分析(一):juju/ratelimit开源限流组件分析(二):u…...

探索 SVG 创作新维度:svgwrite 库揭秘
文章目录 **探索 SVG 创作新维度:svgwrite 库揭秘**背景介绍库简介安装指南基础函数使用实战场景常见问题与解决方案总结 探索 SVG 创作新维度:svgwrite 库揭秘 背景介绍 在数字艺术和网页设计领域,SVG(Scalable Vector Graphic…...
为什么要做PFAS测试?PFAS检测项目详细介绍
PFAS测试之所以重要,主要归因于PFAS(全氟和多氟化合物)的广泛存在、持久性、生物累积性和潜在的毒性。这些特性使得PFAS在环境和人体中可能长期存在,并对生态系统和人类健康构成威胁。以下是对PFAS检测项目的详细介绍以及进行PFAS…...

稀土阻燃协效剂的应用
稀土阻燃协效剂是一类利用稀土元素(如铈、镧、钕、铕等)具有的独特性质,来增强材料阻燃性能的化学物质。在聚合物材料燃烧时可催化酯花成碳,迅速在高分子表面形成致密连续的碳层,隔绝聚合物材料内部的可燃性气体与氮气…...
Java的异常处理
常见异常 ① 运行时异常 a、ClassNotFoundException b、FileNotFoundException c、IOException ② 编译时异常 a、ArrayIndexOutOfBoundsException b、NullPointerException c、ClassCastException d、InputFormatException e、InputMismatchException f、ArithmeticException …...

免费域名邮箱申请和使用教程:有哪些步骤?
免费域名邮箱设置指南?如何免费注册烽火域名邮箱? 对于个人和企业而言,拥有一个专属的域名邮箱不仅能提升专业形象,还能增强品牌识别度。烽火将详细介绍如何申请和使用免费域名邮箱,帮助您轻松拥有一个专属的电子邮件…...

Linux之实战命令45:swapon应用实例(七十九)
简介: CSDN博客专家、《Android系统多媒体进阶实战》一书作者 新书发布:《Android系统多媒体进阶实战》🚀 优质专栏: Audio工程师进阶系列【原创干货持续更新中……】🚀 优质专栏: 多媒体系统工程师系列【…...

提升数据处理效率:TDengine S3 的最佳实践与应用
在当今数据驱动的时代,如何高效地存储与处理海量数据成为了企业面临的一大挑战。为了解决这一问题,我们在 TDengine 3.2.2.0 首次发布了企业级功能 S3 存储。这一功能经历多个版本的迭代与完善后,逐渐发展成为一个全面和高效的解决方案。 S3…...

高级算法设计与分析 学习笔记13 线性规划
注意是线性规划不是动态规划哦 好家伙,这不是凸优化吗? 凸优化标准形式: 先改成统一最大化(凸优化那边怎么是统一最小化?) 原来的x2正负无所谓,但我希望每个x都是有限制的,所以把它改…...

2024年11月软考中项应试技巧与机考注意事项!
软考中项的备考技巧 重点来了!这部分是我辛苦总结出来的备考技巧,都是我当年备考时逐渐整合出来的,绝对够用,赶紧跟我一起掌握吧! 1.基础知识 在学习时建议大家先跟着班课老师结合教材过一遍基础知识。强调跟着班课…...

网络编程中容易踩的坑罗列,谨记!
1、TCP没考虑粘包分包 TCP是面向连接的可靠协议,TCP是流式协议,创建TCP套接字的类型为SOCK_STREAM int sockfd socket(AF_INET, SOCK_STREAM, 0);很多同学面试时对书上的话背诵如流,在实际TCP编程中却没有处理粘包和分包的代码,以…...

SD-WAN:推动企业网络优化与发展
近年来,软件定义广域网(SD-WAN)逐渐成为众多企业的首选网络解决方案。这背后的原因是什么?接下来我们将深入探讨这一趋势。 在快速发展的通信技术领域,企业对高效、灵活且可扩展的网络架构需求愈发迫切。随着数据流量的…...

[MyBatis-Plus]扩展功能详解
代码生成 使用MP的步骤是非常固定的几步操作 基于插件, 可以快速的生成基础性的代码 安装插件安装完成后重启IEDA连接数据库 mp是数据库的名字?serverTimezoneUTC 是修复mysql时区, 不加会报错 生成代码 TablePrefix选项是用于去除表名的前缀, 比如根据tb_user表生成实体类U…...
循序渐进丨MogDB 5.0 远程访问 MogDB/Oracle 数据库的简便方法(使用@符号)
概述 早期的 MogDB 就提供了Postgres_fdw、Oracle_fdw、MySQL_fdw3个插件,用于远程访问 MogDB/Oracle/MySQL数据库。 旧的版本中,访问远程数据库的表,需要显式创建外部表,而在 MogDB 5.0当中,这种用法得到了简化&…...

大模型训练触达「瓶颈」,基座模型厂商还有必要坚持预训练吗?
进入2024年来,中国大模型行业从狂奔进入到了“长跑阶段”。无论是在技术侧,还是在产业侧,行业内都产生了更多新的思考。 从技术发展上看,在算力受限的情况下,中国基座模型的研发能力在全球范围内处在什么身位、如何追赶…...
media3 exoplayer 扩展解码库在这里 take it , please !
Media3 ExoPlayer 扩展解码库介绍 请注意,本文讨论的是 Media3 ExoPlayer 而不是 Google ExoPlayer2。详细参考:Media3 ExoPlayer 迁移指南 文章最后提供了已经编译好的AAR文件,直接引用即可!!! 为什么选…...

在Xshell中查看日志文件详情
学习总结 1、掌握 JAVA入门到进阶知识(持续写作中……) 2、学会Oracle数据库入门到入土用法(创作中……) 3、手把手教你开发炫酷的vbs脚本制作(完善中……) 4、牛逼哄哄的 IDEA编程利器技巧(编写中……) 5、面经吐血整理的 面试技…...

深入理解计算机系统--计算机系统漫游
对于一段最基础代码的文件hello.c,解释程序的运行 #include <stdio.h>int main() {printf ( "Hello, world\n") ;return 0; }1.1、信息就是位上下文 源程序是由值 0 和 1 组成的位(比特)序列,8 个位被组织成一组…...

哪些指标可以用来评估精益生产现场管理和改善的效果?
在探讨如何评估精益生产现场管理和改善效果时,我们需要关注一系列关键指标,这些指标不仅反映了生产流程的效率,还体现了质量和客户满意度的提升。详细如天行健企业管理咨询公司下文所述: 1. 生产效率 每小时生产数量(…...
在 Linux 系统上安装免费杀毒软件
选择合适的免费杀毒软件 Linux 上流行的免费杀毒软件: . ClamAV:最为知名的开源免费杀毒软件,支持多种 Linux 发行版。它可以扫描病毒、恶意软件以及 Windows 系统上的威胁。 Sophos Antivirus for Linux:虽然是商业软件ÿ…...
RestClient
什么是RestClient RestClient 是 Elasticsearch 官方提供的 Java 低级 REST 客户端,它允许HTTP与Elasticsearch 集群通信,而无需处理 JSON 序列化/反序列化等底层细节。它是 Elasticsearch Java API 客户端的基础。 RestClient 主要特点 轻量级ÿ…...
conda相比python好处
Conda 作为 Python 的环境和包管理工具,相比原生 Python 生态(如 pip 虚拟环境)有许多独特优势,尤其在多项目管理、依赖处理和跨平台兼容性等方面表现更优。以下是 Conda 的核心好处: 一、一站式环境管理:…...
日语学习-日语知识点小记-构建基础-JLPT-N4阶段(33):にする
日语学习-日语知识点小记-构建基础-JLPT-N4阶段(33):にする 1、前言(1)情况说明(2)工程师的信仰2、知识点(1) にする1,接续:名词+にする2,接续:疑问词+にする3,(A)は(B)にする。(2)復習:(1)复习句子(2)ために & ように(3)そう(4)にする3、…...

黑马Mybatis
Mybatis 表现层:页面展示 业务层:逻辑处理 持久层:持久数据化保存 在这里插入图片描述 Mybatis快速入门 
PPT|230页| 制造集团企业供应链端到端的数字化解决方案:从需求到结算的全链路业务闭环构建
制造业采购供应链管理是企业运营的核心环节,供应链协同管理在供应链上下游企业之间建立紧密的合作关系,通过信息共享、资源整合、业务协同等方式,实现供应链的全面管理和优化,提高供应链的效率和透明度,降低供应链的成…...
土地利用/土地覆盖遥感解译与基于CLUE模型未来变化情景预测;从基础到高级,涵盖ArcGIS数据处理、ENVI遥感解译与CLUE模型情景模拟等
🔍 土地利用/土地覆盖数据是生态、环境和气象等诸多领域模型的关键输入参数。通过遥感影像解译技术,可以精准获取历史或当前任何一个区域的土地利用/土地覆盖情况。这些数据不仅能够用于评估区域生态环境的变化趋势,还能有效评价重大生态工程…...
Rust 异步编程
Rust 异步编程 引言 Rust 是一种系统编程语言,以其高性能、安全性以及零成本抽象而著称。在多核处理器成为主流的今天,异步编程成为了一种提高应用性能、优化资源利用的有效手段。本文将深入探讨 Rust 异步编程的核心概念、常用库以及最佳实践。 异步编程基础 什么是异步…...

Ascend NPU上适配Step-Audio模型
1 概述 1.1 简述 Step-Audio 是业界首个集语音理解与生成控制一体化的产品级开源实时语音对话系统,支持多语言对话(如 中文,英文,日语),语音情感(如 开心,悲伤)&#x…...
数据库分批入库
今天在工作中,遇到一个问题,就是分批查询的时候,由于批次过大导致出现了一些问题,一下是问题描述和解决方案: 示例: // 假设已有数据列表 dataList 和 PreparedStatement pstmt int batchSize 1000; // …...
【碎碎念】宝可梦 Mesh GO : 基于MESH网络的口袋妖怪 宝可梦GO游戏自组网系统
目录 游戏说明《宝可梦 Mesh GO》 —— 局域宝可梦探索Pokmon GO 类游戏核心理念应用场景Mesh 特性 宝可梦玩法融合设计游戏构想要素1. 地图探索(基于物理空间 广播范围)2. 野生宝可梦生成与广播3. 对战系统4. 道具与通信5. 延伸玩法 安全性设计 技术选…...