Русский
Русский
English
Статистика
Реклама

Как перестать DDoS-ить чужой API и начать жить

Поговорим о способах ограничить число исходящих запросов враспределенном приложении. Это нужно, если внешний API непозволяет обращаться кнему тогда, когда вам вздумается.

Вводные

Для начала немного вводных. Есть наше приложение иесть некий внешний сервис. Например, какое-то банковское ПО, API для отслеживания почтовых отправлений, что угодно. При этом наше приложение непросто использует его, там куча очень важной для нас информации. Прибыль компании напрямую зависит отобъема выгруженных оттуда данных. Мыпонимаем, один сервер это слишком мало изаводим себе пару десятков машин. Чтобы приложение масштабировалось лучше, делаем так: разбиваем весь объем намаленькие задачи иотправляем ихвочередь. Каждый сервер извлекает ихоттуда поодной. Втаком сообщении указан, например, IDпользователя. Затем приложение скачивает данные для него исохраняет ихвбазе. Большая ибыстрая машина обработает много задач, маленькая имедленная поменьше.

Создатели внешнего сервиса понимают, что, если неограничивать наше взаимодействие, они начнут отказывать вобслуживании другим клиентам. Поэтому вводится лимит назапросы: теперь разрешено неболее 1000 одновременных обращений кним.

Немного подумав над условиями задачи находим решение семафор. Ондопускает, чтобы одновременно вкритической секции было не болееN потоков выполнения. Когда внутри скапливается предельное ихколичество новые желающие ожидают навходе. Кто-то выходит значит кто-то ещё может войти.

Один семафор на машину

Делим лимит запросов начисло доступных серверов (1000/20) иполучаем по50конкурентных обращений намашину.

Простой семафор в .NET
private const int RequestsLimit = 50;private static readonly SemaphoreSlim Throttler =   new SemaphoreSlim(RequestsLimit);async Task<HttpResponseMessage> InvokeServiceAsync(HttpClient client){try{await Throttler.WaitAsync().ConfigureAwait(false);return await client.GetAsync("todos/1").ConfigureAwait(false);}finally{Throttler.Release();}}

В .NET Core можно сделать типизированный HttpClient, получится очень вдухе новых веяний, янебуду останавливаться наэтом подробнее, новыможете посмотреть сюда. Там ивцелом такой подход раскрывается детальнее, чем яделаю это здесь.

Попробуем проанализировать то, что получилось.

Вэтой конфигурации будут возникать ситуации, когда одна машина свободна, адругие перегружены. Этого можно избежать, беря задачу вработу при условии, что наконкретном сервере число сообщений вобработке недостигло предела. Тогда возникнет другая проблема как только задача выполнится занять еёместо будет некому, сперва потребуется получить новое сообщение изочереди. Можно постоянно держать систему слегка перегруженной это приведет ктому, что некоторые задачи-неудачники долго будут висеть навходе вкритическую секцию. Вцелом, утакого подхода есть потенциал, сним можно достигнуть хорошей производительности снебольшим тюнингом ипри определенных условиях.

Подведем ему некий итог:

Плюсы:

  1. Простой код

  2. Ресурсы машины используются эффективно

Минусы:

  1. Не полностью утилизируется канал во внешний сервис

Один семафор на всех

Подумаем, вкакую сторону двинуться теперь. Попробуем сделать семафор общим ресурсом для всех серверов. Так тоже без проблем необойдется будет нужно тратить время перед каждым запросом вовнешнюю систему наобращение ксервису-throttler-у. Ноадминистрировать такое, наверное, проще засостоянием одного семафора легче следить, ненадо подбирать лимит под каждую машину вотдельности. Какже сделать общий троттлер? Ну, конечноже вRedis.

Сточки зрения пользователя Redis однопоточный (онтак выглядит). Это круто, большая часть проблем сконкурентным доступом кнему сразу снимается.

ВRedis нет готового семафора, номожно построить его насортированных множествах.

Писать код для него будем наLua. Интерпретатор Lua встроен вRedis, такие скрипты выполняются одним махом, атомарно. Мымоглибы без этого обойтись, нотак код получится ещё сложнее, придется учитывать конкуренцию между серверами заотдельные команды.

Теперь подумаем, что писать. Учтём, что машина, взявшая блокировку, может никогда еёнеотпустить. Вдруг кто-то решит выключить рубильник или что-то пойдет непоплану вмомент обновления нашего приложения. Учтём также, что унас может быть несколько таких семафоров. Это пригодится, например, для разных внешних сервисов, API-ключей или если мызахотим выделить отдельный канал под приоритетные запросы.

