Comment éviter les limites d'étranglement avec la clé API admin ?

Je reçois le message « 429 Trop de requêtes » pour les requêtes API vers mon instance auto-hébergée, même avec :

  • DISCOURSE_MAX_ADMIN_API_REQS_PER_MINUTE augmenté à 600
    • Je l’ai défini dans la section env de app.yml, puis j’ai exécuté ./launcher rebuild et j’ai confirmé que la variable était définie dans le conteneur reconstruit.
    • C’est bien plus que le nombre de requêtes par minute que je tente.
  • une clé API administrateur sans restriction

Il semble que cela ait déjà été discuté sans réponse claire quant à la raison pour laquelle la modification de DISCOURSE_MAX_ADMIN_API_REQS_PER_MINUTE ne semble pas fonctionner :

Comment puis-je m’assurer que les requêtes API avec une clé/un utilisateur administrateur ne sont pas soumises à une limitation de débit ?

Salut @aas,

Pourriez-vous donner un peu de contexte ?

  • Combien de requêtes API faites-vous ? Par seconde, minute, heure, par jour
  • Êtes-vous sûr d’utiliser une clé d’API d’administrateur ?
  • Proviennent-elles toutes de la même adresse IP ? Peut-être à cause d’un proxy inverse ?

Est-ce que nginx ou un autre logiciel pourrait vous causer cette erreur ?

1 « J'aime »

Salut @Bas,

Désolé pour ma réponse tardive !

Je me penche à nouveau sur ce problème car nous avons lancé une intégration Discourse et nous voulons nous assurer de ne rencontrer aucun problème lié aux limites de débit.

Je l’ai testé avec une nouvelle clé pour m’assurer qu’elle n’est limitée d’aucune façon. Pour être clair, que voulez-vous dire exactement par clé API d’administrateur ?

J’ai créé une clé avec les paramètres suivants :

Il est indiqué : « La clé API n’a aucune restriction et tous les points d’accès sont accessibles ».

Je teste cela en effectuant des requêtes API depuis un shell Python local, donc elles proviennent de la même adresse IP. Nous avons également rencontré les limites de débit lors de l’exécution d’un script sur notre serveur. Dans ce cas, toutes les requêtes provenaient de la même adresse IP.

J’ai confirmé que la limite de débit est atteinte avec le code suivant :

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

# Obtenez simplement une tranche de 15 des sujets dans topic_ids pour le test.
topics = asyncio.run(get_topic_post_streams(topic_ids[:15]))

Notez que le paramètre max_per_second est commenté, ce qui n’impose aucune limite au nombre de requêtes.

Cela se termine en 2,05 s et 2 des 15 requêtes retournent 429.

Lorsque je l’exécute avec max_per_second=1, tout se termine avec succès.

Faites-moi savoir si je peux fournir plus de détails. Merci !

@Bas, voici l’équivalent Javascript du code Python pour faciliter la reproduction à l’aide de la console des outils de développement :

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

// Ne limitez pas les requêtes et voyez que vous obtenez deux 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)); // Délai de 1 seconde
    }
    return results;
}

// 1 requête par seconde renvoie tous les 200.
(async () => {
    const topics = await getTopicPostStreamsRateLimited(topicIds.slice(0, 15));
    console.log(topics);
})();
1 « J'aime »

Si je devais faire une supposition, c’est très probablement le problème. Vous n’êtes pas limité par l’API, mais sur la base de votre IP.

Vous pourriez examiner les paramètres par IP ici : Available settings for global rate limits and throttling - #27

Faites-moi savoir si cela résout réellement votre problème :slight_smile:

1 « J'aime »

Merci, @Bas !

Il me semble que je ne devrais pas recevoir ces 429, quels que soient les paramètres mentionnés dans ce post. Dans l’exemple que j’ai fourni, j’ai envoyé 15 requêtes, ce qui est inférieur à toutes les limites par défaut de l’API. Je l’ai fait en utilisant une clé d’API et un nom d’utilisateur d’administrateur.

L’exemple ne dépasse pas les valeurs par défaut par adresse IP :

Il ne dépasse même pas les limites non administratives :

Changer DISCOURSE_MAX_REQS_PER_IP_MODE en warn ou none n’a pas aidé.

Est-ce que je manque quelque chose ? :thinking:

Au fait, j’ai modifié les paramètres en éditant app.yml et en exécutant ./launcher destroy app && ./launcher start app.

Je peux voir dans /var/log/nginx/access.log que l’adresse IP est correcte, donc je ne pense pas que Discourse considère que toutes les requêtes proviennent de la même IP.

Je peux également voir les adresses IP des utilisateurs dans l’administration.

Voici les paramètres que j’ai modifiés :

  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 : Je viens de vérifier le contenu de la réponse d’une des requêtes échouées et j’ai remarqué qu’elle mentionnait 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

Je vais enquêter davantage sur les sujets qui mentionnent nginx.

1 « J'aime »

Les deux sections pertinentes de la configuration nginx semblent être :

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

et

  location @discourse {
add_header Strict-Transport-Security 'max-age=31536000'; # remember the certificate for a year and automatically connect to HTTPS for this domain
  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;
  }
}

Mes questions restantes sont maintenant :

  • Dois-je modifier les deux sections pour qu’elles correspondent à mes paramètres Discourse ? Ou seulement les valeurs pour location @discourse ?

  • Quelle est la bonne façon de modifier ces valeurs et de conserver les modifications lors des reconstructions ?
    Je suppose que je peux modifier la configuration nginx directement dans le conteneur, puis arrêter/démarrer le conteneur. Mais il semble que ces valeurs proviennent à l’origine de templates/web.ratelimited.template.yml et peuvent être écrasées lors d’une reconstruction ?

Merci beaucoup pour votre aide ! :pray:

1 « J'aime »

Ouf, là on sort de ma zone de confort, j’ai bien peur.

Si vous êtes limité par le débit par nginx, alors oui, modifier ces paramètres et les rendre moins restrictifs a du sens. Je ne suis pas sûr que Nginx puisse mettre des adresses IP sur liste blanche ?

Quelque chose comme

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

puis encapsuler les limites dans un if

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

Oui, vous devriez faire quelques remplacements avec des pups lors de la construction pour rendre cela persistant, voir par exemple web.ssl.template.yml pour savoir comment aborder cela.

Ou vous pourriez oublier cela et faire en sorte que votre script client API s’exécute plus lentement en insérant quelques sleep à des endroits stratégiques. ← approche recommandée

4 « J'aime »

Comme dans un rescue lorsqu’il est limité par le débit. C’est ce que j’ai l’habitude de faire, je pense.

1 « J'aime »

Quel est le taux soutenu maximal de requêtes API par minute ou par seconde avec l’installation standard ?

J’ai essayé une requête par seconde et il semble que j’aie atteint une limite. Alors qu’une requête toutes les 7 secondes fonctionne bien.