Введение В Ограничение Запросов С Помощью Redis [Часть 1]

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

Как в коммерческих, так и в личных проектах.

В этой статье, состоящей из двух частей, я хочу рассказать о двух разных, но связанных способах ограничения количества запросов — использовании стандартных команд 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. Собственно, любой из вариантов наших ограничителей может пригодиться в разных приложениях.

Не нашел, как для статьи в песочнице правильно указать, что это перевод:

Теги: #Lua #redis #python #ограничение скорости #перевод #NoSQL
Вместе с данным постом часто просматривают:

Автор Статьи


Зарегистрирован: 2019-12-10 15:07:06
Баллов опыта: 0
Всего постов на сайте: 0
Всего комментарий на сайте: 0
Dima Manisha

Dima Manisha

Эксперт Wmlog. Профессиональный веб-мастер, SEO-специалист, дизайнер, маркетолог и интернет-предприниматель.