在服务限流时个别会限度某个工夫周期内的申请数,简略点会采纳固定窗口算法(也称计数器算法),这种算法实现绝对简略,也很高效;但在理论的利用场景中申请并不是特地平均,某些状况下会产生一些刹时的突发流量,而后很快恢复正常,很多时候这并不会对系统产生破坏性的影响,然而固定窗口算法不能很好的解决这种状况。
比方某个数据查问接口限流每秒 100 次申请,绝大多数的工夫里都不会超过这个数,然而偶然某一秒钟会达到 120 次申请,接着很快又会恢复正常。此时如果采纳固定窗口算法会触发限流,用户的失常拜访会被烦扰,体验上不太好;如果接口的调用方还有重试的逻辑,则在后续的工夫窗口内零碎可能收到更多的申请,而后更多的申请被限流,又产生更多的重试申请,周而复始让零碎的累赘更加惨重,重大的话可能导致系统解体。
假如上文中 120 次的申请不会对系统稳定性带来实质性的影响,则能够在肯定水平上容许这种刹时的突发流量,从而为用户带来更好的应用体验,也可肯定水平上防止因为限流重试导致系统累赘进一步减轻的问题。本文就介绍一种令牌桶的算法来应答这个状况。
算法原理
说了这么多,那么令牌桶算法怎么解决问题的呢?请看下图:
如上图所示,该算法的基本原理是:有一个令牌桶,容量是 X,每 Y 单位工夫会向桶中放入 Z 个令牌,如果桶中的令牌数超过 X,则抛弃令牌;申请要想通过首先须要从令牌桶中获取一个令牌,获取不到令牌则拒绝请求。能够看出对于令牌桶算法 X、Y、Z 这几个数的设定特地重要,Z 应该略大于绝大数时候的 Y 单位工夫内的申请数,零碎会长期处于这个状态,X 能够是零碎容许承载的刹时最大申请数,零碎不能长时间处于这个状态。
算法实现
这里讲两种实现办法:过程内即内存令牌桶算法、基于 Redis 的令牌桶算法。
过程内即内存令牌桶算法
这里在申请时计算投放数量,没有独自的投放解决,比固定窗口算法麻烦一些,然而仔细阅读,也很容易了解。
应用字典,Key 是限流指标,Value 包含以后令牌桶令牌数和上次令牌投放工夫。初始状态下,认为每个限流指标的令牌桶是装满的,即令牌桶令牌数 = 令牌桶容量,不过仅在解决中发现限流指标的令牌桶不存在时才创立这个令牌桶。
申请进入后,依据限流指标在字典中查找:
- 如果找不到,则创立令牌桶,并设置令牌数为:令牌桶容量 - 本次申请耗费令牌数,设置上次令牌投放工夫为:以后工夫。
-
如果找到,则计算以后工夫与上次令牌投放工夫之间的距离:
- 如果大于等于令牌投放工夫距离,则计算令牌数为:max(令牌桶令牌数 + 令牌投放数量, 令牌桶容量)- 本次申请耗费令牌数,上次令牌投放工夫为:以后工夫。
- 如果小于令牌投放工夫距离,则计算令牌数为:令牌桶令牌数 - 本次申请耗费令牌数。‘
- 如果计算出的令牌数小于 0,则触发限流,否则更新到令牌桶中。
在 C# 语言中能够应用 MemoryCache,它的缓存项有一个过期工夫,能够主动回收一些很少应用或者不再应用的令牌桶,缩小内存占用。
过程内算法最适宜单实例解决的程序限流,多实例解决的状况下可能每个实例收到的申请数不平均,不能保障限流成果。
基于 Redis 的令牌桶算法
Redis 作为 KV 存储,相似于字典,而且也自带过期工夫。解决申请时,首先从申请中提取限流指标,而后依据限流指标去 Redis 中查找,其解决规定和内存算法一样,只不过应用了两个 Redis KV:
- 限流指标的令牌桶,Value 是以后令牌数。
- 限流指标的上次令牌投放工夫,Value 是上次投放令牌的工夫戳。
这些操作逻辑能够封装在一个 Lua script 中,因为 Lua script 在 Redis 中执行时也是原子操作,所以 Redis 的限流计数在分布式部署时人造就是精确的。
利用算法
这里以限流组件 FireflySoft.RateLimit 为例,实现 ASP.NET Core 中的令牌桶算法限流。
1、装置 Nuget 包
有多种装置形式,抉择本人喜爱的就行了。
包管理器命令:
Install-Package FireflySoft.RateLimit.AspNetCore
或者.NET 命令:
dotnet add package FireflySoft.RateLimit.AspNetCore
或者我的项目文件间接增加:
<ItemGroup>
<PackageReference Include="FireflySoft.RateLimit.AspNetCore" Version="2.*" />
</ItemGroup>
2、 应用中间件
在 Startup 中应用中间件,演示代码如下(下边会有具体阐明):
public void ConfigureServices(IServiceCollection services)
{
...
app.AddRateLimit(new InProcessTokenBucketAlgorithm(new[] {new TokenBucketRule(30,10,TimeSpan.FromSeconds(1))
{
ExtractTarget = context =>
{return (context as HttpContext).Request.Path.Value;
},
CheckRuleMatching = context =>
{return true;},
Name="default limit rule",
}
})
);
...
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
...
app.UseRateLimit();
...
}
如上须要先注册服务,而后应用中间件。
注册服务的时候须要提供限流算法和对应的规定:
- 这里应用过程内令牌桶算法,对于分布式服务能够应用 RedisTokenBucketAlgorithm,反对 StackExchange.Redis。
- 桶的容量是 30,每秒流入 10 个令牌。最大可能容许每秒 40 次申请,起码可能容许每秒 10 次申请,绝大部分状况下不应该超过每秒 10 次,能够偶然超过 10 次 / 秒,极少数状况下达到 40 次 / 秒。
- ExtractTarget 用于提取限流指标,这里是每个不同的申请 Path,能够依据需要从以后申请中提取要害数据,而后设定各种限流指标。如果有 IO 申请,这里还反对对应的异步办法 ExtractTargetAsync。
- CheckRuleMatching 用于验证以后申请是否限流,传入的对象也是以后申请,不便提取要害数据进行验证。如果有 IO 申请,这里还反对对应的异步办法 CheckRuleMatchingAsync。
- 默认被限流时会返回 HttpStatusCode 429,能够在 AddRateLimit 时应用可选参数 error 自定义这个值,以及 Http Header 和 Body 中的内容。
根本的应用就是上边例子中的这些了。
如果还是基于传统的.NET Framework,则须要在 Application_Start 中注册一个音讯处理器 RateLimitHandler,算法和规定局部都是共用的,具体能够看 Github 上的应用阐明:https://github.com/bosima/Fir…
FireflySoft.RateLimit 是一个基于 .NET Standard 的限流类库,其内核简略笨重,可能灵便应答各种需要的限流场景。
其次要特点包含:
- 多种限流算法:内置固定窗口、滑动窗口、漏桶、令牌桶四种算法,还可自定义扩大。
- 多种计数存储:目前反对内存、Redis 两种存储形式。
- 分布式敌对:通过 Redis 存储反对分布式程序对立计数。
- 限流指标灵便:能够从申请中提取各种数据用于设置限流指标。
- 反对限流惩办:能够在客户端触发限流后锁定一段时间不容许其拜访。
- 动静更改规定:反对程序运行时动静更改限流规定。
- 自定义谬误:能够自定义触发限流后的错误码和谬误音讯。
- 普适性:原则上能够满足任何须要限流的场景。
Github 开源地址:https://github.com/bosima/Fir…
播种更多架构常识,请关注公众号 萤火架构。原创内容,转载请注明出处。