在服务限流时一般会限制某个时间周期内的请求数,简单点会采用固定窗口算法(也称计数器算法),这种算法实现相对简单,也很高效;但在实际的应用场景中请求并不是特别均匀,某些情况下会产生一些瞬时的突发流量,然后很快恢复正常,很多时候这并不会对系统产生破坏性的影响,但是固定窗口算法不能很好的处理这种情况。
在服务限流时一般会限制某个时间周期内的请求数,简单点会采用固定窗口算法(也称计数器算法),这种算法实现相对简单,也很高效;但在实际的应用场景中请求并不是特别均匀,某些情况下会产生一些瞬时的突发流量,然后很快恢复正常,很多时候这并不会对系统产生破坏性的影响,但是固定窗口算法不能很好的处理这种情况。
假设上文中次的请求不会对系统稳定性带来实质性的影响,则可以在一定程度上允许这种瞬时的突发流量,从而为用户带来更好的使用体验,也可一定程度上避免因为限流重试导致系统负担进一步加重的问题。本文就介绍一种令牌桶的算法来应对这个情况。
算法原理
说了这么多,那么令牌桶算法怎么解决问题的呢?请看下图:
如上图所示,该算法的基本原理是:有一个令牌桶,容量是X,每Y单位时间会向桶中放入Z个令牌,如果桶中的令牌数超过X,则丢弃令牌;请求要想通过首先需要从令牌桶中获取一个令牌,获取不到令牌则拒绝请求。可以看出对于令牌桶算法X、Y、Z这几个数的设定特别重要,Z应该略大于绝大数时候的Y单位时间内的请求数,系统会长期处于这个状态,X可以是系统允许承载的瞬时最大请求数,系统不能长时间处于这个状态。
算法实现
这里讲两种实现方法:进程内即内存令牌桶算法、基于Redis的令牌桶算法。
进程内即内存令牌桶算法
这里在请求时计算投放数量,没有单独的投放处理,比固定窗口算法麻烦一些,但是仔细阅读,也很容易理解。
使用字典,Key是限流目标,Value包括当前令牌桶令牌数和上次令牌投放时间。初始状态下,认为每个限流目标的令牌桶是装满的,即令牌桶令牌数=令牌桶容量,不过仅在处理中发现限流目标的令牌桶不存在时才创建这个令牌桶。
请求进入后,根据限流目标在字典中查找:
如果找不到,则创建令牌桶,并设置令牌数为:令牌桶容量-本次请求消耗令牌数,设置上次令牌投放时间为:当前时间。
如果找到,则计算当前时间与上次令牌投放时间之间的间隔:
如果大于等于令牌投放时间间隔,则计算令牌数为:max(令牌桶令牌数+令牌投放数量,令牌桶容量)-本次请求消耗令牌数,上次令牌投放时间为:当前时间。
如果小于令牌投放时间间隔,则计算令牌数为:令牌桶令牌数-本次请求消耗令牌数。‘
如果计算出的令牌数小于0,则触发限流,否则更新到令牌桶中。
在C#语言中可以使用MemoryCache,它的缓存项有一个过期时间,可以自动回收一些很少使用或者不再使用的令牌桶,减少内存占用。
进程内算法最适合单实例处理的程序限流,多实例处理的情况下可能每个实例收到的请求数不均匀,不能保证限流效果。
基于Redis的令牌桶算法
Redis作为KV存储,类似于字典,而且也自带过期时间。处理请求时,首先从请求中提取限流目标,然后根据限流目标去Redis中查找,其处理规则和内存算法一样,只不过使用了两个RedisKV:
限流目标的令牌桶,Value是当前令牌数。
限流目标的上次令牌投放时间,Value是上次投放令牌的时间戳。
这些操作逻辑可以封装在一个Luascript中,因为Luascript在Redis中执行时也是原子操作,所以Redis的限流计数在分布式部署时天然就是准确的。
应用算法
这里以限流组件FireflySoft.RateLimit为例,实现ASP.NETCore中的令牌桶算法限流。
1、安装Nuget包
有多种安装方式,选择自己喜欢的就行了。
包管理器命令:
```shell
Install-PackageFireflySoft.RateLimit.AspNetCore
```
或者.NET命令:
```shell
dotnetaddpackageFireflySoft.RateLimit.AspNetCore
```
或者项目文件直接添加:
```xml
ItemGroup
PackageReferenceInclude="FireflySoft.RateLimit.AspNetCore"Version="2.*"/
/ItemGroup
```
2、使用中间件
在Startup中使用中间件,演示代码如下(下边会有详细说明):
```c#
publicvoidConfigureServices(IServiceCollectionservices)
{
...
app.AddRateLimit(newInProcessTokenBucketAlgorithm(
new[]{
newTokenBucketRule(30,10,TimeSpan.FromSeconds(1))
{
ExtractTarget=context=
{
return(contextasHttpContext).Request.Path.Value;
},
CheckRuleMatching=context=
{
returntrue;
},
Name="defaultlimitrule",
}
})
);
...
}
publicvoidConfigure(IApplicationBuilderapp,IWebHostEnvironmentenv)
{
...
app.UseRateLimit();
...
}
```
如上需要先注册服务,然后使用中间件。
注册服务的时候需要提供限流算法和对应的规则:
这里使用进程内令牌桶算法,对于分布式服务可以使用RedisTokenBucketAlgorithm,支持StackExchange.Redis。
桶的容量是30,每秒流入10个令牌。最大能够允许每秒40次请求,最少能够允许每秒10次请求,绝大部分情况下不应该超过每秒10次,可以偶尔超过10次/秒,极少数情况下达到40次/秒。
ExtractTarget用于提取限流目标,这里是每个不同的请求Path,可以根据需求从当前请求中提取关键数据,然后设定各种限流目标。如果有IO请求,这里还支持对应的异步方法ExtractTargetAsync。
CheckRuleMatching用于验证当前请求是否限流,传入的对象也是当前请求,方便提取关键数据进行验证。如果有IO请求,这里还支持对应的异步方法CheckRuleMatchingAsync。
默认被限流时会返回HttpStatusCode,可以在AddRateLimit时使用可选参数error自定义这个值,以及HttpHeader和Body中的内容。
基本的使用就是上边例子中的这些了。
如果还是基于传统的.NETFramework,则需要在Application_Start中注册一个消息处理器RateLimitHandler,算法和规则部分都是共用的,具体可以看Github上的使用说明: