Q: nginx: rate limit, postgresql?

Вот так вот выглядит статистика веб-антиспама за последние месяцы на libraw.org:

На rawdigger.com еще показательнее:

И это только голова. В том смысле, что это - заблокированное Mollom, а там еще перед ним тоже есть слои антиспама.

Доходит до того, что старый сервер, на котором этот бложек и еще пяток сайтов, еле шевелится (кроме спама - там еще попытки регистрации юзеров и совершенно какие-то тупые попытки подбора паролей).

По счастью, одновременно этим занимаются мало ботов, единовременно - ну полсотни IP-адресов. Значит грепаем по логам (благо паттерны понятные), выбираем верхушку и эту верхушку не обслуживаем. И так - несколько раз в день, остроту проблемы это снимает до приемлемой. Падение бот-активности на втором графике связано именно с тем, что применен этот метод.

Если класть плохие IP в файрволл, то можно случайно отстрелить себе ногу (уже было). Значит резать надо на уровне nginx.

Вопросов у меня, собственно, два:

  • limit_req_zone с очень низким rate (скажем, 0.01 или даже 0.002) - нормально ли рабтает? Я, естественно, в эту зону собираюсь пихать не всех пользователей, а только с плохих IP
  • Модуль ngx_posgres - нормально работает? Если у меня будет по запросу на реквест по небольшой табличке (10k префиксов, к примеру) с индексом - при каком потоке запросов оно развалится? (я прекрасно понимаю, что вопрос абстрактный, но тем не менее)

Comments

Чтобы не отстреливать себе ногу, нужно писать

pass ip from table(1) to me
deny ip from table(2) to me

table 1 заполнять вайтлистом

Ваш, К.О. ;-P

Да понятно все, нужен whitelist (хотя вайтлист с динамическими домашними IP - проблема).

Но вопрос мой про иное - мне хочется BL держать таки в постгресе, потому что там можно всяких метаданных (вроде даты добавления) получить. И выбирать nginx-ом сразу, чтобы количество сущностей не плодить.
Про то мои вопросы.

nginx+SQL = troubles, by design

может, memcached, в который уже будет выбираться из постгреса?

Ну можно раз в час (или чаще) выбрать в текстовый файлик и nginx graceful

Но это лишняя сущность, я ее не хочу.

А как ещё? Ну, или надо писать модуль к nginx, который отдельным воркером будет сериализованно ходить в DB и через shared mem отдавать остальным.

Ещё один memcached получится, короче ;)

Выгнать в файлик в формате mod_geo или как оно там называется в nginx.

include его в конфиг.

Ну и ставить переменную в зависимости от IP.

nginx это давно умеет, примерно с рождения. Т.е. mod_geo мой писался для Рамблера, там естественно использовался вовсю и для выноса апача с фронтендов конечно же требовался аналог.

Можно и в memcached, но memcached префиксов же не понимает, если я захочу сеть /8 забанить, то придется туда вылить 16M отдельных IP?

Redis (заместо мемкеша) ключи по маске выбирать умеет.

Или, как альтернатива, делать батчи запросов вида (ip, подсеть 1 проверяемого ip, подсеть 2 проверяемого ip ..., еще какой-то хеш из ip если надо).
Вместо одного запроса получается батч из нескольких, но можно проверять сразу по подсети.

Я вот почитал для общего развития (идеи использовать REDIS нет, но почитал), нашел как в редиску пихают базу geodb

Я правильно понимаю, что если у меня вложенные префиксы, ну там:
8.0.0.0/8 - хороший
8.0.0.0/16 - плохой
8.0.0.0/24 - хороший
8.0.0.11/32 - плохой
8.0.16.0/24 - хороший

То мне придется такую базу развернуть в линейную последовательность IP-адресов и пихнуть в тамошний ordered set

А Native IP-с-масками (и, соответственно, возможность работы с вложенными ranges) - редисом не поддерживается?

На самом деле не обязательно. Он сам весь одна сплошная мапа способная пережевывать практически любые массивы байтов в виде ключей.

Я редиску только начал крутить, так что возможны более удачные варианты. Но я делал-бы что-то вроде ревёрс индекса:
1. Определиться с приоритетами правил, формирования их ключей и с порядком их "удалённости" от некого конкретного ip.
2. Сделал-бы функцию генерящую по выданному ip однозначную и упорядоченную пачку ключей для соответствующих ему правил. Например (в порядке от ближе к дальше) для ip 8.1.16.3/24 пачка была-бы примерно такой:
spam:ip:8.1.16.3/24
spam:ipgroup:8.1.16.3
spam:ipgroup:8.1.16.0/24
spam:ipgroup:8.1.16.0
spam:ipgroup:8.1.0.0/24
spam:ipgroup:8.1.0.0
spam:ipgroup:8.0.0.0/24
spam:ipgroup:8.0.0.0
3. Для любого ключа имеем 3 варианта значения: пусто(get возвращает nil)/да/нет
4. Правила кладём set-ом вида
set spam:ip:8.1.16.3/24 true
или
set spam:ipgroup:8.1.0.0/24 false
5. Проверяем одним mget-ом (multi-get) со всем списком из 2. с последующим поиском в рез-те первого не нила (редиска порядок держит жестко).

В минусе этого подхода рост длинны пачки в 2. для каждого проверяймого ip при добавлении новых типов правил. Но при общей скорости редиски, количестве конкретных вбитых да/нет правил в районе до тысяч, и даже пусть десятках тысяч запросов в день я не думаю, что по производительность будет какой-то проблемой. (В крайнем разе проверку в редисном серверном скрипте делать можно).

Для непускания излишне активных ip-шников можно пользовать ключи с ttl-ем. Делается в 2 комманды:
set spam:ipactive:8.1.16.3/24 0
expire spam:ipactive:8.1.16.3/24 1000
И, соответственно, если по get-у не nil, то отлупить.

Я не понимаю, вот у меня есть группа, как у вас написано. А дальше пришел пользователь с IP равным 8.0.0.1
Предлагается вручную наложить маски (32 штуки) и сделать 32 запроса?

Виноват, не правильно понял вопрос.

Можно попытаться развернуть айпишники с масками в битмапы и при сравнении гонять по ним редисным скриптом пару операций bitop/bitcount.
Тут, вроде, говорят, что bitcount-ы делаются быстро.

Нет, я не понимаю.
Вот есть адрес 8.0.0.1 (можно его в двоичном виде записать, ничего не изменится, как было 32 бита, так и останется).

Он не равен ни одному из перечисленных выше префиксов:
===
8.0.0.0/8 - хороший
8.0.0.0/16 - плохой
8.0.0.0/24 - хороший
8.0.0.11/32 - плохой
8.0.16.0/24 - хороший
===

Как узнать что он хороший, а 8.0.1.0 - наоборот плохой?

http://stackoverflow.com/questions/9989023/store-ip-ranges-in-redis

беда же, однако, в том, что нет простого способа обновить данные во внешнем хранилище при обновлении данных в postgresql. updatable foreign data wrapper есть только для самого же postgresql (да даже если бы он и был для redis'а, вряд ли бы там "из коробки" была поддержка команд zadd/zrange/z*), поэтому остаётся упражняться с pl/perl или pl/python или возвращаться к cron'у.

хотя сама по себе идея явно годная - иметь основное хранилище IP ranges в SQL, lookup к которому был бы максимально дешевым и годным для high-load. чисто теоретически такое можно было бы сделать на ngx_lua (размещая, например, суррогат radix tree в redis/memcache).

Не, на самом деле идея не такая годная, как кажется на первый взгляд:

1) Обновлять таблицы в SQL в реалтайме на каждый запрос - ну всяко ведь не будешь, потому что это не lookup на HTTP-запрос получится, а прямо таки запись с пересчетом статистики.
2) Следовательно, мы считаем статистики раз в какое-то время и из них уже составляем BL.
3) Ну а значит и во внешнее хранилище, хоть в текстовую табличку для nginx, хоть еще чего угодно - можно вылить при подсчете статистики.

Мое желание хранить в постгресе связано только с тем, что инфраструктуры нужно меньше, типа один скрипт который логи парсит ну и в первом приближении все. А вторым способом, с выгрузкой таблицы, скриптов нужно больше.

P.S. Эту страницу в stackoverflow видел, по сути же предлагается вложенный список IP развернуть в плоский (и еще что-то сделать с gap-ами). Что конечно можно, но очень уж сексуально, лучше уж mod-geo.

"годность" идеи касалась только запросов и только в одну сторону - проверка на "вшивость" из www-frontend'а. и даже в этом случае при более-менее высокой нагрузке один SQL-бэкенд не справится (будь он хоть sqlite'ом - там тоже дикое похмелье при единичных обновлениях и большом потоке одновременных запросов на чтение). результат - либо могучая кучка инстансов SQL-серверов с бесплатно прилагающимися развлечениями в виде репликаций (и дай бог, чтобы только с единственным master'ом), либо дальнейшие поиски философского камня.

а если же при этом есть задача ещё и одновременно собирать статистику (т.е. фиксировать факт lookup'а) - то явно будут другие идеи :)

хотя... и в этом случае прыжки и ужимки с lua отнюдь не лишены смысла. например, инкрементировать счётчик для конкретного ip'а с префиксом в виде текущего часа, а раз в час по cron'у выполнять SQL-запрос, агрегирующий собранную статистику и обновляющий BL как в SQL, так и во внешних хранилищах.

> limit_req_zone с очень низким rate (скажем, 0.01 или даже 0.002) - нормально ли рабтает?

Судя по тому как оно устроено - должно работать.

А про ngx_posgres ничего рассказать не могу. Для начала IMHO стоит сделать общий для всех IP ratelimit на оправку новых сообщений (если URI не совпадает с URI для чтения), такой чтобы честные люди от этого не страдали. Например burst=10 и далее по сообщению раз в 20 секунд.

А потом уже можно подумать по поводу более жёстких лимитов для отдельных нехороших IP.

URI может и совпадать, вот метод не совпадает, все что не нравится фигачит POST.

Но я очень не хочу общего лимита, "одно неловкое движение и ты отец", в смысле нормальным юзерам испортишь малину, а они ни в чем не виноватые

Для POST лучше сделать отдельный лог (если еще не) - удобнее грепать и присматривать.

А по вертикальной оси - это количество алертов в сутки?

Ага, количество ловленого спама в сутки. В последней линии обороны.

лично у меня, во всяком случае. связано это с тем, что на моих тестах с высокой concurrency он падал с segfault в src/core/ngx_log.c:151 при вызове из ngx_postgres. впрочем, детально я не разбирался и причиной этого вполне могут быть издержки совокупления c ngx_lua.

кстати, если ответы из постгреса более-менее статичные, то их можно кэшировать и отдавать из memcache/redis через ngx_srcache (пример: http://romantelychko.com/blog/883/)

> Если класть плохие IP в файрволл, то можно случайно отстрелить себе ногу (уже было).

Белый список же, для своих адресов.

Для своих - да.

Но я чуть платежный гейт не отстрелил, который ходит к ключегенерилке. Откуда мне знать его IP?

Как по мне, limit_req - работает нормально, но минимальный поддерживаемый rate - 1 запрос в минуту. Если тебе давить именно низкочастотную активность - то он подойдёт не очень. С другой стороны, алгоритмика позволяет включать для всех - ставишь разумный burst, выключаешь задержки, и вперёд.

Если давить совсем по заранее известным ip - geo + проверки в нужных местах конфига, перегружать конфиг после подкладывания новых списков.

Про ngx_postgres не скажу, не смотрел. Вроде работает, но вообще у Сикоры бывает всякое. Если тебе именно таблички ip-адресов хранить, меняющиеся редко - то я бы рекомендовал geo, см. выше.

Ага, спасибо.
Попробую geo + 1 запрос в минуту для выделенных IP.

Скажи пожалуйста, а как проще всего (в смысле написания в конфиге) ограничить рейт-лимитом только POST-запросы?

Проще всего - сделать отдельные location'ы для тех мест, куда идут POST-запросы, и там и ограничивать.
Если такой возможности нет, то как-то так:

map $request_method $limit { default ""; POST $binary_remote_addr; }
limit_req_zone $limit ...;

Там, к сожалению, к одному месту (авторизации) идут и GET (получить форму авторизации) и POST (авторизоваться).
Поэтому придется map-ом.

Но наскколько я въехал, предлагаемый метод свернет всех не-POST в один ключ? Т.е. залимитит вообще всех GET-ов в узкую щелочку?

Все не-POST'ы свернутся в пустую строку, пустая строка - не лимитируется.
http://nginx.org/r/limit_req_zone

Понял.

Честно скажу
а) в документации это действительно написано
б) увидел я - только когда ткнули и не с первого раза.

Надо туда будет добавить пример конфигурации, когда ограничивается не всё, да.

То есть наверное вот так как-то:

limit_req_zone $binary_remote_addr zone=megaZone:10m rate=9999r/s;
limit_req_zone $binary_remote_addr zone=postZone:10m rate=0.015r/s;
map $request_method $zone {default "megaZone"; POST "postZone";}
location / {
limit_req zone=$zone;
}

Нет, так не взлетит, зону нужно задать явно, там не может быть переменных. Если хочется два разных лимита - нужно включить оба, и сделать, чтобы там, где один из них не должен работать, была пустая строка.