Ограничение запросов к 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 429errors вместо ответов 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.

Как это работает

Подведем итог этому алгоритму:

  1. Вы делаете запрос через queue.request
  2. SmartQueue добавляет этот запрос в очередь <key, rule>
  3. Если в общей очереди есть первый элемент, выполнить запрос, в противном случае дождаться своей очереди
  4. Разрешить запрос, подогреть его очередь и общую очередь
  5. Удалить пустую очередь
  6. Возвращайтесь к шагу 3, пока в очереди не останется запросов.

Легко и мощно. Больше ничего.

Эпилог

Эта очередь успешно работает с тех пор внутри моих многочисленных ботов, и я почти забыл об этом вещании и начал наслаждаться своей жизнью. Все эти два года я полировал эту библиотеку и готовил ее к изданию. А теперь готов поставить к людям мирные очереди.

В заключение, вот полезные ссылки, которые я использовал при написании этой статьи:

Любой вклад с вашей стороны будет приветствоваться. Спасибо за терпение и внимание. И да пребудет с тобой сила! До свидания.