谈谈经典限流方法—漏桶、令牌桶与Guava RateLimiter的实现

2024-09-06 21:08

本文主要是介绍谈谈经典限流方法—漏桶、令牌桶与Guava RateLimiter的实现,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

大数据技术与架构

点击右侧关注,大数据开发领域最强公众号!

暴走大数据

点击右侧关注,暴走大数据!

高并发的业务系统经常要接受大流量的考验,为了保证系统的响应度和稳定性,往往都需要对有风险的接口实施限流(rate limiting),更高大上的说法则是“流量整形”(traffic shaping)。限流的思想最初来源于计算机网络,有两种经典的方法:漏桶令牌桶。本文先来稍微研究一下它们。

漏桶(Leaky Bucket)

将请求想象成水龙头里流出的水,接口中有一个底部开孔的桶,如下图所示。

这样就能使请求处理的速率有一个稳定的上限。很显然,如果进水的速率比开孔出水的速率大的话,水(请求)就会逐渐在桶中积累。若一直积累直到溢出了,那些溢出去的水(请求)就会被直接丢弃,也就是拒绝服务(denial of service, DoS)。

漏桶的思路非常简单直接,易于实现。但是如果当前的网络环境中充斥着短时流量尖峰,始终保持匀速的漏桶就无法很好地处理了,故后来又产生了令牌桶算法。

令牌桶(Token Bucket)

顾名思义,令牌桶里放的不再是请求,而是令牌,可以理解为处理请求的通行证。如下图所示。

令牌以恒定的速率产生并放入桶中。桶的大小是有限的,也就是说桶满了之后,多余的令牌就扔掉了。每当请求到来时,需要从桶中获取一个令牌才能真正地处理它。如果桶里没有令牌,该请求就会被丢弃。

可见,由于桶里预先留有一定量的令牌,故可以保证在突发流量到来且令牌没耗尽前平滑地处理,比漏桶更灵活一些。

Spark Streaming的限流是靠令牌桶来实现的,具体来讲是Google Guava提供的限流器——RateLimiter。它的设计比较聪明,下面简单地看一看。

Guava RateLimiter

类图如下

RateLimiter抽象类提供限流的所有功能,它的实现类只有SmoothRateLimiter。而SmoothRateLimiter的具体策略又由它的两个内部子类来实现。

  • SmoothBursty:

    兼容突发流量的令牌桶实现,也就是上一节描述的经典令牌桶算法。

  • SmoothWarmingUp:

    带预热过程的令牌桶实现,即在桶较满时产生令牌的速度较慢,随着令牌的消耗慢慢增长到恒定速率。

    如下图所示,x轴为令牌数,y轴为延迟,从右向左看的梯形区域就是令牌消耗的预热过程。

本文以经典的SmoothBursty为范本来讲。先说明一下SmoothRateLimiter的4个属性的含义,它们都非常重要。

  • storedPermits:

    当前桶里有的令牌数量。

  • maxPermits:

    桶装满时的令牌数量,storedPermits不会比它大。

  • stableIntervalMicros:

    产生令牌的频率(时间间隔),单位为微秒。

    举个栗子,如果我们想限制系统的QPS为10,亦即每秒有10个令牌放入桶中,那么stableIntervalMicros的值就是100000。

  • nextFreeTicketMicros:

    下一个令牌可用的时间戳,也就是下一个请求能够被处理的时间戳,单位为微秒。

    该值会随着当前请求获得令牌而增大(因为时间是自然流动的)。

    若当前请求的令牌数超出可用令牌数,这个时间就被推后(需要时间产生新的令牌)。

    当然,如果有一段时间没有请求进入的话,它就会保持在上次请求的过去时间戳。

RateLimiter要发挥作用,首先得产生令牌,由SmoothRateLimiter.resync()方法来实现。

void resync(long nowMicros) {if (nowMicros > nextFreeTicketMicros) {double newPermits = (nowMicros - nextFreeTicketMicros) / coolDownIntervalMicros();      storedPermits = min(maxPermits, storedPermits + newPermits);      nextFreeTicketMicros = nowMicros;    }  }@Overridedouble coolDownIntervalMicros() {return stableIntervalMicros;  }
在当前时间戳大于nextFreeTicketMicros(即后者是一个过去的时间戳)的情况下,就用它们的时间差除以产生令牌的频率,结果就是这段时间内应该产生的令牌数,进而更新桶内的现有令牌数storedPermits,以及将nextFreeTicketMicros更新到现在。

可见,RateLimiter的令牌是延迟(lazy)生成的,也就是说每次受理当前请求时,如果系统已经空闲了一定时间,才会计算上次请求到当前时间应该产生多少个令牌,而不是使用单独的任务来定期产生令牌——因为定时器无法保证较高的精度,并且性能不佳。当然,如果令牌已经“超支”,当前就不需要再更新令牌了。

接下来则是获取令牌,由SmoothRateLimiter.reserveEarliestAvailable()方法来实现,这个方法名非常self-explanatory了。

