Come evitare limiti di throttling con chiave API admin?

Ricevo il messaggio “429 Too Many Requests” per le richieste API alla mia istanza self-hosted anche con:

  • DISCOURSE_MAX_ADMIN_API_REQS_PER_MINUTE aumentato a 600
    • L’ho impostato nella sezione env di app.yml e poi ho eseguito ./launcher rebuild e ho confermato che la variabile era impostata nel container ricostruito.
    • questo è ben al di sopra del numero di richieste al minuto che sto tentando
  • una chiave API admin senza restrizioni

Sembra che questo sia stato discusso in precedenza senza una risposta chiara sul perché la modifica di DISCOURSE_MAX_ADMIN_API_REQS_PER_MINUTE non sembri funzionare:

Come posso assicurarmi che le richieste API con una chiave/utente admin non siano soggette a throttling?

Ciao @aas,

Potresti fornire un po’ di contesto?

  • Quante richieste API stai effettuando? Al secondo, al minuto, all’ora, al giorno?
  • Sei sicuro di utilizzare una chiave API Admin?
  • Provengono tutte dallo stesso indirizzo IP? Forse a causa di un reverse proxy?

Potrebbe essere nginx o un altro software a causarti questo errore?

1 Mi Piace

Ciao @Bas,

Mi scuso per la risposta tardiva!

Ora sto esaminando nuovamente questo problema poiché abbiamo lanciato un’integrazione Discourse e vogliamo assicurarci di non incontrare problemi relativi ai limiti di frequenza.

L’ho testata con una nuova chiave per assicurarmi che non fosse limitata in alcun modo. Per essere chiari, cosa intendi esattamente con chiave API admin?

Ho creato una chiave con le seguenti impostazioni:

Dice: “La chiave API non ha restrizioni e tutti gli endpoint sono accessibili.”

Sto testando questo effettuando richieste API da una shell Python locale, quindi provengono dallo stesso indirizzo IP. Abbiamo anche riscontrato i limiti di frequenza durante l’esecuzione di uno script sul nostro server. In quel caso, tutte le richieste provenivano dallo stesso indirizzo IP.

Ho confermato che il limite di frequenza viene raggiunto con il seguente codice:

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

# Ottieni solo una porzione di 15 degli argomenti in topic_ids per il test.
topics = asyncio.run(get_topic_post_streams(topic_ids[:15]))

Nota che il parametro max_per_second è commentato, il che non impone limiti al numero di richieste.

Questo viene completato in 2,05 secondi e 2 delle 15 richieste restituiscono 429.

Quando lo eseguo con max_per_second=1, tutto viene completato con successo.

Fammi sapere se posso fornire ulteriori dettagli. Grazie!

@Bas, ecco l’equivalente Javascript del codice Python per renderlo più facile da riprodurre utilizzando la console degli strumenti per sviluppatori:

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

// Non limitare la velocità delle richieste e vedi che ottieni due 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)); // Ritardo di 1 secondo
    }
    return results;
}

// 1 richiesta al secondo restituisce tutti i 200
(async () => {
    const topics = await getTopicPostStreamsRateLimited(topicIds.slice(0, 15));
    console.log(topics);
})();
1 Mi Piace

Se dovessi fare un’ipotesi, questo è molto probabilmente il problema. Non stai raggiungendo il limite di frequenza sull’API, ma sulla base del tuo IP.

Potresti consultare le impostazioni per IP qui: Available settings for global rate limits and throttling - #27

Fammi sapere se questo risolve effettivamente il tuo problema :slight_smile:

1 Mi Piace

Grazie, @Bas!

Mi sembra di non dover ricevere questi 429 indipendentemente da qualsiasi impostazione menzionata in quel post. Nell’esempio che ho fornito, ho inviato 15 richieste che rientrano in tutti i limiti predefiniti dell’API. L’ho fatto usando una chiave API e un nome utente amministratore.

L’esempio non supera i seguenti valori predefiniti per IP:

Non supera nemmeno i limiti per i non amministratori:

La modifica di DISCOURSE_MAX_REQS_PER_IP_MODE in warn o none non ha aiutato.

Mi sfugge qualcosa? :thinking:

A proposito, ho modificato le impostazioni modificando app.yml ed eseguendo ./launcher destroy app && ./launcher start app.

Posso vedere in /var/log/nginx/access.log che l’indirizzo IP è corretto, quindi non credo che Discourse consideri tutte le richieste provenienti dallo stesso IP.

Posso anche vedere gli indirizzi IP degli utenti nell’area admin.

Queste sono le impostazioni che ho modificato:

  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: Ho appena controllato il contenuto della risposta di una delle richieste fallite e ho notato che menzionava 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

Farò ulteriori indagini sugli argomenti che menzionano nginx.

1 Mi Piace

Le due sezioni pertinenti della configurazione di nginx sembrano essere:

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'; # ricorda il certificato per un anno e connettiti automaticamente a HTTPS per questo dominio
  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;
  }
}

Le mie restanti domande sono:

  • Dovrei modificare entrambe le sezioni per farle corrispondere alle mie impostazioni di Discourse? O solo i valori per location @discourse?

  • Qual è il modo corretto per modificare questi valori e mantenere le modifiche dopo le ricostruzioni?
    Presumo di poter modificare direttamente la configurazione di nginx nel container e poi fermare/avviare il container. Ma sembra che questi valori provengano originariamente da templates/web.ratelimited.template.yml e possano essere sovrascritti durante una ricostruzione?

Grazie mille per il tuo aiuto! :pray:

1 Mi Piace

Mi dispiace, ora stiamo uscendo dalla mia zona di comfort.

Se sei limitato dalla frequenza di nginx, allora sì, armeggiare con quelle impostazioni e renderle meno restrittive ha senso. Non sono sicuro se Nginx possa creare una whitelist di indirizzi IP?

Qualcosa come

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

e poi racchiudere i limiti in 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;
        }

Sì, dovresti fare alcune sostituzioni con i pups durante la build per rendere questo persistente, vedi ad esempio web.ssl.template.yml su come procedere.
Oppure potresti dimenticarti di questo e far eseguire al tuo script client API più lentamente inserendo alcuni sleep in punti strategici. ← approccio consigliato

4 Mi Piace

Come in un rescue quando viene limitato nella frequenza. È quello che mi piace pensare di fare di solito.

1 Mi Piace

Qual è la frequenza massima sostenuta di richieste API al minuto o al secondo nell’installazione standard?

Ho provato una richiesta al secondo e sembra aver raggiunto un limite. Mentre 1 richiesta ogni 7 secondi funziona bene.