Como evitar limites de throttling com chave de API de administrador?

Estou recebendo a mensagem “429 Too Many Requests” para solicitações de API à minha instância auto-hospedada, mesmo com:

  • DISCOURSE_MAX_ADMIN_API_REQS_PER_MINUTE aumentado para 600
    • Defini isso na seção env de app.yml e depois executei ./launcher rebuild e confirmei que a variável foi definida no contêiner reconstruído.
    • Isso é muito mais do que o número de solicitações por minuto que estou tentando.
  • uma chave de API de administrador irrestrita.

Parece que isso já foi discutido antes sem uma resposta clara sobre por que a alteração de DISCOURSE_MAX_ADMIN_API_REQS_PER_MINUTE não parece funcionar:

Como posso garantir que as solicitações de API com uma chave/usuário de administrador não estejam sujeitas a limitação de taxa?

Olá @aas,

Você poderia fornecer algum contexto?

  • Quantas requisições de API você está fazendo? Por segundo, minuto, hora, por dia?
  • Você tem certeza de que está usando uma chave de API de administrador?
  • Todas essas requisições estão vindo do mesmo endereço IP? Talvez devido a um proxy reverso?

Pode ser o nginx ou outro software te retornando esse erro?

1 curtida

Olá @Bas,

Peço desculpas pela resposta tardia!

Estou analisando isso novamente, pois lançamos uma integração do Discourse e queremos ter certeza de que não encontraremos nenhum problema relacionado à limitação de taxa.

Eu testei com uma nova chave para garantir que ela não seja limitada de forma alguma. Para ser claro, o que exatamente você quer dizer com uma chave de API de administrador?

Criei uma chave com as seguintes configurações:

Diz: “A chave de API não tem restrição e todos os endpoints são acessíveis.”

Estou testando isso fazendo requisições de API de um shell Python local, então elas vêm do mesmo endereço IP. Também encontramos os limites de taxa ao executar um script em nosso servidor. Nesse caso, todas as requisições vieram do mesmo endereço IP.

Confirmei que o limite de taxa é atingido com o seguinte código:

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

# Apenas obtenha uma fatia de 15 dos tópicos em topic_ids para teste.
topics = asyncio.run(get_topic_post_streams(topic_ids[:15]))

Note que o parâmetro max_per_second está comentado, o que resulta em nenhum limite no número de requisições.

Isso é concluído em 2,05 s e 2 das 15 requisições retornam 429.

Quando eu o executo com max_per_second=1, tudo é concluído com sucesso.

Me avise se posso fornecer mais detalhes. Obrigado!

@Bas, aqui está o equivalente em javascript do código python para facilitar a reprodução usando o console de ferramentas do desenvolvedor:

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;
}

// Não limite a taxa das requisições e veja que você recebe dois 429s.
(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)); // Atraso de 1 segundo
    }
    return results;
}

// 1 requisição por segundo retorna todos os 200s
(async () => {
    const topics = await getTopicPostStreamsRateLimited(topicIds.slice(0, 15));
    console.log(topics);
})();
1 curtida

Se eu tivesse que arriscar um palpite, este é o problema mais provável. Você não está sendo limitado pela taxa na API, mas com base no seu IP.

Você pode consultar as configurações por IP aqui: Available settings for global rate limits and throttling - #27

Por favor, me diga se isso realmente resolve o seu problema :slight_smile:

1 curtida

Obrigado, @Bas!

Parece que eu não deveria estar recebendo esses 429s, independentemente de quaisquer configurações mencionadas nesse post. No exemplo que forneci, enviei 15 requisições, o que está abaixo de todos os limites padrão da API. Fiz isso usando uma chave de API e nome de usuário de administrador.

O exemplo não excede os seguintes padrões por IP:

Nem mesmo excede os limites não administrativos:

Alterar DISCOURSE_MAX_REQS_PER_IP_MODE para warn ou none não ajudou.

Estou perdendo alguma coisa? :thinking:

Aliás, alterei as configurações editando app.yml e executando ./launcher destroy app && ./launcher start app.

Consigo ver em /var/log/nginx/access.log que o endereço IP está correto, então não acho que o Discourse considere todas as requisições vindas do mesmo IP.

Também consigo ver os endereços IP dos usuários no admin.

Estas são as configurações que modifiquei:

  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

EDIT: Acabei de verificar o conteúdo da resposta de uma das requisições falhadas e notei que mencionava 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>\n<center>nginx</center>\r\n</body>\r\n</html>\r\n

Vou investigar mais sobre os tópicos que mencionam nginx.

1 curtida

As duas seções relevantes da configuração do nginx parecem ser:

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;
}

e

  location @discourse {
add_header Strict-Transport-Security 'max-age=31536000'; # lembre-se do certificado por um ano e conecte-se automaticamente ao HTTPS para este domínio
  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;
  }
}

Minhas perguntas restantes agora são:

  • Devo editar ambas as seções para corresponder às minhas configurações do Discourse? Ou apenas os valores para location @discourse?

  • Qual é a maneira correta de modificar esses valores e persistir as alterações em reconstruções?
    Eu assumo que posso editar a configuração do nginx diretamente no contêiner e, em seguida, parar/iniciar o contêiner. Mas parece que esses valores vieram originalmente de templates/web.ratelimited.template.yml e podem ser substituídos em uma reconstrução?

Muito obrigado pela sua ajuda! :pray:

1 curtida

Oof, agora estamos saindo da minha zona de conforto, receio.

Se você está sendo limitado pela taxa do nginx, então sim, mexer nessas configurações e torná-las menos restritivas faz sentido. Não tenho certeza se o Nginx pode adicionar endereços IP a uma lista de permissões?

Algo como

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

e então envolva os limites em um if

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

Sim, você deve fazer algumas substituições com pups durante a compilação para tornar isso persistente, veja, por exemplo, web.ssl.template.yml sobre como abordar isso.

Ou você pode esquecer isso e fazer seu script cliente de API rodar mais devagar inserindo alguns sleeps em locais estratégicos. ← abordagem recomendada

4 curtidas

Como em um rescue quando ele atinge o limite de taxa. É o que eu gosto de pensar que geralmente faço.

1 curtida

Qual é a taxa sustentada máxima de solicitações de API por minuto ou segundo na instalação padrão?

Tentei uma por segundo e parece ter atingido um limite. Enquanto 1 solicitação a cada 7 segundos funciona bem.