ASP.NET Core realizes current limiting control based on sliding window algorithm

Posted by mikegotnaild on Thu, 03 Mar 2022 12:19:45 +0100

catalogue

preface

Fixed window algorithm

Sliding window algorithm

realization

use

conclusion

 

preface

In the actual project, in order to ensure the stable operation of the server, it is necessary to limit the access frequency of the interface to avoid excessive pressure on the server due to frequent requests from the client.

AspNetCoreRateLimit[1] is currently ASP Net core is the most commonly used current limiting solution.

Looking at its implementation code, I found that it uses the fixed window algorithm.

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
        };
    }
}

Fixed window algorithm

The fixed window algorithm divides the timeline into fixed size windows and assigns a counter to each window. Each request is mapped to a window according to its arrival time. If the counter in the window has reached the limit, the request falling in this window is rejected.

For example, if we set the window size to 1 minute, 10 requests are allowed per minute:

59 second requests will be blocked because 10 requests have been accepted. The counter is reset to zero at 1 minute, so the request of 1 minute and 01 second is acceptable.

The main problem of the fixed window algorithm is that if a large number of requests occur at the edge of the window, the current limiting strategy will fail.

For example, nine requests are received in 59 seconds, and another 10 requests can be received in 1 minute and 01 seconds, which is equivalent to allowing 20 requests per minute.

Sliding window algorithm

Sliding window is similar to the fixed window algorithm, but it calculates the estimate by adding the weighted count in the previous window to the count in the current window. If the estimate exceeds the count limit, the request will be blocked.

The specific formula is as follows:

Estimate = Previous window count * (1 - Elapsed time of current window / unit time ) + Current window count

For example, suppose the limit is 10 per minute:

There are 9 requests in window [00:00, 00:01) and 5 requests in window [00:01, 00:02). For requests arriving at 01:15, that is, 25% of the position of window [00:01, 00:02), the request count is calculated by the formula: 9 x (1 - 25%) + 5 = 11.75 > 10. Therefore, we reject this request.

Even if neither window exceeds the limit, the request will be rejected because the weighted sum of the previous and Current Windows does exceed the limit.

realization

According to the above formula, the algorithm code of sliding window is as follows:

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;
    }
}

If the last 2 requests are 2 windows apart, the previous window count can be considered as 0 and the count can be restarted.

use

Create a new Middleware and use the sliding window algorithm to limit the current:

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,
                        "Current limiting"));
        }

        await next(context);
    }
}

It should be noted that when registering Middleware, we must use the singleton mode to ensure that all requests are counted through the same SlidingWindow:

services.AddSingleton<RateLimitMiddleware>();

conclusion

Using the sliding window algorithm can effectively avoid the problem that a large number of requests at the window edge can not be limited in the fixed window algorithm.

Topics: ASP.NET