Как избежать ограничений на скорость при использовании ключа API администратора?

Я получаю сообщение «429 Too Many Requests» при запросах к API на моём собственном экземпляре, даже при следующих условиях:

  • DISCOURSE_MAX_ADMIN_API_REQS_PER_MINUTE увеличен до 600
    • Я установил это значение в секции env файла app.yml, затем выполнил ./launcher rebuild и подтвердил, что переменная установлена в пересобранном контейнере.
    • Это значение значительно превышает количество запросов в минуту, которые я пытаюсь выполнить.
  • используется неограниченный ключ администратора API.

Кажется, эта проблема уже обсуждалась ранее, но чёткого ответа, почему изменение DISCOURSE_MAX_ADMIN_API_REQS_PER_MINUTE не работает, так и не было найдено:

Как можно гарантировать, что запросы к API с ключом/учётной записью администратора не будут подвергаться ограничению скорости?

Привет, @aas,

Можешь дать немного контекста?

  • Сколько запросов к API ты делаешь? В секунду, минуту, час, в день
  • Уверен ли ты, что используешь ключ API администратора?
  • Все ли они приходят с одного IP-адреса? Возможно, из-за обратного прокси?

Не может ли это быть nginx или другое программное обеспечение, которое выдаёт тебе эту ошибку?

Привет, @Bas,

Приношу извинения за задержку с ответом!

Я снова приступаю к изучению этого вопроса, так как мы запустили интеграцию с Discourse и хотим убедиться, что не столкнемся с проблемами, связанными с ограничением частоты запросов (rate limiting).

Я протестировал это с новым ключом, чтобы убедиться, что на него не наложено никаких ограничений. Чтобы прояснить ситуацию: что именно вы подразумеваете под ключом API администратора?

Я создал ключ со следующими настройками:

В нем указано: «Ключ API не имеет ограничений, и доступны все конечные точки».

Я провожу тестирование, отправляя запросы к API из локальной оболочки Python, поэтому они действительно исходят с одного IP-адреса. Мы также столкнулись с лимитами частоты запросов при запуске скрипта на нашем сервере. В этом случае все запросы также приходили с одного IP-адреса.

Я подтвердил, что лимит частоты запросов достигается, используя следующий код:

async def get_topic_post_stream(topic_id):
    url = f"{DISCOURSE_URL}/t/{topic_id}"
    async with httpx.AsyncClient(headers=HEADERS) as client:
        topic = await client.get(url)
    return topic.status_code


async def get_topic_post_streams(topic_ids):
    tasks = [functools.partial(get_topic_post_stream, topic_id) for topic_id in topic_ids]
    topics = await aiometer.run_all(
        tasks,
        # max_per_second=1,
        )
    return topics

# Для тестирования берем срез из 15 тем из topic_ids.
topics = asyncio.run(get_topic_post_streams(topic_ids[:15]))

Обратите внимание, что параметр max_per_second закомментирован, что означает отсутствие ограничений на количество запросов.

Это выполняется за 2,05 секунды, и 2 из 15 запросов возвращают 429.

Когда я запускаю код с параметром max_per_second=1, всё выполняется успешно.

Дайте знать, если я могу предоставить какие-либо дополнительные детали. Спасибо!

@Bas, вот эквивалент на JavaScript для Python-кода, чтобы упростить воспроизведение с помощью консоли инструментов разработчика:

const DISCOURSE_URL = '';
const HEADERS = {
    'Api-Key': '',
    'Api-Username': '',
    'Content-Type': 'application/json'
};


const topicIds = Array.from({ length: 100 }, (_, i) => i + 1);

async function getTopicPostStream(topicId) {
    const url = `${DISCOURSE_URL}/t/${topicId}`;
    const response = await fetch(url, { headers: HEADERS });
    return response.status;
}

async function getTopicPostStreams(topicIds) {
    const results = await Promise.all(topicIds.map(topicId => getTopicPostStream(topicId)));
    return results;
}

// Не ограничивайте скорость запросов и посмотрите, что получите два ответа 429.
(async () => {
    const topics = await getTopicPostStreams(topicIds.slice(0, 15));
    console.log(topics);
})();

async function getTopicPostStreamsRateLimited(topicIds) {
    const results = [];
    for (const topicId of topicIds) {
        const result = await getTopicPostStream(topicId);
        results.push(result);
        await new Promise(resolve => setTimeout(resolve, 1000)); // Задержка на 1 секунду
    }
    return results;
}

// 1 запрос в секунду возвращает все ответы 200
(async () => {
    const topics = await getTopicPostStreamsRateLimited(topicIds.slice(0, 15));
    console.log(topics);
})();

Если мне придется высказать предположение, то, скорее всего, проблема именно в этом. Вас ограничивают не на уровне API, а на основе вашего IP-адреса.

Вы можете посмотреть настройки для каждого IP-адреса здесь: Available settings for global rate limits and throttling - #27

Дайте знать, если это действительно решит вашу проблему :slight_smile:

Спасибо, @Bas!

Мне кажется, что я не должен получать эти ошибки 429 независимо от каких-либо настроек, упомянутых в том посте. В предоставленном мной примере я отправил 15 запросов, что меньше всех лимитов API по умолчанию. Я сделал это, используя ключ API администратора и имя пользователя.

Пример не превышает следующие лимиты по умолчанию для одного IP-адреса:

Он даже не превышает лимиты для неадминистраторов:

Изменение DISCOURSE_MAX_REQS_PER_IP_MODE на warn или none не помогло.

Не упускаю ли я что-то? :thinking:

Кстати, я изменил настройки, отредактировав app.yml и выполнив ./launcher destroy app && ./launcher start app.

Я вижу в /var/log/nginx/access.log, что IP-адрес указан верно, поэтому, думаю, Discourse не считает все запросы приходящими с одного IP.

Также я могу видеть IP-адреса пользователей в админ-панели.

Вот настройки, которые я изменил:

  DISCOURSE_MAX_ADMIN_API_REQS_PER_MINUTE: 1200
  DISCOURSE_MAX_USER_API_REQS_PER_MINUTE: 60
  DISCOURSE_MAX_REQS_PER_IP_MODE: none
  DISCOURSE_MAX_REQS_PER_IP_PER_10_SECONDS: 100
  DISCOURSE_MAX_REQS_PER_IP_PER_MINUTE: 400

РЕДАКТИРОВАНИЕ: Я только что проверил содержимое ответа одного из неудавшихся запросов и заметил, что в нём упоминается nginx:

<html>\r\n<head><title>429 Too Many Requests</title></head>\r\n<body>\r\n<center><h1>429 Too Many Requests</h1></center>\r\n<hr><center>nginx</center>\r\n</body>\r\n</html>\r\n

Я проведу дополнительное расследование по темам, в которых упоминается nginx.

Похоже, что две соответствующие секции конфигурации nginx выглядят следующим образом:

limit_req_zone $binary_remote_addr zone=flood:10m rate=12r/s;
limit_req_zone $binary_remote_addr zone=bot:10m rate=200r/m;
limit_req_status 429;
limit_conn_zone $binary_remote_addr zone=connperip:10m;
limit_conn_status 429;
server {
  listen 80;
  return 301 https://community.ankihub.net$request_uri;
}

и

  location @discourse {
add_header Strict-Transport-Security 'max-age=31536000'; # сохранить сертификат на год и автоматически подключаться по HTTPS для этого домена
  limit_conn connperip 20;
  limit_req zone=flood burst=12 nodelay;
  limit_req zone=bot burst=100 nodelay;
    proxy_set_header Host $http_host;
    proxy_set_header X-Request-Start "t=${msec}";
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header X-Forwarded-Proto $thescheme;
    proxy_pass http://discourse;
  }
}

Теперь у меня остались следующие вопросы:

  • Нужно ли редактировать обе секции, чтобы они соответствовали моим настройкам Discourse? Или только значения для location @discourse?

  • Как правильно изменить эти значения и сохранить изменения при пересборке?
    Я предполагаю, что могу отредактировать конфигурацию nginx напрямую в контейнере, а затем остановить и запустить контейнер заново. Но похоже, что эти значения изначально взяты из templates/web.ratelimited.template.yml и могут быть перезаписаны при пересборке?

Большое спасибо за вашу помощь! :pray:

Ох, боюсь, мы теперь выходим за пределы моей зоны комфорта.

Если вы сталкиваетесь с ограничением скорости из-за nginx, то да, изменение этих настроек и их смягчение имеет смысл. Не уверен, может ли Nginx добавлять IP-адреса в белый список?

Что-то вроде

map $remote_addr $exclude_from_limit {
    default 0;
    192.168.1.1 1;
    192.168.1.2 1;
}

а затем обернуть лимиты в if

        if ($exclude_from_limit = 0) {
            limit_req zone=flood burst=24 nodelay;
            limit_req zone=bot burst=400 nodelay;
            limit_conn connperip 20;
        }

Да, вам следует выполнить некоторую замену с помощью pups во время сборки, чтобы сделать это постоянным. Например, посмотрите web.ssl.template.yml, чтобы понять, как это реализовать.

Или же вы можете забыть об этом и замедлить работу своего скрипта API-клиента, добавив задержки (sleep) в стратегически важных местах. ← рекомендуемый подход

Например, в блоке rescue, когда происходит ограничение скорости. Именно так я обычно поступаю, по крайней мере, так мне хочется думать.

Какова максимальная устойчивая частота запросов к API в минуту или секунду при стандартной установке?

Я попробовал делать один запрос в секунду, и, похоже, уперся в лимит. В то же время один запрос каждые 7 секунд работает без проблем.