@Overridefinal long reserveEarliestAvailable(int requiredPermits, long nowMicros) {    resync(nowMicros);long returnValue = nextFreeTicketMicros;double storedPermitsToSpend = min(requiredPermits, this.storedPermits);double freshPermits = requiredPermits - storedPermitsToSpend;long waitMicros =        storedPermitsToWaitTime(this.storedPermits, storedPermitsToSpend)            + (long) (freshPermits * stableIntervalMicros);
this.nextFreeTicketMicros = LongMath.saturatedAdd(nextFreeTicketMicros, waitMicros);this.storedPermits -= storedPermitsToSpend;return returnValue;  }@Overridelong storedPermitsToWaitTime(double storedPermits, double permitsToTake) {return 0L;  }

这个方法的执行流程如下:

  • 调用上述resync()方法产生令牌。

  • 计算实际能提供的令牌数storedPermitsToSpend,它其实就是本次请求需要的令牌数requiredPermits和桶中有的令牌数storedPermits的较小值。

  • 计算需要新产生的令牌数freshPermits。

    当上一步桶中有的令牌不够用时,该值就大于0。

  • 根据freshPermits计算新产生这批令牌需要多长时间,记为waitMicros。

    由于SmoothBursty始终以恒定速率产生令牌,只需要将它与令牌产生的速率简单相乘就行。

    SmoothWarmingUp需要考虑预热的延时,所以storedPermitsToWaitTime()方法实现要复杂得多。

  • 更新nextFreeTicketMicros和storedPermits的值。

弄明白了产生令牌和获取令牌的细节,我们回到RateLimiter的顶层,看看它到底是如何发挥作用的。

Guava提供了多个静态create()方法创建RateLimiter,其中创建SmoothBursty的源码如下。

public static RateLimiter create(double permitsPerSecond) {return create(permitsPerSecond, SleepingStopwatch.createFromSystemTimer());  }
@VisibleForTestingstatic RateLimiter create(double permitsPerSecond, SleepingStopwatch stopwatch) {    RateLimiter rateLimiter = new SmoothBursty(stopwatch, 1.0 );    rateLimiter.setRate(permitsPerSecond);return rateLimiter;  }
可见,我们创建RateLimiter时只需要指定期望的QPS。那么SmoothBursty构造方法中的maxBurstSeconds参数有什么用?顾名思义,它表示在令牌桶满时能够承受的突发流量时长,这样就能够确定出桶的最大容量maxPermits了:
maxPermits = maxBurstSeconds * permitsPerSecond;

不过maxBurstSeconds当前不支持自定义,Guava规定为1秒,故桶的最大容量总是与我们指定的QPS相同。

RateLimiter向用户提供了acquire()方法用于获取令牌,以及带超时的tryAcquire()方法。下面的代码示出从acquire()到reserveEarliestAvailable()的调用链,方法都很短。

