2009-09-03 11 views
50

Los usuarios legítimos de mi sitio a veces martillan el servidor con solicitudes API que causan resultados no deseados. Quiero establecer un límite de no más de una llamada de API cada 5 segundos o n llamadas por minuto (aún no he calculado el límite exacto). Obviamente, pude registrar todas las llamadas de la API en un DB y hacer el cálculo en cada solicitud para ver si están por encima del límite, pero toda esta carga adicional en CADA solicitud sería contraria al propósito. ¿Cuáles son otros métodos menos intensivos en recursos que podría usar para establecer un límite? Estoy usando PHP/Apache/Linux, por lo que vale.¿Cómo acelero los usuarios de la API de mi sitio?

+0

Se trata sólo de un vendaje, mientras que permite ajustar la API o añadir más servidores? Es muy peligroso quitarle algo a/poner restricciones a los desarrolladores ... –

+11

No, estoy tratando de establecer límites razonables para que el sitio sea sostenible. Agregar capacidad de servidor para algunos usuarios excesivamente celosos no es parte del plan. – scotts

Respuesta

48

Ok, no hay forma de hacer lo que solicité sin cualquier escribe en el servidor, pero al menos puedo eliminar el registro de cada solicitud. Una forma es utilizar el método de aceleración "leaky bucket", donde solo hace un seguimiento de la última solicitud() y una proporción del número de solicitudes/límite para el marco de tiempo ($minute_throttle). El cubo con fugas nunca restablece su contador (a diferencia del acelerador de Twitter API que se restablece cada hora), pero si el depósito se llena (el usuario alcanzó el límite), deben esperar n segundos para que el depósito se vacíe un poco antes de poder realizar otra solicitud . En otras palabras, es como un límite móvil: si hay solicitudes previas dentro del marco de tiempo, se están filtrando lentamente del cubo; solo te restringe si llenas el cubo.

Este fragmento de código calculará un nuevo valor $minute_throttle en cada solicitud. Especifiqué minutos en $minute_throttle porque puede agregar aceleradores para cualquier período de tiempo, como por hora, por día, etc. ... aunque más de uno empezará a confundir rápidamente a los usuarios.

$minute = 60; 
$minute_limit = 100; # users are limited to 100 requests/minute 
$last_api_request = $this->get_last_api_request(); # get from the DB; in epoch seconds 
$last_api_diff = time() - $last_api_request; # in seconds 
$minute_throttle = $this->get_throttle_minute(); # get from the DB 
if (is_null($minute_limit)) { 
    $new_minute_throttle = 0; 
} else { 
    $new_minute_throttle = $minute_throttle - $last_api_diff; 
    $new_minute_throttle = $new_minute_throttle < 0 ? 0 : $new_minute_throttle; 
    $new_minute_throttle += $minute/$minute_limit; 
    $minute_hits_remaining = floor(($minute - $new_minute_throttle) * $minute_limit/$minute ); 
    # can output this value with the request if desired: 
    $minute_hits_remaining = $minute_hits_remaining >= 0 ? $minute_hits_remaining : 0; 
} 

if ($new_minute_throttle > $minute) { 
    $wait = ceil($new_minute_throttle - $minute); 
    usleep(250000); 
    throw new My_Exception ('The one-minute API limit of ' . $minute_limit 
     . ' requests has been exceeded. Please wait ' . $wait . ' seconds before attempting again.'); 
} 
# Save the values back to the database. 
$this->save_last_api_request(time()); 
$this->save_throttle_minute($new_minute_throttle); 
+1

¿Puedes explicar por qué '$ minute_limit' sería nulo? – nrathaus

+0

Creo que debería ser '$ minute_throttle', ya que proviene de DB. – Sljux

+1

¿Has pensado en extraer el RateLimiter en su propia clase? – mblaettermann

1

