¿Cómo evitar los límites de limitación con la clave API de administrador?

Estoy recibiendo el mensaje “429 Demasiadas solicitudes” para las solicitudes de API a mi instancia autoalojada, incluso con:

  • DISCOURSE_MAX_ADMIN_API_REQS_PER_MINUTE aumentado a 600
    • Lo configuré en la sección env de app.yml y luego ejecuté ./launcher rebuild y confirmé que la variable estaba configurada en el contenedor reconstruido.
    • Esto está muy por encima del número de solicitudes por minuto que estoy intentando.
  • una clave de API de administrador sin restricciones.

Parece que esto ya se ha discutido sin una respuesta clara sobre por qué cambiar DISCOURSE_MAX_ADMIN_API_REQS_PER_MINUTE no parece funcionar:

¿Cómo puedo asegurarme de que las solicitudes de API con una clave/usuario de administrador no estén sujetas a limitación?

Hola @aas,

¿Podrías dar algo de contexto?

  • ¿Cuántas solicitudes de API estás haciendo? ¿Por segundo, minuto, hora, por día?
  • ¿Estás seguro de que estás usando una clave de API de administrador?
  • ¿Todas estas provienen de la misma dirección IP? ¿Quizás debido a un proxy inverso?

¿Podría ser nginx u otro software el que te esté dando ese error?

1 me gusta

Hola @Bas,

¡Disculpas por la respuesta tardía!

Ahora estoy revisando esto nuevamente, ya que hemos lanzado una integración de Discourse y queremos asegurarnos de no encontrar ningún problema relacionado con los límites de velocidad.

Lo probé con una nueva clave para asegurarme de que no esté limitada de ninguna manera. Para ser claro, ¿qué significa exactamente una clave de API de administrador?

Creé una clave con la siguiente configuración:

Dice: “La clave de API no tiene restricciones y todos los puntos finales son accesibles”.

Estoy probando esto haciendo solicitudes a la API desde un shell de Python local, por lo que provienen de la misma dirección IP. También nos encontramos con los límites de velocidad al ejecutar un script en nuestro servidor. En ese caso, todas las solicitudes provenían de la misma dirección IP.

Confirmé que el límite de velocidad se alcanza con el siguiente 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

# Solo obtén una porción de 15 de los temas en topic_ids para probar.
topics = asyncio.run(get_topic_post_streams(topic_ids[:15]))

Tenga en cuenta que el parámetro max_per_second está comentado, lo que resulta en que no haya límites en el número de solicitudes.

Esto se completa en 2.05 s y 2 de las 15 solicitudes devuelven 429.

Cuando lo ejecuto con max_per_second=1, todo se completa con éxito.

Avísame si puedo proporcionar más detalles. ¡Gracias!

@Bas, aquí tienes el equivalente en javascript del código python para que sea más fácil de reproducir usando la consola de herramientas de desarrollador:

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

// No limites la tasa de las solicitudes y comprueba que obtienes dos 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)); // Retraso de 1 segundo
    }
    return results;
}

// 1 solicitud por segundo devuelve todos los 200
(async () => {
    const topics = await getTopicPostStreamsRateLimited(topicIds.slice(0, 15));
    console.log(topics);
})();
1 me gusta

Si tuviera que arriesgar una suposición, este es muy probablemente el problema. No se le está limitando la velocidad en la API, sino en función de su IP.

Podría consultar la configuración por IP aquí: Available settings for global rate limits and throttling - #27

Por favor, hágame saber si eso realmente resuelve su problema :slight_smile:

1 me gusta

Gracias, @Bas!

Me parece que no debería estar recibiendo estos 429, independientemente de la configuración mencionada en esa publicación. En el ejemplo que proporcioné, envié 15 solicitudes, lo cual está por debajo de todos los límites predeterminados de la API. Lo hice usando una clave de API y un nombre de usuario de administrador.

El ejemplo no excede los siguientes valores predeterminados por IP:

Ni siquiera excede los límites para no administradores:

Cambiar DISCOURSE_MAX_REQS_PER_IP_MODE a warn o none no ayudó.

¿Me estoy perdiendo algo? :thinking:

Por cierto, cambié la configuración editando app.yml y ejecutando ./launcher destroy app && ./launcher start app.

Puedo ver en /var/log/nginx/access.log que la dirección IP es correcta, así que no creo que Discourse considere que todas las solicitudes provienen de la misma IP.

También puedo ver las direcciones IP de los usuarios en el panel de administración.

Estas son las configuraciones que he modificado:

  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

EDITAR: Acabo de revisar el contenido de la respuesta de una de las solicitudes fallidas y noté que mencionaba 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

Investigaré más sobre los temas que mencionan nginx.

1 me gusta

Las dos secciones relevantes de la configuración de nginx parecen 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;
}

y

  location @discourse {
add_header Strict-Transport-Security 'max-age=31536000'; # recuerda el certificado durante un año y conéctate automáticamente a HTTPS para este 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;
  }
}

Mis preguntas restantes ahora son:

  • ¿Debo editar ambas secciones para que coincidan con mi configuración de Discourse? ¿O solo los valores para location @discourse?

  • ¿Cuál es la forma correcta de modificar estos valores y mantener los cambios en futuras reconstrucciones?
    Supongo que puedo editar la configuración de nginx directamente en el contenedor y luego detener/iniciar el contenedor. Pero parece que estos valores originalmente provinieron de templates/web.ratelimited.template.yml y pueden ser sobrescritos en una reconstrucción.

¡Muchas gracias por tu ayuda! :pray:

1 me gusta

Me temo que esto está fuera de mi zona de confort.

Si estás siendo limitado por nginx, entonces sí, tiene sentido jugar con esas configuraciones y hacerlas menos restrictivas. ¿No estoy seguro si Nginx puede poner en lista blanca direcciones IP?

Algo como

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

y luego envolver los límites en 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í, deberías hacer algunos reemplazos con pups durante la compilación para que esto sea persistente, consulta por ejemplo web.ssl.template.yml sobre cómo abordarlo.

O podrías olvidarte de esto y hacer que tu script cliente de API se ejecute más lentamente insertando algunas pausas en lugares estratégicos. ← enfoque recomendado

4 Me gusta

Como en un rescue cuando se limita la velocidad. Eso es lo que me gusta pensar que suelo hacer.

1 me gusta

¿Cuál es la tasa máxima sostenida de solicitudes de API por minuto o segundo en la instalación estándar?

Intenté una por segundo y parece que he alcanzado un límite. Mientras que 1 solicitud cada 7 segundos funciona bien.