ЗАЩИТА ОТ DDoS при помощи NGINX. Горячий кеш при помощи LUA.

Для опытных пользователей и системных администраторов не лишним будет знать, что большинство промышленных решений по защите от DDoS атак создается на базе кластеров и использовании на них NGINX web сервера. NGINX является одним из лучших кеширующих прокси серверов, умеющих обрабатывать колосальные обьемы данных. И одной из самых важных функций этого веб-сервера является его уникальные возможности по кешированию контента и его быстрой отдачи посетителям сайтов. Мы продолжим тему использования NGINX для защиты от ддос атак, а в текущей статье более подробно остановимся на создании кеша.

Защита от DDoS при помощи NGINX

В этом материале nginx используется как «горячий кеш» неких постоянно пополняемых данных, запрашиваемых клиентами по интервалу с опциональным группированием (некий аналог BETWEEN и GROUP BY/AGGREGATE из SQL). Подгрузка данных в кеш осуществляется самим же lua+nginx из Redis. Исходные данные в Redis складываются ежесекундно, а клиенты хотят их от сих до сих (интервал в секундах, минутах, часах…) с агрегацией по N (1<=N<=3600) секунд, отсортированные по дате и в json формате.

С хорошим hitrate на имеющейся машине получается обеспечить 110-130к «хотелок» в секунду, правда с плохим — только 20-30к. Что, в общем-то, тоже приемлемо для нас на одной инстанции nginx.

Из некоего источника ежесекундно приходят данные, которые складываются в Redis ZSET. Важным моментом является привязка данных именно ко времени — выборка будет идти по временным интервалам. Пришел один клиент — «дай мне от сих до сих посекундно», пришел другой — «а мне вот этот интервальчик, но давай с часовой агрегацией», третьему понадобилась одна последняя секунда, четвертому за сутки с аггрегацией по 27 секунд, ну и т.д… Стучаться за данными непосредственно в Redis нереально. Заранее кешировать подготовленные данные весьма проблематично, т.к. требуемые интервалы и шаг агрегации в общем случае у каждого клиента/запроса свой и могут произвольно варьироваться. Сервер должен быть готов быстро ответить на любой разумный запрос.

Пример работы кеша NGINX в облачном сервисе AMAZON

Первоначально была идея выполнять агрегацию на стороне Redis, вызывая через EVAL redis-lua код из nginx-lua кода. Данная «технология We need to go deeper» не подошла из-за однопоточной природы самого Redis: по быстрому отдать «сырые данные» выходит значительно быстрее, чем сгруппировать и выпихнуть готовый результат.

Данные в Redis хранятся поэлементно уже в json формате вида:

Ключом является timestamp, в dt строковый эквивалент по версии «наполняльщика».
Соответственно, выборка диапазона:

И на lua через resty Redis:

Про пул коннектов в resty Redis

Важно, что Resty использует настраиваемый пул коннектов к Redis и R:connect() в общем случае не создает новое соединение. Возврат соединения после использования НЕ выполняется автоматически, его нужно выполнить вызовом R:set_keepalive(), возвращающим соединение обратно в пул (после возврата использовать его без повторного R:connect() уже нельзя). Счетчик доставаний текущего коннекта из пула можно узнать через R:get_reused_times(). Если >0 — значит это уже ранее созданное и настроенное соединение. В таком случае не нужно повторно слать AUTH и т.п.

Собираем nginx (lua-nginx-module + lua-resty-redis), бегло настраиваем:

Про работу с shared dict

В конфиге указывается shared dict «ourmegacache», который будет доступен в lua как таблица (словарь, хеш). Данная таблица одна для всех worker процессов nginx и операции на ней атомарны для нас.
Доступ к таблице прост:

При исчерпании свободного места в памяти, начинается чистка по методу LRU, что в нашем случае подходит. Кому не подходит — смотрите в сторону методов safe_add, flush_expired, и т.п. Так же стоит учитывать еще, вроде как, не решенный официально баг в nginx, связанный с хранением больших элементов в данном shared dict.

Для разнообразия границы запрашиваемого интервала и шаг агрегации будем получать из GET параметров запроса from, to и step. С данным соглашением примерный формат запроса к сервису будет таким:

/data.js?step=300&from=1386700653&to=1386701764

Итак, у нас есть поэлементные json записи, хранящиеся в Redis, которые мы можем оттуда получать. Как их лучше кешировать и отдавать клиентам?

  • Можно хранить посекундные записи в таблице по отдельности. Однако, как показала практика, выполнение уже нескольких десятков запросов к таблице крайне негативно сказывается на производительности. А если придет запрос на сутки, то ответа с небольшим таймаутом можно и не дождаться;
  • Записи можно хранить блоками, объединяя через некий общий разделитель или сериализуя их хоть в тот же json. А при запросе нужно разбербанивать по разделителю или десериализовывать. Так себе вариант;
  • Хранить данные иерархически, с частичными повторами на разных уровнях аггрегации. Используются блоки кеша разного размера: 1 секунда (одиночная запись), 10 секунд, 1 минута, 10 минут, час. В каждом блоке содержатся данные всех его секунд. Самое важное, что содержимое блока никак не меняется и не отдается кусками: или целиком как есть или никак.

Выбран последний вариант, потребляющий больше памяти, но значительно уменьшающий число обращений к таблице. Используются блоки кеша разного размера: 1 секунда (одиночная запись), 10 секунд, 1 минута, 10 минут, час. В каждом блоке содержатся данные всех его секунд. Каждый блок выровнен на границу своего интервала, например первый элемент 10 секундного интервала всегда имеет timestamp, имеющий десятичный остаток 9 (сортировка по убыванию, как хотят клиенты), а часовой блок содержит элементы 59:59, 59:58,… 00:00. При объединении элементов, они сразу склеиваются с разделителем — запятой, что позволяет отдавать данные блоки клиенту одним действием: ‘[‘, block, ‘]’, а также быстро объединять их в более крупные куски.

Для покрытия запрошенного интервала выполняется разбиение на максимально возможные блоки с достройкой по краям более мелкими блоками. Т.к. у нас есть единичные блоки, то всегда возможно полное покрытие требуемого интервала. Для запроса интервала 02:29:58… 03:11:02 получаем раскладку по кешам:

Это лишь пример. Реальные вычисления выполняют на timestamp’ах.

Выходит, что нужны 8 запросов к локальному кешу. Или к Redis, если локально их уже/еще нет. А чтобы не ломиться за одинаковыми данными из разных worker’ов/connect’ов, можно использовать атомарность операций с shared dict для реализации блокировок (где key — строковый ключ кеша, содержащий в себе сведения о интервале и шаге агрегации):

Имея нужную раскладку по кешам, возможность выбора нужного диапазона из Redis, и логику агрегации (тут очень специфично, не привожу примера), получаем отличный кеширующий сервер, который, после прогрева, стучится в Redis только раз в секунду за новым элементом + за старыми, если они еще не выбирались или были выброшены по LRU. И не забываем про ограниченный пул коннектов в Redis.

Защита от DDoS при помощи NGINX

В нашем случае прогрев выглядит как кратковременный скачок входящего трафика порядка 100-110Мб/сек на несколько секунд. По cpu на машине с nginx прогрева вообще почти не заметно. Таким образом, при использовании данного решения на продакшн сервере, вы можете получить простую защиту от ddos при помощи данного кеширования. При увеличении количества количества серверов, можно увеличить мощность кластера и добиться намного большей производительности.

Рейтинг материала
[Голосов: 1 Рейтинг: 5]
13 марта 2017, 09:17

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *