Pull to refresh

Comments 18

Мне кажется или код
pipe.incr(key)
pipe.expire(key, duration)
if pipe.execute()[0] > limit:

очень грязно считает время, за счет expire на каждом шаге. В итоге если постоянно понемногу добавлять, то мы все равно наберем limit за счет того, что ключ не выпадет по expire.
Для фиксированного лимитера expire только в момент создания ключа должен создаваться, т.е. если результат инкремента вернет 1. Для плавающего лимитера, нужно учитывать duration для каждого предыдущего запроса, так как для части из них это время уже могло пройти.
Вам кажется. Ключ живёт ещё какое-то время после того как мы уже используем другой интервал. Expire тут играет роль «чистильщика» ключей, которые нам уже не нужны, не более того. Сам ключ, который отвечает на выбор диапазона — это конкатенация идентификатора пользователя, duration и остатка от деления текущего времени в секундах на duration. За счёт последней части мы и выбираем тот интервал времени в котором пользователя стоит ограничить.
Пороговый лимитер получается. Почему бы тогда просто не сетить expire первому запросу и писать все в 1 ключ? Получится пороговый лимитер, который отсчет dyration ведет от первого запроса и сам удаляется по expire, но при этом всего 1 ключ.
К сожалению в redis после того как вы сделаете INCR время жизни ключа «слетит». Поэтому приходится каждый раз делать expire после изменения ключа.
Только что проверил отладчиком. TTL не слетает.

$redis->expire($key, 20);
sleep(4);
print_r($redis->ttl($key)); // 16
sleep(2);
$redis->incr($key);
print_r($redis->ttl($key)); // 14
Бес попутал. Простите, что ввёл в заблуждение. Может быть в прошлом была такая или схожая проблема.
В прошлом действительно была, но уже год точно все нормально.
Мне кажется, эта статья для глупых джуниоров, при этом нет плашки tutorial. Во всех остальных случаях под такую задачу писать LUA скрипт глупо, тогда как настроить редис, чтобы тот автоматически очищал память (maxmemory + maxmemory-policy опции в конфиге) по LRU вместо экспайров, может кто угодно, кто умеет обращаться с редисом. Автор, похоже, не умеет. В крайнем случае, то же самое можно сделать в мемкеше, который точно знают все веб-разработчики.
Я думаю, что автор (Josiah Carlson) может работать в ситуации, когда он не может или хочет использовать LRU. Например, он использует Redis не только как кэш сервер.
Что мешает поставить ещё один (или десять) редисов? Memory footprint у чистого редиса настолько незначительный, что нет никаких проблем поднимать много редисов. Более того, из-за однопоточности редиса это быстро становится необходимым для сохранения стабильно низкого времени ответа, особенно на современных многоядерных серверах.
Давайте спросим у него в его блоге :)
Какой смысл переносить проблему с больной головы на здоровую? Мы хотим снизить нагрузку с Redis и уменьшаем количество запросов с 12 до 6. А потом весело переносим все внутрь базы данных, начав использовать Lua. В итоге как у нас была нагрузка на базу данных, так она и осталась.

Может быть проще придумать более изящный способ решить эту проблему, а не делать вид, что нашли решение?
Дело в том, что большая часть «расходов» ляжет именно на отправку и получение ответа от redis. Выполнение этих запросов внутри контекста redis сервера существенно быстрее их же выполнения из клиентского скрипта и помогает немного сэкономить — используя LUA вам уже не нужно использовать MULTI запросы.
Вообще ограничивать запросы на API иногда оказывается полезно.
Достал тут как-то один пользователь.
Ломится, понимаешь, на API /Auth несколько раз в секунду с одним и тем же неправильным логин-паролем.
В принципе пофиг, просто логи читать мешает.
Вообще для пользования сервисом достаточно было /Auth один раз в 4 часа вызвать, так что поставили ограничение на 1 запрос в минуту.
Не таким, конечно, способом как в статье, а на nginx limit_req.
Так что бы вы думали, ни один пользователь не пожаловался, кроме своих же.
«Уберите ограничение, мы тут че-то это не кэшируем ...»
«А нам тут надо много и сразу...»
Ограничение конечно убрали, а тот пользователь с плохим паролем похоже намек понял и больше так не делает :)
Мне кажется всё намного проще. В качестве ключа в redis используем ID пользователя, в качестве значения храним пару — timestamp последнего обращения и приблизительное количество запросов пользователя за последний час. Пересчитываем так:
from datetime import datetime
def over_limit(value, duration=3600., limit=240.):
    value.requests = max(0.0, value.requests - (datetime.now() - value.ts).total_seconds() * limit / duration) + 1.0
    overlimit = (value.requests > limit)
    return value, overlimit

Запроса будет всего два — get и set. А в качестве лимита будет скользящее среднее за последний час.
Зависит от того, от чего защищаться. От флуда и всяческих переборов паролей эффективно защищаться короткоживущими счётчиками, по ID юзера и по IP (стоит добавить, что в этом случае экспайр заведомо вреден). Для уменьшения нагрузки на базу можно придумывать что-то такое, но в целом это не очень логичное поведение — особо любопытному пользователю, или поисковому краулеру, отдавать какую-нибудь 500-ую ошибку вместо контента. Хотя, конечно, в нормальных языках программирования можно безболезненно задержать такие запросы, чтобы общий рейт запросов в БД был адекватным. Ну и в большем количестве случаев ограничения рейта запросов на уровне nginx достаточно.
Осталось выяснить, как можно ограничить количество запросов к редису
Sign up to leave a comment.

Articles