Это официально — Microsoft выпустила новый .NET Rate Limiter в .NET 7! Это потрясающая вещь, и она позволит избежать любых проблем, с которыми вы сталкивались при ограничении скорости в прошлом! Ознакомьтесь с этим сообщением в блоге, чтобы узнать о нем больше!
Что касается производительности, то новый ограничитель скорости .NET был разработан для обработки тысяч одновременных запросов с минимальными накладными расходами, что делает его идеальным для сайтов с высокой посещаемостью и миллионами активных пользователей.
Прежде всего, прежде чем мы начнем говорить об удивительных возможностях, которые ограничение скорости Microsoft привносит в .NET 7, нам нужно понять, что такое ограничение скорости, для чего оно нужно и как оно работает.
Что такое ограничение скорости?
Прежде чем мы создадим наш проект (API .NET, приложение Blazor или_что_там_еще…), мы должны убедиться, что у нас нет никаких проблем с безопасностью, а дросселирование приложения уже давно работает. Поэтому концепция ограничения скорости не нова. Ограничение скорости — это (как следует из названия) ограничение или лимитирование доступа к определенным ресурсам в определенное время путем установления максимальной скорости доступа.
Чтобы объяснить эту концепцию на практике, представьте, что у вас есть приложение, которое подключается к базе данных через API. Предположим, что эта база данных и API не размещены на квантовом суперсервере, и мы знаем, что он может обрабатывать около 100 000 запросов ежедневно. В этот момент многие разработчики зададутся такими вопросами, как:
- Смогут ли API и база данных обрабатывать более 100 000 запросов ежедневно?
- Что произойдет, если количество запросов превысит это значение?
- Если он больше не сможет обрабатывать запросы, не упадет ли он?
Все эти вопросы заставляют усомниться в том, что база данных и API способны обрабатывать большее количество запросов.
Именно поэтому была создана концепция ограничения скорости. Ограничитель скорости (использующий различные типы алгоритмов) будет принимать, в данном случае, 100 000 запросов в день и отклонять или блокировать любой запрос, превышающий это число. Таким образом, мы можем гарантировать, что сервис никогда не будет перегружен.
Если ограничение скорости не реализовано в наших разработках, мы создаем хорошие ворота для любого злоумышленника, который хочет нанести вред нашему приложению или сервису (или даже непреднамеренно, чрезмерными запросами!).
Реализация ограничения скорости в службах .NET и контроль максимального количества запросов предотвратит возможное насыщение API и баз данных (скорость ограничения API), но это не единственная проблема, которую она решает.
Зачем нам нужно и почему мы используем Rate Limiting?
Как я уже говорил, проблема насыщения сервиса или функциональности может быть вызвана не только непроизвольным действием. Она также может быть намеренно задумана и выполнена хакером или злоумышленником, вызывая DoS-атаку (отказ в обслуживании).
Если мы предположим случай, когда у нас есть публичный API, к которому одновременно могут обращаться множество клиентов. Даже если большинство из них являются легитимными пользователями, наличие API или публичного сервиса открывает возможность для атак извне.
Это зависит от требований, которые могут предъявляться к API, поскольку дросселирование может затрагивать конкретную конечную точку или все конечные точки.
Использование ограничения скорости API также поможет защитить нас от возможных Dos-атак со стороны злоумышленников или даже бот-атак, которые представляют собой несколько зараженных компьютеров, контролируемых злоумышленником (ботнеты).
Эти средства управления могут ограничивать по IP-адресу (обычно), по пользователю или по любому идентификатору.
Другое (более коммерческое) использование ограничения скорости — это предложение платы или подписки за использование услуги или API (так называемая оплата по мере использования). Таким образом, многие компании IaaS и облачные провайдеры, предлагающие публичные API, гарантируют, что их пользователи и клиенты не используют услугу больше, чем они заплатили.
Теперь, когда концепция Rate Limiting ясна, давайте посмотрим и объясним новый Rate Limiter, который Microsoft выпустила в .NET 7.
Ограничение скорости в .NET
Прежде всего, давайте вспомним, что у нас есть пакет AspNetCoreRateLimit nuget для приложения MVC или веб-API, и он также есть на Github у Стефана Продана. Этот новый ограничитель скорости интегрирован в .NET 7 и, согласно Microsoft, он поможет нам поддерживать трафик нашего приложения на безопасном уровне и предотвратит перегрузку приложения:
«Ограничение скорости предоставляет способ защиты ресурса, чтобы избежать перегрузки вашего приложения и поддерживать трафик на безопасном уровне».
На самом деле существует множество комбинаций алгоритмов и различных способов контролировать поток всех запросов, которые может иметь приложение .NET, и Microsoft решила представить 4 основных алгоритма для приложений .NET, предусмотренных в .NET 7.
Ограничитель параллелизма
Ограничитель параллелизма — это самое «простое» решение для ограничения скорости. Этот ограничитель, представленный Microsoft, отвечает за ограничение максимального количества одновременных запросов. Указав предельное число, ограничитель будет отклонять следующий запрос, поскольку он превысил максимальное количество разрешенных запросов.
Представьте, что вы установили ограничение на 50 одновременных запросов, что означает, что одновременно будет разрешено только 50 запросов.
Если по какой-либо причине будет сгенерирован 51-й запрос, он будет отклонен за превышение указанного лимита.
Используя RateLimitLease с классом RateLimiter
, мы можем увеличивать количество разрешенных запросов каждый раз, когда запрос завершен. Давайте проверим этот пример кода:
public abstract class RateLimiter : IAsyncDisposable, IDisposable
{
public abstract int GetAvailablePermits();
public abstract TimeSpan? IdleDuration { get; }
public RateLimitLease Acquire(int permitCount = 1);
public ValueTask<RateLimitLease> WaitAsync(int permitCount = 1, CancellationToken cancellationToken = default);
public void Dispose();
public ValueTask DisposeAsync();
}
RateLimitLease является частью [System.Threading.RateLimiting](https://www.nuget.org/packages/System.Threading.RateLimiting)
, нового пакета nuget, который предоставляет встроенные «примитивы» и алгоритмы для создания и настройки ограничителей скорости и включен в .NET 7.
Ограничитель Token bucket
Token bucket — это второй алгоритм, выпущенный компанией Microsoft. Этот алгоритм ограничивает количество запросов на основе определенного количества разрешенных запросов. Как говорит Microsoft, его название описывает принцип его работы (немного неточно), но да, давайте разберем его на простом примере.
Предположим, у нас есть приложение, и у этого приложения есть воображаемое ведро. Это ведро имеет лимит токенов (запросов к приложению), и в него может поместиться, например, только 50 токенов.
Если пользователь приходит и делает запрос (1 токен), он будет израсходован из ведра, и останется 49 токенов.
Теперь представим, что приходит злоумышленник с ботнетом и генерирует 100 токенов (100 запросов). Поскольку до этого оставалось 49 свободных токенов, только первые 49 из 100 запросов, сделанных атакующим, будут обработаны, а остальные 51 запрос будут отклонены.
В данном сценарии ни один запрос не будет обработан, пока в ведре не появится свободный токен. Этот процесс происходит каждую минуту.
Фиксированное ограничение окна
Третий алгоритм, выпущенный Microsoft в новом .NET ограничителе скорости, — это ограничение с фиксированным окном. Этот алгоритм в чем-то схож с ограничением «ведра токенов», но у него есть и свои отличия.
Fixed window limit отвечает за разрешение определенного количества запросов через фиксированное окно за определенное время.
Для более практического объяснения я воспользуюсь примером Microsoft.
Представим, что перед нами кинотеатр на 50 мест (максимальная вместимость — 50). Теперь в этом кинотеатре демонстрируется фильм «Форсаж 9», который длится 2 часа 15 минут.
50 человек заходят и смотрят фильм, но в то же время еще 50 человек могут встать в очередь на следующий сеанс.
По истечении 2 ч 15 м первого сеанса 50 человек, смотревших фильм, уходят, оставляя 50 мест свободными для следующих 50 человек, и так далее.
Допустим, в данном примере это происходит циклами по 50 человек за раз.
Ограничение по скользящему окну
Четвертым алгоритмом ограничения скорости в .NET является скользящее окно. Ограничение скользящего окна похоже на ограничение фиксированного окна, но имеет добавление делений (также известных как сегменты).
Давайте разберемся в этом на примере, снова предоставленном Microsoft.
Представим, что у нас есть окно длительностью 2 часа, это окно разделено на 2 сегмента по 1 часу каждый и может принимать максимум 40 запросов одновременно. Теперь у нас также есть индекс, который будет указывать на текущий сегмент окна (самый последний или новый).
Теперь в течение первого часа мы получаем 20 запросов. Эти 20 запросов будут направлены непосредственно на текущий сегмент, на который указывает индекс.
По истечении первого часа индекс окна перейдет к следующему сегменту с общей емкостью 20 запросов (поскольку остальные 20 заняты в предыдущем сегменте).
Во второй час поступает 10 новых запросов, которые возвращаются в сегмент, на который указывает индекс, и количество доступных запросов уменьшается до 10 запросов.
Опять же, когда пройдет этот второй час, индекс окна переместится на следующую позицию, и, поскольку первые 20 запросов, поступившие в первый час, остались вне окна, эти 20 запросов восстанавливаются, оставляя в общей сложности 30 свободных запросов на данный момент.
Объяснив это, мы также имеем пару абстракций внутри nuget-пакета System.Threading.RateLimiting, таких как PartitionedRateLimiter
или RateLimiting middleware
, и давайте объясним их, чтобы понять их и быть в состоянии переместить наши .NET приложения и API от throttle.
PartitionedRateLimiter
Как я уже говорил минуту назад, PartitionedRateLimiter
принадлежит популярному nuget-пакету System.Threading.RateLimiting
. В некоторых аспектах он похож на RateLimiter
, о котором я рассказывал выше, но главное отличие в том, что PartitionedRateLimiter
позволяет иметь аргументы для методов на экземплярах TResource.
Как объясняет Microsoft, Acquire становится:
Acquire(TResource resourceID, int permitCount = 1)
Давайте посмотрим, как Microsoft приводит пример:
enum MyPolicyEnum
{
One,
Two,
Admin,
Default
}
PartitionedRateLimiter<string> limiter = PartitionedRateLimiter.Create<string, MyPolicyEnum>(resource =>
{
if (resource == "Policy1")
{
return RateLimitPartition.Create(MyPolicyEnum.One, key => new MyCustomLimiter());
}
else if (resource == "Policy2")
{
return RateLimitPartition.CreateConcurrencyLimiter(MyPolicyEnum.Two, key =>
new ConcurrencyLimiterOptions(permitLimit: 2, queueProcessingOrder: QueueProcessingOrder.OldestFirst, queueLimit: 2));
}
else if (resource == "Admin")
{
return RateLimitPartition.CreateNoLimiter(MyPolicyEnum.Admin);
}
else
{
return RateLimitPartition.CreateTokenBucketLimiter(MyPolicyEnum.Default, key =>
new TokenBucketRateLimiterOptions(tokenLimit: 5, queueProcessingOrder: QueueProcessingOrder.OldestFirst,
queueLimit: 1, replenishmentPeriod: TimeSpan.FromSeconds(5), tokensPerPeriod: 1, autoReplenishment: true));
}
});
RateLimitLease lease = limiter.Acquire(resourceID: "Policy1", permitCount: 1);
// ...
RateLimitLease lease = limiter.Acquire(resourceID: "Policy2", permitCount: 1);
// ...
RateLimitLease lease = limiter.Acquire(resourceID: "Admin", permitCount: 12345678);
// ...
RateLimitLease lease = limiter.Acquire(resourceID: "other value", permitCount: 1);
Если вы хотите узнать больше, ознакомьтесь с первоисточником: Microsoft PartitionedRateLimiter.
Промежуточное программное обеспечение RateLimiting
Промежуточное ПО RateLimiting также является частью System.Threading.RateLimiting
и его основной функцией является возможность конфигурирования и добавления пользовательских политик ограничения скорости к конечным точкам.
Возвращаясь к примерам Microsoft, в данном случае мы можем обнаружить следующее:
Func<HttpContext, RateLimitPartition<TPartitionKey>>
Имеет ту же функциональность, что и PartionedRateLimiter
.
Он также имеет опцию RateLimiterOptions
, которая включает RejectionStatusCode
, с помощью которой мы можем вернуть код статуса (по умолчанию 503).
new RateLimiterOptions()
{
OnRejected = (context, cancellationToken) =>
{
context.HttpContext.StatusCode = StatusCodes.Status429TooManyRequests;
return new ValueTask();
}
};
Если вы хотите узнать больше, пожалуйста, ознакомьтесь с первоисточником: Microsoft RateLimiting middleware.
Надеюсь, эти объяснения прояснили многие сомнения по поводу ограничения скорости .NET. Я хотел бы узнать, собирается ли кто-нибудь из разработчиков внедрить или уже внедрил ограничитель скорости .NET для ограничения скорости в API.