一、限流算法
在高并发系统中,有三把利器用来保护系统:缓存、降级和限流。
本文主要是介绍限流,限流算法主要有以下三种:
1.计数器算法
固定窗口
滑动窗口
2.令牌桶算法
3.漏桶算法
1.计数器算法
1.1 固定窗口算法
计数器算法是限流算法里最简单也是最容易实现的一种算法。比如我们规定,对于A接口来说,我们1分钟的访问次数不能超过100个。那么我们可以这么做:在一开 始的时候,我们可以设置一个计数器counter,每当一个请求过来的时候,counter就加1,如果counter的值大于100并且该请求与第一个 请求的间隔时间还在1分钟之内,那么说明请求数过多;如果该请求与第一个请求的间隔时间大于1分钟,且counter的值还在限流范围内,那么就重置 counter。
java中的具体实现如下:
public class CounterTest { public long timeStamp = getNowTime(); public int reqCount = 0; public final int limit = 100; // 时间窗口内最大请求数 public final long interval = 1000; // 时间窗口ms public boolean grant() { long now = getNowTime(); if (now < timeStamp + interval) { // 在时间窗口内 reqCount++; // 判断当前时间窗口内是否超过最大请求控制数 return reqCount <= limit; } else { timeStamp = now; // 超时后重置 reqCount = 1; return true; } } public long getNowTime() { return System.currentTimeMillis(); } }
.NET Core中的具体实现如下:
AspNetCoreRateLimit是目前ASP.NET Core下最常用的限流解决方案,AspNetCoreRateLimit的源码实现是固定窗口算法如下:
var entry = await _counterStore.GetAsync(counterId, cancellationToken); if (entry.HasValue) { // entry has not expired if (entry.Value.Timestamp + rule.PeriodTimespan.Value >= DateTime.UtcNow) { // increment request count var totalCount = entry.Value.Count + _config.RateIncrementer?.Invoke() ?? 1; // deep copy counter = new RateLimitCounter { Timestamp = entry.Value.Timestamp, Count = totalCount }; } }
固定窗口算法缺点
从上图中我们可以看到,假设有一个恶意用户,他在0:59时,瞬间发送了100个请求,并且1:00又瞬间发送了100个请求,那么其实这个用户在 1秒里面,瞬间发送了200个请求。我们刚才规定的是1分钟最多100个请求,也就是每秒钟最多1.7个请求,用户通过在时间窗口的重置节点处突发请求, 可以瞬间超过我们的速率限制。用户有可能通过算法的这个漏洞,瞬间压垮我们的应用。
1.2 滑动窗口算法
滑动窗口类似于固定窗口算法,但它通过将前一个窗口中的加权计数添加到当前窗口中的计数来计算估计数,如果估计数超过计数限制,则请求将被阻止。
具体公式如下:
估计数 = 前一窗口计数 * (1 - 当前窗口经过时间 / 单位时间) + 当前窗口计数
窗口[00:00, 00:01)中有9个请求,窗口[00:01, 00:02)中有5个请求。对于01:15到达的请求,即窗口[00:01, 00:02)的25%位置,通过公式计算请求计数:9 x (1 - 25%) + 5 = 11.75 > 10. 因此我们拒绝此请求。
即使两个窗口都没有超过限制,请求也会被拒绝,因为前一个和当前窗口的加权和确实超过了限制。
2.令牌桶算法
令牌桶算法是比较常见的限流算法之一,大概描述如下:
1)所有的请求在处理之前都需要拿到一个可用的令牌才会被处理;
2)根据限流大小,设置按照一定的速率往桶里添加令牌;
3)桶设置最大的放置令牌限制,当桶满时、新添加的令牌就被丢弃或者拒绝;
4)请求达到后首先要获取令牌桶中的令牌,拿着令牌才可以进行其他的业务逻辑,处理完业务逻辑之后,将令牌直接删除;
5)令牌桶有最低限额,当桶中的令牌达到最低限额的时候,请求处理完之后将不会删除令牌,以此保证足够的限流;
3.漏桶算法
漏桶算法其实很简单,可以粗略的认为就是注水漏水过程,往桶中以一定速率流出水,以任意速率流入水,当水超过桶流量则丢弃,因为桶容量是不变的,保证了整体的速率。
二、ASP.NET Core中间件实现限流
1.中间件代码
public class SlidingWindow { private readonly object _syncObject = new object(); private readonly int _requestIntervalSeconds; private readonly int _requestLimit; private DateTime _windowStartTime; private int _prevRequestCount; private int _requestCount; public SlidingWindow(int requestLimit, int requestIntervalSeconds) { _windowStartTime = DateTime.Now; _requestLimit = requestLimit; _requestIntervalSeconds = requestIntervalSeconds; } public bool PassRequest() { lock (_syncObject) { var currentTime = DateTime.Now; var elapsedSeconds = (currentTime - _windowStartTime).TotalSeconds; if (elapsedSeconds >= _requestIntervalSeconds * 2) { _windowStartTime = currentTime; _prevRequestCount = 0; _requestCount = 0; elapsedSeconds = 0; } else if (elapsedSeconds >= _requestIntervalSeconds) { _windowStartTime = _windowStartTime.AddSeconds(_requestIntervalSeconds); _prevRequestCount = _requestCount; _requestCount = 0; elapsedSeconds = (currentTime - _windowStartTime).TotalSeconds; } var requestCount = _prevRequestCount * (1 - elapsedSeconds / _requestIntervalSeconds) + _requestCount + 1; if (requestCount <= _requestLimit) { _requestCount++; return true; } } return false; } }
如果最近的2次请求相距2个窗口时间,则可以认为前一窗口计数为0,重新开始计数。
public class RateLimitMiddleware : IMiddleware { private readonly SlidingWindow _window; public RateLimitMiddleware() { _window = new SlidingWindow(10, 60); } public async Task InvokeAsync(HttpContext context, RequestDelegate next) { if (!_window.PassRequest()) { context.SetEndpoint(new Endpoint((context) => { context.Response.StatusCode = StatusCodes.Status403Forbidden; return Task.CompletedTask; }, EndpointMetadataCollection.Empty, "限流")); } await next(context); } }
2.在管道中的使用
需要注意的是,我们注册Middleware时,必须使用单例模式,保证所有请求通过同一SlidingWindow计数:
services.AddSingleton<RateLimitMiddleware>();