开源限流组件分析(二):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.Duration
	clock      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 = -1s
		maxSlack: -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

获取令牌

要实现标准的漏桶算法,其实比较简单:

记录上次放行时间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 = now
		return t.last
	}

	// 当前时间距离上次放行时间的间隔,
	sleepFor := t.perRequest - now.Sub(t.last)

	// 如果比规定的间隔小,需要sleep
	if sleepFor > 0 {
		t.clock.Sleep(t.sleepFor)
		t.last = now.Add(t.sleepFor)
		t.sleepFor = 0

	return t.last
}

松弛量

如果不引入松弛量,按照标准漏桶算法的流程:

假设现在有三个请求,req1req2req3,获取令牌间隔perRequest规定为 10ms

  1. req1 先到来

  2. req1 完成之后 20msreq2 才到来,此时距离上次放行有20ms,大于10ms,可以对req2 放行

  3. req2 放行后5msreq3 到来,此时距离上次放行才5ms,不足 规定的间隔10ms,因此还需要等待 5ms 才能继续放行req3

在这里插入图片描述

这种策略有什么问题?无法应对任何突发流量,没有弹性,定死了相邻两次请求放行的间隔必须大于等于规定的值

于是引入了松弛量的概念:当较长时间(超过perRequest)没有请求到来时,会在桶中积累一些松弛量,这样接下来的请求可以先消耗松弛量,而不会被漏洞的获取令牌间隔限制。直到把松弛量耗尽为止

当然松弛量也不是无限积累,这样当很长时间没有请求后,就跟没有限流没区别了。因此桶中规定了最多积累多少松弛量maxSlack

加上松弛量后代获取令牌代码如下:

  1. 计算perRequest - now.Sub(t.last)

    1. 如果大于0,说明当前时间距离上次放行时间不足规定间隔,需要等待,或者需要消耗松弛量
    2. 如果小于0,说明当前时间距离上次放行时间超过了规定间隔,那么当前请求一定可以放行,并且可以在桶中积累一些松弛量,给下次请求使用
    3. 将结果累加到t.sleepFor
  2. 假设累加的松弛值超过了maxSlack,修正为maxSlack,默认为10倍的规定间隔

  3. 如果t.sleepFor > 0,说明本次请求应该等待的时间,在使用完桶中的松弛量后还不够,需要sleep这段时间,结束后将sleepFor置为0

  4. 否则t.sleepFor <= 0,说明桶中的存储松弛余量可以满足本次请求,本次请求放行

func (t *mutexLimiter) Take() time.Time {
	t.Lock()
	defer t.Unlock()

	now := t.clock.Now()

	// 如果第一次请求,直接放行
	if t.last.IsZero() {
		t.last = now
		return t.last
	}

	// 假设perRequest=10ms,now.Sub(t.last)=5ms,那么需要睡5ms
	// 假设perRequest=10ms,now.Sub(t.last)=15ms,说明距离上传请求的时间超过了漏桶的限流间隔10ms,那么sleepFor变为-5
	t.sleepFor += t.perRequest - now.Sub(t.last)


	// 默认最多松弛 10倍的perRequest
	// 如果累加的松弛值超过了maxSlack,修正为maxSlack
	if t.sleepFor < t.maxSlack {
		t.sleepFor = t.maxSlack
	}

	// 需要sleep
	if 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.Duration
	clock      Clock
}

func newAtomicInt64Based(rate int, opts ...Option) *atomicInt64Limiter {
	
	config := buildConfig(opts)
	perRequest := config.per / time.Duration(rate)
	l := &atomicInt64Limiter{
		perRequest: perRequest,
		// 最大松弛量:10倍的perRequest
		maxSlack: 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 int64
		now                          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倍perRequest
		case 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:
			// 更新上次发放许可时间,+= perRequest
			newTimeOfNextPermissionIssue = timeOfNextPermissionIssue + int64(t.perRequest)
		}

		if atomic.CompareAndSwapInt64(&t.state, timeOfNextPermissionIssue, newTimeOfNextPermissionIssue) {
			break
		}
	}

  
	sleepDuration := time.Duration(newTimeOfNextPermissionIssue - now)
	// 没有余量了,需要sleep
    if sleepDuration > 0 {
		t.clock.Sleep(sleepDuration)
		// 返回放行时的时间
		return time.Unix(0, newTimeOfNextPermissionIssue)
	}
	return time.Unix(0, now)
}

测试漏桶的松弛量

下面对漏桶的松弛量进行测试:如果先积累一些松弛量,就会起到因对突发流量的效果,例如让时间先走45ms