Скрипт для Redis
--[[  KEYS[1] - Имя семафора ARGV[1] - Время жизни блокировки ARGV[2] - Идентификатор блокировки, чтобы её можно было возвратить ARGV[3] - Доступный объем семафора ]]--   -- Будем использовать команды с недетерминированным результатом,  -- Redis-у важно знать заранее redis.replicate_commands()local unix_time = redis.call('TIME')[1]   -- Удаляем блокировки с истёкшим TTL redis.call('ZREMRANGEBYSCORE', KEYS[1], '-inf', unix_time - ARGV[1])   -- Получаем число элементов в множестве local count = redis.call('zcard', KEYS[1])   if count < tonumber(ARGV[3]) then-- добавляем блокировку в множество, если есть место  -- время будет являться ключем сортировки (для последующий чистки записей) redis.call('ZADD', KEYS[1], unix_time, ARGV[2])       -- Возвращаем число взятых блокировок (например, для логирования)    return count end   return nil

Чтобы освободить блокировку будем удалять элемент изсортированного множества. Это одна команда. Обойдемся без отдельного скрипта, многие клиенты вт.ч.идля .NET это умеют.

Подробнее о вариантах реализации блокировок с Redis и семафоров в частности можно посмотреть здесь.

Иногда внешний сервис ограничивает число обращений другим образом, например, разрешает делать неболее 1000 запросов вминуту. Вэтом случае вRedis можно завести счётчик сфиксированным временем жизни.

Вкоде нашего приложения сделаем оболочку вокруг написанного скрипта, некий класс, экземпляр которого заменит семафор изпервого варианта.

Код для приложения
public sealed class RedisSemaphore{private static readonly string AcquireScript = "...";private static readonly int TimeToLiveInSeconds = 300;private readonly Func<ConnectionMultiplexer> _redisFactory;public RedisSemaphore(Func<ConnectionMultiplexer> redisFactory){_redisFactory = redisFactory;}public async Task<LockHandler> AcquireAsync(string name, int limit){var handler = new LockHandler(this, name);do{var redisDb = _redisFactory().GetDatabase();var rawResult = await redisDb.ScriptEvaluateAsync(AcquireScript, new RedisKey[] { name },new RedisValue[] { TimeToLiveInSeconds, handler.Id, limit }).ConfigureAwait(false);var acquired = !rawResult.IsNull;if (acquired)break;await Task.Delay(10).ConfigureAwait(false);} while (true);return handler;}public async Task ReleaseAsync(LockHandler handler, string name){var redis = _redisFactory().GetDatabase();await redis.SortedSetRemoveAsync(name, handler.Id).ConfigureAwait(false);}}public sealed class LockHandler : IAsyncDisposable{private readonly RedisSemaphore _semaphore;private readonly string _name;public LockHandler(RedisSemaphore semaphore, string name){_semaphore = semaphore;_name = name;Id = Guid.NewGuid().ToString();}public string Id { get; }public async ValueTask DisposeAsync(){await _semaphore.ReleaseAsync(this, _name).ConfigureAwait(false);}}

Посмотрим, что получилось.

Плюсы:

  1. Просто конфигурировать лимит

  2. Канал используется эффективно

  3. Легко наблюдать за утилизацией канала

Минусы:

  1. Дополнительный элемент инфраструктуры

  2. Ещё одна точка отказа

  3. Накладные расходы на обращение к Redis-у

  4. Нетривиальный код

Если выиспользуете Redis впроекте, топочемубы несделать нанём иблокировки. Тащитьже его ксебе только ради этого уже нетак весело. Что-то подобное можно реализовать, используя базу данных, носкорее всего это добавит немало хлопот инакладных расходов. Второй минус лично мне не кажется очень существенным. Что ни говори, а отказывает Redis обычно не так уж часто, особенно, если это SaaS.

Легкость настройки такой блокировки это существенный плюс. Не надо все перенастраивать при каждом масштабировании, подбирать лимиты под машины. Можно следить за утилизацией канала в графане, это удобно.

Думаю, можно реализовать троттлинг исходящих запросов инауровне инфраструктуры, номне кажется удобным иметь вкоде контроль над состоянием блокировки. Кроме того, настроить это в чужих облаках наверняка будет непросто. Авам приходилось когда-нибудь ограничивать число исходящих запросов? Как делаете этовы?

Источник: habr.com
К списку статей
Опубликовано: 25.04.2021 12:10:20
0

Сейчас читают

Комментариев (0)
Имя
Электронная почта

Программирование

Net

C

Redis

Semaphore

Throttling

Net core

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru