Cómo Manejar Tráfico Intermitente: Algoritmos de Leaky Bucket y Token Bucket
API7.ai
January 5, 2023
En los artículos anteriores, aprendimos sobre la optimización de código y el diseño de caché, que están estrechamente relacionados con el rendimiento general de la aplicación y merecen nuestra atención. Sin embargo, en escenarios comerciales reales, también debemos considerar el impacto del tráfico repentino en el rendimiento. El tráfico repentino aquí puede ser normal, como el tráfico de noticias de última hora, promociones, etc., o tráfico anormal, como ataques DDoS.
OpenResty se utiliza principalmente como una capa de acceso para aplicaciones web como WAFs y API gateways, que tienen que manejar el tráfico repentino normal y anormal mencionado anteriormente. Después de todo, si no puedes manejar el tráfico repentino, los servicios de back-end pueden verse fácilmente afectados y el negocio no responderá adecuadamente. Así que hoy, veremos formas de manejar el tráfico repentino.
Control de Tráfico
El control de tráfico es una característica imprescindible para WAF (Web Application Firewall) y API gateways. Asegura que los servicios upstream puedan funcionar adecuadamente canalizando y controlando el tráfico de entrada a través de algunos algoritmos, manteniendo así el sistema saludable.
Dado que la capacidad de procesamiento del back-end es limitada, necesitamos considerar múltiples aspectos como el costo, la experiencia del usuario y la estabilidad del sistema. No importa qué algoritmo se utilice, inevitablemente causará que las solicitudes normales de los usuarios se ralenticen o incluso se rechacen, sacrificando parte de la experiencia del usuario. Por lo tanto, necesitamos controlar el tráfico mientras equilibramos la estabilidad del negocio y la experiencia del usuario.
De hecho, hay muchos casos de "control de tráfico" en la vida real. Por ejemplo, durante el período de viajes del Año Nuevo Chino, el flujo de personas se congestiona en estaciones de metro, estaciones de tren, aeropuertos y otros centros de transporte porque la capacidad de manejo de estos vehículos es limitada. Por lo tanto, los pasajeros deben esperar en fila y entrar en la estación en lotes para garantizar su seguridad y el funcionamiento regular del tráfico.
Esto naturalmente afecta la experiencia del pasajero, pero en general, asegura el funcionamiento eficiente y seguro del sistema. Por ejemplo, si no hubiera filas y lotes, sino que se permitiera a todos entrar en la estación en masa, el resultado sería que todo el sistema colapsaría.
Volviendo a la tecnología, por ejemplo, supongamos que un servicio upstream está diseñado para manejar 10,000 solicitudes por minuto. En momentos pico, si no hay control de flujo en el punto de entrada y la pila de tareas alcanza 20,000 por minuto, el rendimiento de procesamiento de este servicio upstream se degradará a quizás solo 5,000 solicitudes por minuto y continuará deteriorándose, quizás eventualmente llevando a la indisponibilidad del servicio. Este no es el resultado que queremos ver.
Los algoritmos comunes de control de tráfico que utilizamos para hacer frente a este tráfico repentino son el algoritmo del cubo con fugas y el algoritmo del cubo de tokens.
Algoritmo del Cubo con Fugas
Comencemos por ver el algoritmo del cubo con fugas, que tiene como objetivo mantener una tasa constante de solicitudes y suavizar los picos de tráfico. Pero, ¿cómo se logra? Primero, veamos la siguiente abstracción conceptual de la introducción de Wikipedia al algoritmo del cubo con fugas.
Podemos imaginar el tráfico del cliente como agua que fluye desde una tubería con una tasa de flujo incierta, a veces rápida y a veces lenta. El módulo de procesamiento de tráfico externo, que es el cubo que recibe el agua, tiene un agujero en la parte inferior para la fuga. Este es el origen del nombre del algoritmo del cubo con fugas, que tiene los siguientes beneficios:
Primero, ya sea que el flujo hacia el cubo sea un goteo o una inundación monstruosa, se garantiza que la tasa de agua que fluye fuera del cubo sea constante. Este tráfico constante es amigable para los servicios upstream, que es de lo que se trata el modelado de tráfico.
Segundo, el cubo en sí tiene un cierto volumen y puede acumular una cierta cantidad de agua. Esto equivale a que las solicitudes del cliente puedan ponerse en cola si no se pueden procesar de inmediato.
Tercero, el agua que excede el volumen del cubo no será aceptada por el cubo, sino que fluirá lejos. La metáfora correspondiente aquí es que si hay demasiadas solicitudes del cliente que exceden la longitud de la cola, se devolverá directamente un mensaje de falla al cliente. En este momento, el lado del servidor no puede manejar tantas solicitudes y la cola se vuelve innecesaria.
Entonces, ¿cómo debería implementarse este algoritmo? Tomemos como ejemplo la biblioteca resty.limit.req
que viene con OpenResty. Es un módulo de límite de tasa implementado por el algoritmo del cubo con fugas. Hablaremos más sobre él en el siguiente artículo. Hoy comenzaremos con un breve vistazo a las siguientes líneas de código, que son las clave:
local elapsed = now - tonumber(rec.last)
excess = max(tonumber(rec.excess) - rate * abs(elapsed) / 1000 + 1000,0)
if excess > self.burst then
return nil, "rejected"
end
-- return the delay in seconds, as well as excess
return excess / rate, excess / 1000
Leamos brevemente estas líneas de código. Donde elapsed
es el número de milisegundos entre la solicitud actual y la última, y rate
es la tasa que establecemos por segundo. Dado que la unidad más pequeña de rate
es 0.001 s/r, el código implementado anteriormente necesita multiplicarse por 1000
para calcularlo.
excess
indica el número de solicitudes que aún están en la cola, 0
significa que el cubo está vacío, no hay solicitudes en la cola, y burst
se refiere al volumen de todo el cubo. Si excess
es mayor que burst
, significa que el cubo está lleno, y el tráfico que entra se descartará directamente; si excess
es mayor que 0
y menor que burst
, entrará en la cola para esperar el procesamiento, y el excess/rate
devuelto aquí es el tiempo de espera.
De esta manera, podemos controlar la longitud de la cola del tráfico repentino ajustando el tamaño de burst
, mientras que la capacidad de procesamiento del servicio de back-end permanece sin cambios. Pero, por supuesto, depende de tu escenario comercial si le dices al usuario que hay demasiadas solicitudes y que intente más tarde, o dejas que el usuario espere por un período más largo.
Algoritmo del Cubo de Tokens
Tanto el algoritmo del cubo de tokens como el algoritmo del cubo con fugas tienen el mismo propósito, asegurar que los servicios de back-end no sean golpeados por picos de tráfico, aunque los dos no se implementan de la misma manera.
El algoritmo del cubo con fugas utiliza la IP del extremo para hacer los fundamentos de tráfico y límite de tasa. De esta manera, la tasa de salida del algoritmo del cubo con fugas de cada cliente es fija. Sin embargo, esto plantea un problema:
Supongamos que las solicitudes del usuario A
son frecuentes y las solicitudes de otros usuarios son infrecuentes. En ese caso, el algoritmo del cubo con fugas ralentizará o rechazará algunas de las solicitudes de A
, incluso si el servicio puede manejarlas en ese momento, aunque la presión general del servicio no sea muy alta.
Aquí es donde el cubo de tokens resulta útil.
Mientras que el algoritmo del cubo con fugas se preocupa por suavizar el tráfico, el cubo de tokens permite que los picos de tráfico entren en el servicio de back-end. El principio del cubo de tokens es poner tokens en el cubo a una tasa fija y seguir poniéndolos siempre que el cubo no esté lleno. De esta manera, todas las solicitudes que vienen del extremo necesitan ir al cubo de tokens para obtener el token primero antes de que el back-end pueda procesarlas; si no hay tokens dentro del cubo, entonces la solicitud será rechazada.
Sin embargo, OpenResty no implementa cubos de tokens para limitar el tráfico y la tasa en su biblioteca. Así que aquí hay una breve introducción al módulo de límite de tasa basado en cubos de tokens lua-resty-limit-rate
, que es de código abierto por UPYUN, como ejemplo:
local limit_rate = require "resty.limit.rate"
-- global 20r/s 6000r/5m
local lim_global = limit_rate.new("my_limit_rate_store", 100, 6000, 2)
-- single 2r/s 600r/5m
local lim_single = limit_rate.new("my_limit_rate_store", 500, 600, 1)
local t0, err = lim_global:take_available("__global__", 1)
local t1, err = lim_single:take_available(ngx.var.arg_userid, 1)
if t0 == 1 then
return -- global bucket is not hungry
else
if t1 == 1 then
return -- single bucket is not hungry
else
return ngx.exit(503)
end
end
En este código, configuramos dos cubos de tokens: un cubo de tokens global y un cubo de tokens con ngx.var.arg_userid
como key
, dividido por usuario. Hay una combinación de los dos cubos de tokens, que tiene el siguiente beneficio principal:
- No tener que determinar el cubo de tokens del usuario si todavía hay tokens en el cubo de tokens global, y servir tantas solicitudes repentinas de los usuarios como sea posible si el servicio de back-end puede funcionar correctamente.
- En ausencia de un cubo de tokens global, las solicitudes no pueden rechazarse indiscriminadamente, por lo que es necesario determinar el cubo de tokens de usuarios individuales y rechazar las solicitudes de usuarios con más solicitudes repentinas. De esta manera, se asegura que las solicitudes de otros usuarios no se vean afectadas.
Obviamente, los cubos de tokens son más flexibles que los cubos con fugas, permitiendo situaciones en las que los picos de tráfico se pasan a los servicios de back-end. Pero, por supuesto, ambos tienen sus pros y sus contras, y puedes elegir usarlos según tu situación.
Módulo de Límite de Tasa de NGINX
Con estos dos algoritmos fuera del camino, finalmente veamos cómo implementar un límite de tasa en NGINX. En NGINX, el módulo limit_req
es el módulo de límite de tasa más comúnmente utilizado, y la siguiente es una configuración simple:
limit_req_zone $binary_remote_addr zone=one:10m rate=1r/s;
server {
location /search/ {
limit_req zone=one burst=5;
}
}
Este código toma la dirección IP del cliente como key
, solicita un espacio de dirección de memoria de 10M
llamado one
, y limita la tasa a 1
solicitud por segundo.
En la ubicación del servidor, también se hace referencia a la regla de límite de tasa one
, y se establece brust
en 5
. Si la tasa excede 1r/s, 5
solicitudes pueden ponerse en cola simultáneamente, dando un cierto área de amortiguación. Si brust
no se establece, las solicitudes que exceden la tasa se rechazarán directamente.
Este módulo de NGINX se basa en un cubo con fugas, por lo que es esencialmente lo mismo que resty.limit.req
en OpenResty, que describimos anteriormente.
Resumen
El mayor problema de los límites de tasa en NGINX es que no se pueden modificar dinámicamente. Después de todo, necesitas reiniciar el archivo de configuración después de modificarlo para que sea efectivo, lo cual es inaceptable en un entorno que cambia rápidamente. Por lo tanto, el siguiente artículo verá cómo implementar límites de tráfico y tasa dinámicamente en OpenResty.
Finalmente, consideremos una pregunta. Desde la perspectiva de WAF y API gateways, ¿hay una mejor manera de identificar qué son solicitudes normales de usuarios y cuáles son maliciosas? Porque, para el tráfico repentino de usuarios normales, podemos escalar rápidamente los servicios de back-end para aumentar la capacidad del servicio, mientras que para las solicitudes maliciosas, es mejor rechazarlas directamente en la capa de acceso.
Eres bienvenido a compartir este artículo con tus colegas y amigos para aprender y progresar juntos.