Usted dice que "todos los gastos indirectos adicionales en CADA solicitud estarían derrotando el propósito", pero no estoy seguro de que sea correcto. ¿No es el propósito de evitar el martilleo de su servidor? Esta es probablemente la forma en que lo implementaría, ya que solo requiere una lectura/escritura rápida. Incluso podría sacrificar las verificaciones del servidor de la API a una base de datos/disco diferente si estuviera preocupado por el rendimiento.

Sin embargo, si desea alternativas, debe consultar mod_cband, un módulo de apache de terceros diseñado para ayudar en la aceleración del ancho de banda. A pesar de ser principalmente para la limitación del ancho de banda, también puede acelerarse en función de las solicitudes por segundo. Nunca lo he usado, así que no estoy seguro de qué tipo de resultados obtendrías. Hubo otro módulo llamado mod-throttle también, pero ese proyecto parece estar cerrado ahora, y nunca fue lanzado para nada por encima de la serie Apache 1.3.

+0

Sí, probablemente tenga que guardar algo en el disco ... preferiblemente no todas las solicitudes de registro. Podría guardar la última solicitud API exitosa y asegurarme de que sea n segundos más tarde que eso. – scotts

3

solución más simple sería simplemente dar a cada clave de API un número limitado de solicitudes por 24 horas, y restablecerlos en algún conocido, fijo, el tiempo.

Si agota sus solicitudes API (es decir, el contador llega a cero, o el límite, dependiendo de la dirección en la que esté contando), deje de mostrarles datos hasta que restablezca su contador.

De esta manera, será en su el mejor interés para no golpearlo con las solicitudes.

1

Además de la implementación desde cero, también puede consultar la infraestructura de API como 3scale (http://www.3scale.net) que limita la velocidad, así como un montón de otras cosas (análisis, etc.). Hay un plugin de PHP para él: https://github.com/3scale/3scale_ws_api_for_php.

También puede pegar algo así como Varnish enfrente de la API y hacer la limitación de la tasa de API de esa manera.

4

No sé si este hilo todavía está vivo o no, pero sugeriría mantener estas estadísticas en la memoria caché como memcached. Esto reducirá la sobrecarga de registrar la solicitud en la base de datos, pero aún sirve para el propósito.

+0

Acepto completamente y lo implementamos así como también es atómico. Puedes usar algo como AWS elasticache para almacenarlos y luego tener un cronjob simplemente registra los resultados agregados luego en una base de datos. En realidad, tenemos una pequeña instancia de memcached por servidor para hacer incrementos y luego lave/incremente esto a elasticache una vez por minuto, de esa forma tampoco moverá el cuello de botella a elasticache. – Ross

+0

@Kedar todavía puede registrar todas las llamadas en un archivo para diferentes tipos de análisis ', lo que no molestaría a su base de datos, simplemente poniendo en cola las escrituras en el búfer del disco. – kommradHomer

+0

¿Redis sería una mejor solución? ¿Está en ram pero también no volátil? – BeardedGeek

7

Puede controlar la frecuencia con el token bucket algorithm, que es comparable al algoritmo del cubo con fugas. Tenga en cuenta que deberá compartir el estado del depósito (es decir, la cantidad de tokens) sobre los procesos (o el alcance que desee controlar). Por lo que es posible que desee pensar en bloquear para evitar las condiciones de carrera.

La buena noticia: Hice todo eso para usted: bandwidth-throttle/token-bucket

use bandwidthThrottle\tokenBucket\Rate; 
use bandwidthThrottle\tokenBucket\TokenBucket; 
use bandwidthThrottle\tokenBucket\storage\FileStorage; 

$storage = new FileStorage(__DIR__ . "/api.bucket"); 
$rate = new Rate(10, Rate::SECOND); 
$bucket = new TokenBucket(10, $rate, $storage); 
$bucket->bootstrap(10); 

if (!$bucket->consume(1, $seconds)) { 
    http_response_code(429); 
    header(sprintf("Retry-After: %d", floor($seconds))); 
    exit(); 
} 
+0

Gracias por el enlace al algoritmo del cubo de fichas: sin eso no me habría dado cuenta de que el cubo con fugas era un algoritmo de buena fe. – Colin

Cuestiones relacionadas