func Example_default() {
	// 每10ms放行一个请求, 最大松弛量=100ms
	rl := ratelimit.New(100)

	// 在0ms发放许可
	rl.Take()
	// 此时时间来到45ms
	time.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中的漏桶状态:

  1. i=0, 下次应该在10ms发放许可,当前时刻45ms,放行
  2. i=1, 下次应该在20ms发放许可,当前时刻45ms,放行
  3. i=2, 下次应该在30ms发放许可,当前时刻45ms,放行
  4. i=3, 下次应该在40ms发放许可,当前时刻45ms,放行
  5. i=4, 下次应该在50ms发放许可,当前时刻45ms,更新timeOfNextPermissionIssue=50,需要sleep 5ms
  6. i=5, 下次应该在60ms发放许可,当前50ms,需要sleep10ms,更新timeOfNextPermissionIssue=60

如下图所示:

在这里插入图片描述

总结

加上松弛量后,这个漏桶和标准的令牌桶区别就不大了:

  • 都能应对一定的突发流量
  • 当桶中没有松弛量(没有令牌时),都按照固定时间间隔放行请求

这个库有个问题是:对桶中等待中的请求数没有限制,这样当并发量非常大时,会导致请求一直在桶中堆积

因此工程中要使用的话,最好对源码进行改造:当等待中的请求数超过阈值就快速返回失败

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/900727.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

部署前后端分离若依项目--CentOS7宝塔版

准备&#xff1a; CentOS7服务器一台 通过网盘分享的文件&#xff1a;CentOS 7 h 链接: https://pan.baidu.com/s/17DF8eRSSDuj9VeqselGa_Q 提取码: s7x4 大家有需要可以下载这个&#xff0c;密码61 若依前端编译后文件 通过网盘分享的文件&#xff1a;ruoyi-admin.jar 链…

生信软件39 - GATK最佳实践流程重构,提高17倍分析速度的LUSH流程

1. LUSH流程简介 基因组测序通常用于分子诊断、分期和预后&#xff0c;而大量测序数据在分析时间方面提出了挑战。 对于从FASTQ到VCF的整个流程&#xff0c;LUSH流程在非GVCF和GVCF模式下都大大降低了运行时间&#xff0c;30 X WGS数据耗时不到2 h&#xff0c;从BAM到VCF约需…

【计网】UDP Echo Server与Client实战:从零开始构建简单通信回显程序

目录 前言&#xff1a; 1.实现udpserver类 1.1.创建udp socket 套接字 --- 必须要做的 socket&#xff08;&#xff09;讲解 代码实现&#xff1a;​编辑 代码讲解&#xff1a; 1.2.填充sockaddr_in结构 代码实现&#xff1a; 代码解析&#xff1a; 1.3.bind sockfd和…

linux中级wed服务器(https搭建加密服务器)

一。非对称加密算法&#xff1a; 公钥&#xff1a;公共密钥&#xff0c;开放 私钥&#xff1a;私有密钥&#xff0c;保密 1.发送方用自己的公钥加密&#xff0c;接受方用发送方的私钥解密&#xff1a;不可行 2.发送方用接受方的公钥加密&#xff0c;接受方用自己的私钥解密…

ffmpeg视频滤镜: 色温- colortemperature

滤镜简述 colortemperature 官网链接 》 FFmpeg Filters Documentation 这个滤镜可以调节图片的色温&#xff0c;色温值越大显得越冷&#xff0c;可以参考一下下图&#xff1a; 咱们装修的时候可能会用到&#xff0c;比如选择灯还有地板的颜色的时候&#xff0c;选暖色调还是…

【论文+源码】基于spring boot的垃圾分类网站

创建一个基于Spring Boot的垃圾分类网站涉及多个步骤&#xff0c;包括环境搭建、项目创建、数据库设计、后端服务开发、前端页面设计等。下面我将引导您完成这个过程。 第一步&#xff1a;准备环境 确保您的开发环境中安装了以下工具&#xff1a; Java JDK 8 或更高版本Mav…

Unity URP ShaderGraph 基本设置

先简单了解一下各种渲染管线之间的区别 Unity 从 2019.3 版本开始正式支持通用渲染管线&#xff08;URP&#xff0c;Universal Render Pipeline&#xff09;。URP 是轻量渲染管线&#xff08;LWRP&#xff0c;Lightweight Render Pipeline&#xff09;的升级和重命名版本&…

【解决】使用Hypermark将Markdown文件转化为HTML文件

写在前面&#xff1a; 如果文章对你有帮助&#xff0c;记得点赞关注加收藏一波&#xff0c;利于以后需要的时候复习&#xff0c;多谢支持&#xff01; 文章目录 一、文件准备&#xff08;一&#xff09;HTML模板文件&#xff08;二&#xff09;MD文件夹和储存文件夹 二、文件转…

COSCon'24 志愿者招募令:共创开源新生活!

亲爱的开源爱好者们&#xff0c; 第九届中国开源年会&#xff08;COSCon24&#xff09;即将在北京中关村国家自主创新示范区会议中心于2024年11月2日至3日隆重举行。今年的主题是“Open Source, Open Life&#xff5c;开源新生活”&#xff0c;旨在探索开源技术如何在各个领域推…

日常记录,使用springboot,vue2,easyexcel使实现字段的匹配导入

目前的需求是数据库字段固定&#xff0c;而excel的字段不固定&#xff0c;需要实现excel导入到一个数据库内。 首先是前端的字段匹配&#xff0c;显示数据库字段和表头字段 读取表头字段&#xff1a; 我这里实现的是监听器导入&#xff0c;需要新建一个listen类。 读Excel …

uniApp 加载google地图 并规划路线

uniApp 加载google地图 并规划路线 备注:核心代码实例 备注: 打开谷歌地图失败的话 参考google开发文档 https://developers.google.com/maps/documentation/urls/ios-urlscheme?hlzh-cn#swift核心代码 mounted() {this.loadGoogleMapsScript(); }, methods: {//加载loadGo…

AI服务器HBA卡的国产PCIe4.0/5.0 switch信号完整性设计与实现,支持定制(二)

表 &#xff12; 展示了 &#xff30;&#xff23;&#xff22; 板所选介质材料 &#xff30;&#xff33;&#xff32;&#xff14;&#xff10;&#xff10;&#xff10;&#xff21;&#xff35;&#xff33;&#xff17;&#xff10;&#xff13; &#xff0c; &#xff3…

解决Redis缓存穿透(缓存空对象、布隆过滤器)

文章目录 背景代码实现前置实体类常量类工具类结果返回类控制层 缓存空对象布隆过滤器结合两种方法 背景 缓存穿透是指客户端请求的数据在缓存中和数据库中都不存在&#xff0c;这样缓存永远不会生效&#xff0c;这些请求都会打到数据库 常见的解决方案有两种&#xff0c;分别…

使用DolphinScheduler接口实现批量导入工作流并上线

使用DS接口实现批量导入工作量并上线脚本 前面实现了批量生成DS的任务&#xff0c;当导入时发现只能逐个导入&#xff0c;因此通过接口实现会更方便。 DS接口文档 DS是有接口文档的地址是 http://IP:12345/dolphinscheduler/swagger-ui/index.html?languagezh_CN&lang…

安全见闻---清风

注&#xff1a;本文章源于泷羽SEC&#xff0c;如有侵权请联系我&#xff0c;违规必删 学习请认准泷羽SEC学习视频:https://space.bilibili.com/350329294 安全见闻1 泷哥语录&#xff1a;安全领域什么都有&#xff0c;不要被表象所迷惑&#xff0c;无论技术也好还是其他方面…

网站建设中需要注意哪些安全问题?----雷池社区版

服务器与应用安全指南 1. 服务器安全 1.1 操作系统安全 及时更新补丁&#xff1a;确保操作系统始终安装最新补丁&#xff0c;以防范系统漏洞。例如&#xff0c;Windows Server 定期推送安全更新&#xff0c;修复如远程代码执行等潜在威胁。优化系统服务配置&#xff1a;关闭不…

PoissonRecon学习笔记

1. Screened Poisson Reconstruction (SPR) 源码&#xff1a;https://github.com/mkazhdan/PoissonRecon However, as noted by several researchers, it suffers from a tendency to over-smooth the data. 泊松重建存在过度平滑的现象。 方法&#xff1a;position and gradi…

【C++】一文带你深入理解C++异常机制

⭐️个人主页&#xff1a;小羊 ⭐️所属专栏&#xff1a;C 很荣幸您能阅读我的文章&#xff0c;诚请评论指点&#xff0c;欢迎欢迎 ~ 目录 前言一、C语言处理错误的方式二、C异常三、异常的使用3.1 异常的抛出和捕获3.2 异常的重新抛出3.3 异常安全3.4 异常规范 四、自定义异…

擎创科技声明

近日&#xff0c;我司陆续接到求职者反映&#xff0c;有自称是擎创科技招聘人员&#xff0c;冒用“上海擎创信息技术有限公司”名义&#xff0c;用“126.com”的邮箱向求职者发布招聘信息&#xff0c;要求用户下载注册APP&#xff0c;进行在线测评。 对此&#xff0c;我司郑重…

7、哈希表

7、哈希表 哈希表最主要的作用就是把一个比较庞大的空间或者值域 映射到比较小的值域 (0-n) 就是将-10^9 ~10^9 映射到 0 ~10^5 一、存储结构 映射的方法可以是 h(x) x mod 10^5 但是这样映射会出现一个问题 可能会有重复的数字出现 所以就引出了两个方法 开放寻址法 和…