Недавно я написал несколько разных способов ограничить количество запросов, используя Редис .
Как в коммерческих, так и в личных проектах.
В этой статье, состоящей из двух частей, я хочу рассказать о двух разных, но связанных способах ограничения количества запросов — использовании стандартных команд Redis и использовании Луа сценарии.
Каждый последующий описанный метод будет добавлять новые варианты использования и решать недостатки предыдущих.
В этом посте предполагается, что у вас есть некоторый опыт работы с Python и Redis и, в меньшей степени, с Lua, но тем, у кого такого опыта нет, он также будет интересен.
Зачем ограничивать количество запросов?
Например, Твиттер ограничивает количество запросов к своему API и Реддит И Переполнение стека используйте ограничения на количество сообщений и комментариев.Некоторые ограничивают количество запросов для оптимизации использования ресурсов, другие борются со спамерами.
Другими словами, в современном Интернете ограничение количества запросов к платформе направлено на ограничение влияния, которое может оказывать пользователь.
Независимо от причины, давайте предположим, что нам следует подсчитать некоторые действия пользователя и предотвратить их, если пользователь достиг или превысил некоторый лимит. Начнем с ограничения количества запросов к какому-либо API до 240 запросов в час на пользователя.
Мы знаем, что нам нужно подсчитывать действия и ограничивать пользователя, поэтому нам понадобится немного вспомогательного кода.
Во-первых, у нас должна быть функция, которая дает нам один или несколько идентификаторов пользователя, выполняющего действие.
Иногда это просто IP пользователя, иногда его ID. Я предпочитаю использовать оба, если это возможно.
Хотя бы IP, если пользователь не авторизован.
Ниже приведена функция получения IP и идентификатора пользователя с помощью Колба Плагин Flask-Login.
from flask import g, request def get_identifiers(): ret = ['ip:' + request.remote_addr] if g.user.is_authenticated(): ret.append('user:%s'%g.user.get_id()) return ret
Просто используйте счетчики
Теперь у нас есть функция, возвращающая идентификаторы пользователей, и мы можем начать подсчитывать наши действия.Один из самых простых методов, доступных в Redis, — вычислить ключ для временного диапазона и увеличивать в нем счетчик каждый раз, когда происходит интересующее нас действие.
Если число в счетчике превысит нужное нам значение, мы не позволим выполнить действие.
Вот функция, которая использует переключатели автоматического тушения с диапазоном (и сроком службы) 1 час: import time
def over_limit(conn, duration=3600, limit=240):
bucket = ':%i:%i'%(duration, time.time() // duration)
for id in get_identifiers():
key = id + bucket
count = conn.incr(key)
conn.expire(key, duration)
if count > limit:
return True
return False
Это довольно простая функция.
Для каждого идентификатора мы увеличиваем соответствующий ключ в Redis и устанавливаем время его жизни 1 час.
Если значение счетчика превышает предел, вы вернете True. В противном случае мы вернем False. Вот и все.
Ну или почти.
Это позволяет нам достичь нашей цели — ограничить количество запросов до 240 в час для каждого пользователя.
Однако реальность такова, что пользователи быстро заметят, что лимит сбрасывается в начале каждого часа.
И ничто не помешает им сделать свои 240 запросов за пару секунд прямо в начале часа.
В этом случае наша работа пойдет прахом.
Мы используем разные диапазоны
Наша первоначальная цель по почасовому ограничению запросов была успешной, но пользователи начинают отправлять все свои запросы в API как можно скорее (в начале каждого часа).Похоже, помимо почасового лимита стоит ввести посекундный и поминутный лимит, чтобы сгладить ситуации с пиковым количеством запросов.
Допустим, мы решили, что 10 запросов в секунду, 120 запросов в минуту и 240 запросов в час достаточны для наших пользователей и позволят нам лучше распределять запросы во времени.
Для этого мы можем просто использовать нашу функцию over_limit() : def over_limit_multi(conn, limits=[(1, 10), (60, 120), (3600, 240)]):
for duration, limit in limits:
if over_limit(conn, duration, limit):
return True
return False
Это будет работать так, как мы ожидали.
Однако каждый из трех вызовов over_limit() может выполнять две команды Redis — одну для обновления счетчика, а вторую для установки срока действия ключа.
Мы запустим их на предмет IP и идентификатора пользователя.
В результате к Redis может потребоваться до 12 запросов только для того, чтобы сказать, что один человек превысил лимит на одну операцию.
Самый простой способ минимизировать количество запросов к Redis — использовать `конвейерная обработка` (конвейерные запросы).
Такие запросы в Redis также называются транзакционными.
В контексте Redis это означает, что вы отправите множество команд за один запрос.
Нам повезло, что наша функция over_limit() написана таким образом, что мы можем легко заменить вызов ИНКР И Срок действия истекает по запросу с МУЛЬТИ .
Это изменение позволит нам сократить количество запросов к Redis с 12 до 6, когда мы используем его с over_limit_multi() .
def over_limit(conn, duration=3600, limit=240):
pipe = conn.pipeline(transaction=True)
bucket = ':%i:%i'%(duration, time.time() // duration)
for id in get_identifiers():
key = id + bucket
pipe.incr(key)
pipe.expire(key, duration)
if pipe.execute()[0] > limit:
return True
return False
Сокращение количества вызовов Redis вдвое — это здорово, но мы по-прежнему делаем 6 запросов, чтобы проверить, сможет ли пользователь выполнить вызов API. Вы можете написать другой вариант over_limit_multi() , который выполняет все операции одновременно и после этого проверяет ограничения, но, очевидно, в реализации будет несколько ошибок.
Мы сможем ограничить пользователей и разрешить им делать не более 240 запросов в час, однако в худшем случае это будет всего 10 запросов в час.
Да, ошибку можно исправить, сделав еще один запрос к Redis, а можно просто перенести всю логику в Redis!
Мы думаем правильно
Вместо того, чтобы исправлять нашу предыдущую реализацию, давайте переместим ее в сценарий LUA, который мы будем выполнять внутри Redis. В этом скрипте мы сделаем то же самое, что и выше — пройдемся по списку ограничений, для каждого идентификатора будем увеличивать счетчик, обновлять время жизни и проверять, не превысил ли счетчик лимит. import json
def over_limit_multi_lua(conn, limits=[(1, 10), (60, 125), (3600, 250)]):
if not hasattr(conn, 'over_limit_multi_lua'):
conn.over_limit_multi_lua = conn.register_script(over_limit_multi_lua_)
return conn.over_limit_multi_lua(
keys=get_identifiers(), args=[json.dumps(limits), time.time()])
over_limit_multi_lua_ = '''
local limits = cjson.decode(ARGV[1])
local now = tonumber(ARGV[2])
for i, limit in ipairs(limits) do
local duration = limit[1]
local bucket = ':' .
duration .
':' .
math.floor(now / duration)
for j, id in ipairs(KEYS) do
local key = id .
bucket
local count = redis.call('INCR', key)
redis.call('EXPIRE', key, duration)
if tonumber(count) > limit[2] then
return 1
end
end
end
return 0
'''
Посмотрите на фрагмент кода сразу после 'локальное ведро' .
Вы можете видеть, что наш скрипт Lua выглядит как наше предыдущее решение и выполняет те же операции, что и исходный.
over_limit() ?
Заключение
Мы начали с одного временного интервала, и в итоге имеем метод ограничения количества запросов, который может работать с несколькими уровнями ограничений, работать с разными идентификаторами для одного пользователя и выполнять только один запрос к Redis. Собственно, любой из вариантов наших ограничителей может пригодиться в разных приложениях.Не нашел, как для статьи в песочнице правильно указать, что это перевод:
- Автор статьи Джозайя Карлсон
- Оригинальная статья
-
Форматирование В Комментариях
19 Oct, 24 -
Анализатор Исходного Кода Rats
19 Oct, 24