@CanIgnoreReturnValuepublic double acquire() {return acquire(1);  }@CanIgnoreReturnValuepublic double acquire(int permits) {long microsToWait = reserve(permits);    stopwatch.sleepMicrosUninterruptibly(microsToWait);return 1.0 * microsToWait / SECONDS.toMicros(1L);  }final long reserve(int permits) {    checkPermits(permits);synchronized (mutex()) {return reserveAndGetWaitLength(permits, stopwatch.readMicros());    }final long reserveAndGetWaitLength(int permits, long nowMicros) {long momentAvailable = reserveEarliestAvailable(permits, nowMicros);return max(momentAvailable - nowMicros, 0);  }
可见:
  • 获取令牌的过程是加锁的。

  • reserveAndGetWaitLength()返回的是获取令牌需要等待的时间,在acquire()方法中会借助Stopwatch阻塞(睡眠)直到获取成功。

    Stopwatch是Guava自行实现的一个高精度计时器。

  • acquire()方法会将上述等待时间返回,但不需要用户再处理了,所以该返回值可以忽略(即@CanIgnoreReturnValue注解的含义)。

最后举个简单的例子吧。

public class RateLimiterExample {  public static void main(String[] args) throws Exception {    RateLimiter rateLimiter = RateLimiter.create(10);    Random random = new Random();    for (int i = 0; i < 20; i++) {      int numPermits = random.nextInt(20);      System.out.println(numPermits + "\t" + rateLimiter.acquire(numPermits));    }  }}

运行结果如下。

2   0.013  0.1987924   1.2940886   0.3962218  0.5951612  1.79779814  1.19866414  1.39732113  1.3962616  1.2995343   1.5954539   0.2993254   0.89885718  0.3949732   1.79583513  0.19592611  1.2948512   1.095513   0.1948576   0.297884
可见,每次获取的令牌数和获取时需要等待的时长是符合上面所述逻辑的预期的。

http://www.ngui.cc/el/5716362.html

这篇关于谈谈经典限流方法—漏桶、令牌桶与Guava RateLimiter的实现的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/1143127

相关文章

hdu1043(八数码问题,广搜 + hash(实现状态压缩) )

利用康拓展开将一个排列映射成一个自然数,然后就变成了普通的广搜题。 #include<iostream>#include<algorithm>#include<string>#include<stack>#include<queue>#include<map>#include<stdio.h>#include<stdlib.h>#include<ctype.h>#inclu

【C++】_list常用方法解析及模拟实现

相信自己的力量,只要对自己始终保持信心,尽自己最大努力去完成任何事,就算事情最终结果是失败了,努力了也不留遗憾。💓💓💓 目录   ✨说在前面 🍋知识点一:什么是list? •🌰1.list的定义 •🌰2.list的基本特性 •🌰3.常用接口介绍 🍋知识点二:list常用接口 •🌰1.默认成员函数 🔥构造函数(⭐) 🔥析构函数 •🌰2.list对象

【Prometheus】PromQL向量匹配实现不同标签的向量数据进行运算

✨✨ 欢迎大家来到景天科技苑✨✨ 🎈🎈 养成好习惯,先赞后看哦~🎈🎈 🏆 作者简介:景天科技苑 🏆《头衔》:大厂架构师,华为云开发者社区专家博主,阿里云开发者社区专家博主,CSDN全栈领域优质创作者,掘金优秀博主,51CTO博客专家等。 🏆《博客》:Python全栈,前后端开发,小程序开发,人工智能,js逆向,App逆向,网络系统安全,数据分析,Django,fastapi

让树莓派智能语音助手实现定时提醒功能

最初的时候是想直接在rasa 的chatbot上实现,因为rasa本身是带有remindschedule模块的。不过经过一番折腾后,忽然发现,chatbot上实现的定时,语音助手不一定会有响应。因为,我目前语音助手的代码设置了长时间无应答会结束对话,这样一来,chatbot定时提醒的触发就不会被语音助手获悉。那怎么让语音助手也具有定时提醒功能呢? 我最后选择的方法是用threading.Time

Android实现任意版本设置默认的锁屏壁纸和桌面壁纸(两张壁纸可不一致)

客户有些需求需要设置默认壁纸和锁屏壁纸  在默认情况下 这两个壁纸是相同的  如果需要默认的锁屏壁纸和桌面壁纸不一样 需要额外修改 Android13实现 替换默认桌面壁纸: 将图片文件替换frameworks/base/core/res/res/drawable-nodpi/default_wallpaper.*  (注意不能是bmp格式) 替换默认锁屏壁纸: 将图片资源放入vendo

C#实战|大乐透选号器[6]:实现实时显示已选择的红蓝球数量

哈喽,你好啊,我是雷工。 关于大乐透选号器在前面已经记录了5篇笔记,这是第6篇; 接下来实现实时显示当前选中红球数量,蓝球数量; 以下为练习笔记。 01 效果演示 当选择和取消选择红球或蓝球时,在对应的位置显示实时已选择的红球、蓝球的数量; 02 标签名称 分别设置Label标签名称为:lblRedCount、lblBlueCount

浅谈主机加固,六种有效的主机加固方法

在数字化时代,数据的价值不言而喻,但随之而来的安全威胁也日益严峻。从勒索病毒到内部泄露,企业的数据安全面临着前所未有的挑战。为了应对这些挑战,一种全新的主机加固解决方案应运而生。 MCK主机加固解决方案,采用先进的安全容器中间件技术,构建起一套内核级的纵深立体防护体系。这一体系突破了传统安全防护的局限,即使在管理员权限被恶意利用的情况下,也能确保服务器的安全稳定运行。 普适主机加固措施:

webm怎么转换成mp4?这几种方法超多人在用!

webm怎么转换成mp4?WebM作为一种新兴的视频编码格式,近年来逐渐进入大众视野,其背后承载着诸多优势,但同时也伴随着不容忽视的局限性,首要挑战在于其兼容性边界,尽管WebM已广泛适应于众多网站与软件平台,但在特定应用环境或老旧设备上,其兼容难题依旧凸显,为用户体验带来不便,再者,WebM格式的非普适性也体现在编辑流程上,由于它并非行业内的通用标准,编辑过程中可能会遭遇格式不兼容的障碍,导致操

透彻!驯服大型语言模型(LLMs)的五种方法,及具体方法选择思路

引言 随着时间的发展,大型语言模型不再停留在演示阶段而是逐步面向生产系统的应用,随着人们期望的不断增加,目标也发生了巨大的变化。在短短的几个月的时间里,人们对大模型的认识已经从对其zero-shot能力感到惊讶,转变为考虑改进模型质量、提高模型可用性。 「大语言模型(LLMs)其实就是利用高容量的模型架构(例如Transformer)对海量的、多种多样的数据分布进行建模得到,它包含了大量的先验

Kubernetes PodSecurityPolicy:PSP能实现的5种主要安全策略

Kubernetes PodSecurityPolicy:PSP能实现的5种主要安全策略 1. 特权模式限制2. 宿主机资源隔离3. 用户和组管理4. 权限提升控制5. SELinux配置 💖The Begin💖点点关注,收藏不迷路💖 Kubernetes的PodSecurityPolicy(PSP)是一个关键的安全特性,它在Pod创建之前实施安全策略,确保P