Ограничение запросов к API: правильный путь
Всем привет. Меня зовут Александр, я разработчик Javascript. Сегодня я хочу рассказать вам историю о том, как я пытался найти дзен в создании серверного приложения, которое удовлетворит все API в мире.
Пролог
Он был запущен в июне 2015 года, когда Telegram анонсировал новую платформу ботов в рамках API. Я был полнофункциональным разработчиком Javascript и PHP, работающим в небольшой веб-студии, и вся моя работа заключалась в быстрой разработке целевых страниц на основе этого стека. Мысль о том, чтобы ваш рабочий робот был прямо в вашем мессенджере, была сногсшибательной, нечто подобное я разрабатывал в 2010 году для ICQ. Единственный оставшийся вопрос - идея будущего бота. Что он будет делать? Как я буду его развивать? Поэтому я решил написать банексбот.
Я начал читать API ботов каждый день, пытаясь понять, как мне его использовать. Я решил написать небольшого PHP-бота с базой данных MySQL, который должен просто запрашивать у группы ВК новые сообщения, транслировать их всем подписанным пользователям и может показывать самые понравившиеся сообщения в день / неделю / месяц / когда-либо. Он был выпущен в июле 2015 года.
В 2016 году я решил полностью переписать этого бота на Node.JS. Вместо MySQL я выбрал MongoDB для хранения сообщений и ElsaticSearch для быстрого поиска. Вместо долгого опроса я начал использовать веб-перехватчики. Итак, вышел банексбот v2.0. У меня было около 20 000 пользователей, которые были подписаны на новые сообщения, поэтому я быстро столкнулся с ограничениями Bot API и начал получать HTTP 429
errors вместо ответов Telegram. Это был октябрь 2016 года, и тогда я понял, что мне нужно как-то ограничить запросы ботов и сделать их медленнее.
Кратко о тарифах / лимитах
Как вы, возможно, знаете, многие службы, совместимые с REST API, имеют так называемое ограничение скорости для предотвращения DoS-атак и перегрузки сервера. У кого-то есть мягкие правила, по которым вы можете выйти за их пределы на короткий период времени, а у кого-то строгие правила, по которым вы сразу получите HTTP 429 в качестве ответа и тайм-аут, по истечении которого вы можете повторить свой запрос.
В моем случае у Telegram были мягкие, но жесткие правила ограничения скорости, которые я неоднократно игнорировал, и многие мои сообщения никогда не отправлялись пользователям. Поэтому я решил найти решения и попытаться найти подходы, как можно следовать этим правилам.
Самый простой способ - просто установить тайм-аут, а затем отправить сообщение:
const delay = interval => new Promise(resolve => setTimeout(resolve, interval)); const sendMessage = async params => { await delay(1000); return axios(params); };
Плюсы:
- Легко как ад
- Работает как шарм
Минусы:
- Трудно управлять
- Невозможно настроить индивидуально
Так что это в основном основной подход для людей, только что перешедших с PHP на Node.JS и пытающихся написать что-то, что не будет работать так быстро . Но очевидно, что Node.JS намного мощнее в случае асинхронных вещей, поэтому нам нужно что-то элегантное. А для этого нам нужна очередь.
Очереди запросов
На самом деле на npmjs.com много очередей запросов. Некоторые из них довольно хороши, некоторые - нет. Я начал пробовать их и посмотреть, смогут ли они правильно работать в моих сценариях использования. Я использовал библиотеку под названием request
для простого выполнения HTTP-запросов. после require('http').request
это было похоже на глоток свежего воздуха: у вас есть обещания, вы можете использовать потоки, вы можете давать им удобные для пользователя URL-адреса и т. д. Итак, моим первым выбором был ограничитель скорости запросов. Его легко настроить и использовать везде. На самом деле, это идеальная библиотека для 95% случаев использования.
const RateLimiter = require('request-rate-limiter');
const limiter = new RateLimiter(120); // 120 requests per minute
const sendMessage = params => limiter.request(params);
sendMessage('/sendMessage?text=hi') .then(response => { console.log('hello!', response); }).catch(err => { console.log('oh my', err); });
Плюсы:
- Он не будет отправлять больше запросов, чем разрешено API.
- В нем есть встроенная очередь, поэтому вы можете легко отбросить туда запросы и дождаться ответа.
Минусы:
- Для каждого экземпляра можно настроить только одно правило
- Таким образом, у вас не может быть общей очереди, чтобы ваше приложение глобально соответствовало этим правилам.
Как видите, это почти идеальная библиотека для постановки запросов в очередь. Но давайте более внимательно ознакомимся с Telegram Bot API. Есть несколько правил:
- Вы можете отправлять 1 сообщение в секунду в отдельные чаты.
- Вы можете отправлять 20 сообщений в минуту группам / каналам.
- Но в этот момент вы не можете отправлять более 30 сообщений в секунду в целом .
Вы можете подумать, что если я установлю скорость / лимит на 1/3 (20 сообщений за 60 секунд), все будут счастливы. Но давайте еще раз подумаем об этих цифрах.
Подождите 3 секунды перед следующим запросом
Страшно звучит, да? Но это правда. Если вы хотите легко следовать этим правилам, вы не можете отправить его вместо этого срока. Я был разочарован. Я хотел отправлять эти сообщения из ВК как можно быстрее, чтобы предоставлять моим пользователям самый популярный контент сразу после того, как он появился в ВК. Также я обещал в своем боте пришлет вам новый пост не позднее, чем через минуту после публикации этого поста. Поэтому я решил разработать свою собственную очередь: с блэкджеком и… правилами.
Умная очередь
Я начал разрабатывать эту очередь в январе 2017 года. Создал базовую концепцию и написал первый прототип через неделю. Основными концепциями этой очереди были:
- Должна быть очередь, которая будет хранить запросы и выполнять их в правильном порядке.
- Должна быть возможность устанавливать несколько правил для разных запросов.
- Правила должны иметь приоритет, чтобы запросы с меньшим приоритетом могли задерживаться немного и уступать место более важным.
- Даже если я идеально напишу эти правила, должен быть план Б, чтобы повторить этот запрос без лишних усилий и получить ответ в том же обещании.
Так я создал свою первую публичную библиотеку npm под названием smart-request-balancer. Он может легко следовать этим правилам и сделать мой API бота безопасным почти на два года. Позвольте мне объяснить, как это работает:
Прежде всего, вам нужно создать очередь,
const SmartQueue = require('smart-request-balancer');
затем вы инициализируете его
const queue = new SmartQueue(config);
и пользуйся этим. Ничего особенного!
const sendMessage = params => queue.request(retry => axios(params)
.then(response => response.data)
.catch(error => {
if (error.response.status === 429) {
return retry(error.response.data.parameters.retry_after);
}
throw error;
}), user_id, rule);
sendMessage('/sendMessage?text=hi')
.then(response => {
console.log('hello!', response);
}).catch(err => {
console.log('oh my', err);
});
Вы спросите, а что такое error.response.data.parameters.rerty_after
? user_id
? _11 _ ??? Позволь мне объяснить.
В этом конкретном примере мы создали функцию sendMessage
, которая в основном выполняет запрос axios
, но также имеет еще два параметра: user_id
и rule
. Здесь user_id
- это просто уникальный ключ для пользователя, на основе которого мы можем хранить эти запросы в очереди. Например, если вы хотите отправить 30 сообщений для пользователя 1 и 50 сообщений для пользователя 2, будет 2 очереди, и они не должны ждать друг друга и работать независимо. 1 сообщение в секунду для пользователей, помните? Но в то же время это не должно нарушать общее правило для Telegram, которое не должно превышать 30 сообщений в секунду в глобальном масштабе!
Все еще не понимаете? Позвольте мне показать вам, как настроить эту очередь:
const config = { rules: { individual: { rate: 1, limit: 1, priority: 1 }, group: { rate: 20, limit: 60, priority: 1 }, broadcast: { rate: 30, limit: 1, priority: 2 } }, overall: { rate: 30, limit: 1 } }
Как видите, у нас есть 3 правила: для отдельных лиц, для групп / каналов и для вещания. А также у нас есть общее правило. Кроме того, индивидуальные и групповые правила имеют более высокий приоритет, чем широковещательные, что означает, что чем когда мой бот простаивает и хочет транслировать сообщения, он легко их транслирует, но как только кто-то отправит команду, он немедленно ответит, а затем продолжит широковещательную рассылку. Также бот никогда не достигнет общего лимита в 30 сообщений в секунду, чтобы его не игнорировал API.
Итак, вернемся к нашему примеру. Что такое retry
? retry
- это специальная функция, которую следует вызывать, если вы случайно достигли лимита (в нашем случае - 429) или по какой-то причине хотите повторить свой запрос. Когда вы вызываете эту функцию с некоторым интервалом внутри, этот запрос будет добавлен в очередь сразу после этого интервала и разрешит именно это обещание сразу после этого интервала. Довольно аккуратно, да?
Но подождите, что такое rule
? И это именно rule
, который мы указали в config.
Как это работает
Подведем итог этому алгоритму:
- Вы делаете запрос через
queue.request
SmartQueue
добавляет этот запрос в очередь<key, rule>
- Если в общей очереди есть первый элемент, выполнить запрос, в противном случае дождаться своей очереди
- Разрешить запрос, подогреть его очередь и общую очередь
- Удалить пустую очередь
- Возвращайтесь к шагу 3, пока в очереди не останется запросов.
Легко и мощно. Больше ничего.
Эпилог
Эта очередь успешно работает с тех пор внутри моих многочисленных ботов, и я почти забыл об этом вещании и начал наслаждаться своей жизнью. Все эти два года я полировал эту библиотеку и готовил ее к изданию. А теперь готов поставить к людям мирные очереди.
В заключение, вот полезные ссылки, которые я использовал при написании этой статьи:
- Ограничение скорости в википедии
- Ссылка на библиотеку smart-request-balancer на npm
- Ссылка на библиотеку smart-request-balancer на github
- Ссылка на телеграм-бот baneksbot на github
Любой вклад с вашей стороны будет приветствоваться. Спасибо за терпение и внимание. И да пребудет с тобой сила! До свидания.