Read in English · Lire en français · Auf Deutsch lesen
Sección 9.3 de 9.8 · Intermedio · 29 min de lectura
Asincronía, colas y back pressure
Videolección
5 min · narrado en inglés, con subtítulosSolo escuchar (audio)
Leer la transcripción
Te damos la bienvenida a la Sección nueve punto tres. La carga nunca llega de forma uniforme. Esta lección trata de lo que debe hacer un sistema cuando el trabajo llega más rápido de lo que puede hacerse.
Piensa en una panadería concurrida a la hora de abrir. Reparte tickets numerados, así que los clientes esperan en una fila ordenada y los panaderos trabajan a un ritmo constante. Eso es una cola. Pero una fila solo puede ser tan larga antes de que se acabe el pan, así que una buena panadería deja de repartir tickets cuando la fila es demasiado larga. Atiende primero los encargos y limita a cada cliente a un número razonable de barras. Y dice con franqueza a los que llegan tarde que vuelvan en diez minutos, en lugar de dejarlos esperar para nada.
Aquí la API de pedidos pone trabajo en una cola, y varios workers idénticos lo van sacando. Cada mensaje va solo a un worker. Se llaman consumidores competidores. Cuando las llegadas se disparan por encima de lo que pueden gestionar los consumidores, el trabajo sobrante espera en la cola como un backlog. Los workers siguen a su ritmo constante y vacían el backlog después del pico. Así que los consumidores pueden dimensionarse para la media, no para el pico. El precio es el retraso. Un backlog es simplemente una espera que sufrirá cada elemento que quede detrás. Está bien para un correo electrónico, pero es inaceptable para un pago que el cliente está mirando.
Si un worker se cae antes de confirmar un mensaje, otro worker lo recibe. Así que la entrega es al menos una vez, y los consumidores deben ser idempotentes: procesar un mensaje dos veces debe ser seguro. Una cola absorbe un exceso temporal, nunca uno permanente. Una cola sin límite crece hasta agotar la memoria, o retiene trabajo durante horas después de que a nadie le importe. Así que da un límite a cada cola. La ley de Little convierte la espera que estás dispuesto a aceptar en una longitud máxima. Y mide la antigüedad del mensaje más antiguo además de la longitud. La antigüedad es lo que experimentan los usuarios.
Vamos a dimensionar una cola. Las entradas para un concierto salen a la venta a las diez en punto. Cuarenta workers tardan cada uno doscientos cincuenta milisegundos por pedido, así que procesan ciento sesenta pedidos por segundo. La previsión es de seiscientos por segundo durante dos minutos. Eso deja cincuenta y dos mil ochocientos pedidos en espera, y el último espera cinco minutos y medio. La promesa era una respuesta en menos de dos minutos. Para cumplir esa promesa, la ley de Little dice que la cola puede contener como máximo ciento sesenta por ciento veinte: diecinueve mil doscientos pedidos. ¿Más workers? El proveedor de pagos solo permite trescientas solicitudes por segundo, así que setenta y cinco workers como máximo. La espera pasa a ser exactamente de dos minutos. Se cumple por muy poco, sin ningún margen. Pero hay veinticinco mil entradas, a dos y media por pedido: solo diez mil pedidos pueden tener éxito. Poner el resto en cola solo retrasa lo inevitable: agotado. Así que el equipo usa setenta y cinco workers, una sala de espera y una cola limitada a doce mil: las existencias más un veinte por ciento por los pagos fallidos. A partir de ahí, los nuevos pedidos reciben de inmediato la respuesta de agotado.
El back pressure es cualquier forma en que una etapa más lenta hace que una más rápida vaya más despacio. Aquí el productor envía quinientos elementos por segundo, el consumidor procesa trescientos y la cola acotada está llena. Puedes bloquear al productor, de modo que espere a que haya espacio. Eso es adecuado para pipelines y trabajos por lotes. Puedes rechazar el trabajo nuevo, fallando rápido con un cuatrocientos veintinueve o un quinientos tres, para que el cliente pueda reintentar más tarde. O puedes descartar elementos, como los más antiguos o los de menor prioridad. Cada opción es la correcta en algún caso. El error es no elegir.
Un servidor que lo acepta todo en una sobrecarga gasta toda su capacidad en solicitudes cuyos clientes ya se han rendido. El goodput, las solicitudes completadas a tiempo para ser útiles, se desploma hacia cero. El descarte de carga rechaza algunas solicitudes de forma deliberada. Debe ser temprano y barato, antes de cualquier trabajo costoso. Debe estar priorizado. En la plataforma de reparto de comida, los pagos y las ubicaciones de los repartidores tienen prioridad sobre las recomendaciones, que a su vez la tienen sobre los eventos de analítica. Y debe ser consciente del tiempo: una solicitud que ha esperado más que el timeout de quien la llama debe descartarse, no procesarse.
La limitación de tasa reparte la capacidad de forma equitativa entre los clientes. El algoritmo habitual es el token bucket. Esta cubeta contiene como máximo veinte tokens y se recarga a cinco tokens por segundo. Cada solicitud, contada por clave de API, debe tomar un token. Si queda un token, la solicitud se reenvía. Si no, la respuesta es un cuatrocientos veintinueve, Too Many Requests, idealmente con una cabecera Retry-After. Así, un cliente inactivo puede enviar una ráfaga: de treinta solicitudes a la vez, veinte se reenvían y diez se rechazan. Pero en cualquier periodo de sesenta segundos puede enviar como máximo veinte más cinco por sesenta: trescientas veinte solicitudes.
La sección completa compara los algoritmos de limitación de tasa y trata las colas de mensajes fallidos. A continuación, en la Sección nueve punto cuatro, veremos los patrones que impiden que un fallo se propague. Nos vemos allí.
Lo que necesitas saber
- Una cola entre productor y consumidor convierte un pico en un trabajo acumulado (backlog) que los consumidores vacían a su propio ritmo, lo que permite dimensionarlos para la media; el costo es el retraso de cada elemento que queda detrás del backlog.
- Los consumidores competidores escalan el trabajo en segundo plano añadiendo workers; la entrega es al menos una vez (at-least-once), así que los consumidores deben ser idempotentes, y los timeouts de visibilidad y el prefetch deben ajustarse a los tiempos de procesamiento.
- Una cola no puede absorber un exceso sostenido; limita cada cola con la ley de Little (longitud máxima = tasa de procesamiento × espera aceptable) y monitoriza la antigüedad del mensaje más antiguo además de la longitud.
- El back pressure hace que las etapas más rápidas vayan más despacio; cuando una cola acotada está llena, el sistema debe bloquear al productor, rechazar el trabajo nuevo o descartar parte del trabajo, y debería elegir de forma deliberada.
- El descarte de carga (load shedding) rechaza parte del trabajo pronto y a bajo costo, por prioridad y por antigüedad, para que el goodput se mantenga alto en situaciones de sobrecarga.
- La limitación de tasa (rate limiting) reparte la capacidad por cliente: el token bucket permite ráfagas hasta su capacidad y una tasa de recarga constante, el leaky bucket suaviza la salida, y las ventanas fijas permiten ráfagas dobles en los cambios de ventana.
- Responde a los límites con 429 (o 503 cuando todo el servidor está sobrecargado) más Retry-After, y envía los mensajes que fallan repetidamente a una cola de mensajes fallidos (dead-letter queue) con un responsable y monitorizada.
Las colas convierten los picos en trabajo acumulado
La carga nunca llega de forma uniforme. Los pedidos del almuerzo se concentran a las 12:30, los compradores de entradas pulsan todos el botón a las 10:00:00, y un trabajo por lotes vuelca un millón de eventos a medianoche. Un servicio síncrono debe tener capacidad para el pico más alto o fallar durante él. Poner una cola entre quien produce el trabajo y el servicio que lo realiza cambia la aritmética: durante un pico, el trabajo sobrante espera en la cola, y los consumidores lo procesan a su propio ritmo constante, y se ponen al día cuando el pico ha pasado. El catálogo de patrones de nube de Microsoft llama a esto nivelación de carga basada en colas (queue-based load levelling). La Sección 2.4 mostró por qué se forman colas siempre que las llegadas se concentran; esta sección trata de usarlas de forma deliberada.
El beneficio es real: los consumidores pueden dimensionarse para la media en lugar de para el pico, un consumidor lento o que se reinicia no pierde trabajo, y el productor ya no está acoplado temporalmente al consumidor (Sección 6.5). El precio es que el trabajo ahora se hace más tarde. Una cola convierte un pico en un trabajo acumulado (backlog), y un backlog es simplemente un retraso que sufrirá cada elemento que quede detrás. Eso está bien para enviar correos electrónicos de confirmación o actualizar un índice de búsqueda, es discutible para asignar un repartidor a un pedido, e inaceptable para un pago cuya confirmación espera ver el cliente. Por eso, la primera pregunta de diseño para cualquier cola es: ¿cuánto tiempo puede esperar un elemento y qué ocurre con los elementos que esperarían más?
Una cola que absorbe un pico
Colas de trabajo y consumidores competidores
El uso más habitual es una cola de trabajo con consumidores competidores: varios workers idénticos leen de una misma cola, cada mensaje va solo a uno de ellos, y el rendimiento (throughput) crece añadiendo workers. Es el eje X del cubo de escalado aplicado al trabajo en segundo plano, y escala de forma natural con la señal adecuada: el backlog por worker o la antigüedad del mensaje más antiguo.
La mecánica importa para la corrección. Un worker toma un mensaje, lo procesa y después confirma su recepción; si el worker se cae antes, el intermediario vuelve a hacer visible el mensaje tras un timeout (SQS lo llama visibility timeout, timeout de visibilidad) y otro worker lo procesa. Por tanto, la entrega es al menos una vez, así que los consumidores deben ser idempotentes (Sección 6.4). Un timeout de visibilidad más corto que el tiempo real de procesamiento hace que el mismo mensaje se procese dos veces en paralelo. El orden se pierde en cuanto hay más de un consumidor, salvo que los mensajes se particionen por clave, en cuyo caso el número de particiones limita el número de consumidores útiles. Y el prefetch (que un worker tome varios mensajes a la vez para ahorrarse viajes de ida y vuelta) debe mantenerse bajo para las tareas lentas, o un worker acaparará mensajes que podrían haber procesado otros workers ociosos.
Una cola no crea capacidad
Una cola absorbe un exceso temporal; no puede absorber uno permanente. Si el trabajo llega más rápido de lo que se completa durante el tiempo suficiente, el backlog crece sin límite, y la ley de Little (L = λ × W) dice que el tiempo de espera crece con él. Una cola en memoria sin límite acaba agotando la memoria y haciendo caer el proceso, con lo que se pierde todo lo que contiene; una cola duradera sin límite llena su disco o conserva trabajo durante horas después de que a nadie le importe. Peor aún, la cola oculta el problema: los productores reciben confirmaciones rápidas, las tasas de error se quedan en cero, y el único síntoma es que los resultados llegan cada vez más tarde. Los ingenieros de Amazon describen estos backlogs como potencialmente insuperables, porque un servicio que se queda lo bastante atrás puede gastar toda su capacidad en procesar trabajo que sus clientes abandonaron hace tiempo.
De ahí se siguen dos prácticas. Primero, mide la antigüedad del mensaje más antiguo además de la longitud de la cola: la antigüedad es lo que experimentan los usuarios, y reacciona de inmediato cuando los consumidores se atascan. Segundo, da a cada cola un límite derivado del tiempo de espera que estás dispuesto a aceptar, usando la ley de Little, y decide de antemano qué ocurre cuando se alcanza el límite. Esa decisión es el tema de las tres partes siguientes de esta sección.
Ejemplo práctico
Ejemplo práctico: dimensionar la cola de pedidos para una salida a la venta de entradas
Las entradas para un concierto en un estadio salen a la venta a las 10:00. Los envíos de pedidos van a una cola; los workers autorizan el pago y asignan los asientos, y cada uno tarda 250 ms. La previsión es de 600 envíos por segundo durante los dos primeros minutos. La promesa a los clientes es una respuesta en menos de dos minutos. Hay 25 000 entradas y el pedido medio es de 2.5 entradas.
| Capacidad de los workers con 40 workers | 40 ÷ 0.25 s = 160 pedidos por segundo |
|---|---|
| Backlog tras el pico de dos minutos | (600 − 160) × 120 s = 52 800 pedidos en espera |
| Espera del último pedido de la cola (ley de Little, W = L ÷ λ) | 52 800 ÷ 160 = 330 s, cinco minutos y medio: la promesa se incumple |
| Cola más larga que cumple la promesa | L = λ × W = 160 × 120 = 19 200 pedidos |
| ¿Más workers? | El contrato del proveedor de pagos permite 300 solicitudes por segundo, así que como máximo 75 workers (300 pedidos por segundo). El backlog pasa a ser (600 − 300) × 120 = 36 000 y la espera 36 000 ÷ 300 = 120 s: la promesa se cumple por muy poco, sin ningún margen |
| Lo que permiten las existencias | 25 000 ÷ 2.5 = 10 000 pedidos pueden tener éxito. A 600 por segundo, esa cantidad llega en menos de 17 segundos; poner en cola los otros 62 000 pedidos aproximadamente solo retrasa el inevitable 'agotado' |
| Decisión | 75 workers; una sala de espera delante de la página de pedidos admite a 300 compradores por segundo; la cola de pedidos se limita a 12 000 (las existencias más un 20% por los pagos fallidos); una vez que las reservas cubren las existencias, los nuevos envíos reciben de inmediato la respuesta 'agotado, únete a la lista de espera' en lugar de entrar en la cola |
La ley de Little convierte una promesa sobre el tiempo de espera en una longitud máxima de cola. Cualquier cola a la que se deje crecer por encima de ella es una promesa incumplida que todavía no se ha anunciado.
El límite de tasa del sistema posterior, y no el número de workers que podía arrancar la plataforma, fijó la capacidad real, así que añadir consumidores tenía un techo infranqueable.
El mejor límite para una cola suele ser un límite de negocio: no tiene sentido retener trabajo que no puede tener éxito de ninguna manera.
Back pressure: hacer que los productores vayan más despacio
El back pressure (contrapresión) es cualquier mecanismo por el que una etapa posterior más lenta hace que una etapa anterior más rápida vaya más despacio, en lugar de dejar que el trabajo se amontone entre ambas. El término viene de los fluidos en tuberías, y la idea apareció en tuberías y filtros (Sección 5.4). Está integrado en muchas capas de la pila que ya usas. El control de flujo de TCP detiene a un emisor cuando el búfer del receptor está lleno. Una cola bloqueante acotada hace que el put del productor espere a que haya espacio. Los consumidores basados en pull, como en Kafka, toman trabajo solo cuando están listos, de modo que es el log del broker, y no la memoria del consumidor, lo que guarda el backlog, y un productor de Kafka se bloquea o falla cuando su propio búfer de envío está lleno. La especificación Reactive Streams permite que un suscriptor solicite un número concreto de elementos, de modo que un flujo nunca entrega más de lo que el suscriptor ha pedido.
El back pressure funciona bien dentro de un proceso o a lo largo de un pipeline. A través de una frontera de solicitud-respuesta con usuarios humanos, normalmente se convierte en rechazo: un servicio HTTP sobrecargado no puede hacer que un teléfono espere educadamente, así que responde 429 Too Many Requests o 503 Service Unavailable, idealmente con una cabecera Retry-After, y confía en que el cliente reduzca el ritmo. Cuando una cola acotada está llena solo hay tres opciones: bloquear al productor, rechazar el trabajo nuevo o descartar algo ya aceptado. Cada una es la correcta en algún caso; el error es no elegir.
Tres respuestas a una cola llena
Elegir qué ocurre cuando una cola está llena
| Estrategia | Comportamiento | Adecuada para | Riesgo |
|---|---|---|---|
| Bloquear al productor | El productor espera hasta que haya espacio | Pipelines y trabajos por lotes en los que el productor puede hacer una pausa | Los productores bloqueados retienen hilos y conexiones; entre servicios, esto propaga la ralentización hacia arriba |
| Rechazar el trabajo nuevo | Fallar rápido con un error como 429 o 503 | Solicitudes interactivas; el cliente puede reintentar, esperar o mostrar un mensaje | Los clientes que reintentan de inmediato convierten el rechazo en más carga (Sección 9.4) |
| Descartar el más antiguo | Desechar el elemento que lleva más tiempo esperando | Telemetría, actualizaciones de ubicación, precios en vivo: los datos más recientes son los más valiosos | Pérdida silenciosa; debe contarse y ser visible |
| Descartar por prioridad | Desechar primero los elementos menos importantes | Cargas de trabajo mixtas: conservar los pagos, descartar los eventos de analítica | Las prioridades bajas pueden quedar desatendidas indefinidamente; las prioridades deben asignarse con honestidad |
| Desbordar a almacenamiento duradero | Desbordar a disco o a una cola duradera | Trabajo que debe hacerse tarde o temprano, pero puede esperar horas | Solo es una cola más grande: sigue necesitando un límite y una estimación del tiempo de vaciado |
Descarte de carga y priorización
Un servidor con más carga de la que puede gestionar lo hace peor que si atendiera su capacidad y rechazara el resto. Lo acepta todo, cada solicitud espera en sus colas internas, los tiempos de respuesta superan los timeouts de los clientes, y acaba gastando toda su capacidad en solicitudes cuyos clientes ya se han rendido. El rendimiento (throughput) se mantiene alto mientras que el goodput, la tasa de solicitudes completadas a tiempo para ser útiles, se desploma hacia cero. El descarte de carga (load shedding) lo evita rechazando algunas solicitudes de forma deliberada para que el resto tenga éxito.
Un buen descarte tiene tres propiedades. Es temprano y barato: rechazar una solicitud debe costar mucho menos que atenderla, así que se hace en la puerta de entrada, antes de la autenticación costosa o del trabajo con la base de datos, a partir de una señal barata como las solicitudes en curso o la antigüedad de la cola. Está priorizado: el libro de SRE de Google describe cómo etiquetar cada solicitud con una criticidad (de CRITICAL_PLUS a SHEDDABLE) para que el tráfico por lotes y de precarga se descarte antes que el tráfico de cara al usuario. En la plataforma de reparto de comida, los pagos y las actualizaciones de ubicación de los repartidores tienen prioridad sobre las recomendaciones, que a su vez la tienen sobre los eventos de analítica. Y es consciente del tiempo: una solicitud que ya ha esperado más que el timeout de quien la llama debe descartarse en lugar de procesarse. Los ingenieros de Facebook han descrito dos refinamientos: una cola de retardo controlado que solo permite una espera corta cuando una cola no se ha vaciado recientemente, y pasar a último en entrar, primero en salir (LIFO) bajo carga, para que se atiendan primero las solicitudes más recientes, cuyos usuarios todavía están esperando.
Un paso más es adaptar el propio límite. Los límites de concurrencia fijos son o demasiado bajos en un buen día o demasiado altos en uno malo; los limitadores de concurrencia adaptativos aumentan el número permitido de solicitudes en curso mientras la latencia se mantiene baja y lo reducen cuando la latencia sube, tomando la idea del control de congestión de TCP. En el extremo frontal de un sistema, el control de admisión puede adoptar la forma de una sala de espera virtual, como en la venta de entradas anterior: los usuarios se retienen en una cola en el borde y se admiten al ritmo que el núcleo puede atender, lo cual es descarte de carga presentado como equidad.
Algoritmos de limitación de tasa
La limitación de tasa (rate limiting) restringe cuánto trabajo puede enviar en un periodo un cliente, una clave de API, un inquilino (tenant) o una dirección IP concretos. Se diferencia del descarte de carga en su propósito: el descarte protege al servidor de una sobrecarga total, la cause quien la cause, mientras que la limitación de tasa reparte la capacidad de forma equitativa y protege frente a cualquier cliente concreto, ya tenga errores o sea malicioso. Ambos responden de la misma manera, normalmente con HTTP 429. Predominan cuatro algoritmos.
Comparación de los algoritmos de limitación de tasa
| Algoritmo | Cómo funciona | Ráfagas | Estado por clave | Notas |
|---|---|---|---|---|
| Token bucket (cubeta de tokens) | Se añaden tokens a la tasa r hasta la capacidad b; cada solicitud gasta uno | Permite ráfagas de hasta b y después r de media | Dos números: los tokens y el momento de la última recarga | El más usado; la ráfaga y la tasa sostenida se ajustan por separado |
| Leaky bucket (cubeta con fugas) | Las solicitudes entran en una cubeta que se vacía a una tasa constante; cuando está llena, se rechazan las nuevas solicitudes | La salida es perfectamente uniforme; las ráfagas esperan o se rechazan | Una cola, o un contador y una marca de tiempo | Se usa para la conformación de tráfico (traffic shaping); la limitación de solicitudes de NGINX se basa en él |
| Contador de ventana fija | Cuenta las solicitudes por ventana de calendario (por minuto); se pone a cero al cambiar de ventana | Hasta el doble del límite en torno a un cambio de ventana | Un contador | El más sencillo; 100 solicitudes a las 12:00:59 y 100 a las 12:01:00 pasan todas |
| Registro de ventana deslizante | Guarda la marca de tiempo de cada solicitud de la última ventana; las cuenta | Exacto | Una entrada por solicitud | Preciso, pero consume mucha memoria con tasas altas |
| Contador de ventana deslizante | Pondera el recuento de la ventana anterior según cuánto se solapa todavía y suma el recuento actual | Aproximación cercana | Dos contadores | A los 30 s de un minuto, con 80 en el minuto anterior y 40 ahora: 80 × 0.5 + 40 = 80 |
El token bucket (cubeta de tokens) merece un examen más detenido porque es la opción predeterminada habitual. La cubeta contiene hasta b tokens y gana r tokens por segundo. Cada solicitud toma un token; si no queda ninguno, se rechaza (o, en la variante de conformación, se retrasa). Por tanto, un cliente que ha estado inactivo puede enviar una ráfaga de b solicitudes a la vez, lo que se adapta a clientes reales como una app móvil que abre una pantalla, mientras que en cualquier periodo más largo no puede superar r de media. El leaky bucket (cubeta con fugas) es su imagen especular: suaviza la salida hasta una tasa constante, lo que resulta útil para proteger un sistema posterior que no admite ráfagas en absoluto.
El token bucket
Ejemplo práctico
Ejemplo práctico: un token bucket para una API de socios
La clave de API de un restaurante asociado está limitada por un token bucket con capacidad b = 20 y tasa de recarga r = 5 tokens por segundo (300 por minuto de media). La cubeta está llena en t = 0.
| t = 0: una ráfaga de 30 solicitudes | Las 20 primeras toman los 20 tokens y se reenvían; 10 se rechazan con 429 |
|---|---|
| t = 0 a 1 s | Llegan 5 tokens nuevos (5 por segundo) |
| t = 1 a 10 s: 8 solicitudes por segundo constantes | Se aceptan unas 5 por segundo y se rechazan 3; la cubeta se mantiene casi vacía |
| t = 10 a 14 s: ninguna solicitud | La cubeta se recarga a 5 por segundo y vuelve a estar llena (20) en t = 14 |
| Cota superior en cualquier periodo T | Como máximo b + r × T solicitudes: en cualquier periodo de 60 segundos, 20 + 5 × 60 = 320, así que la media a largo plazo nunca puede superar 5 por segundo |
| Retry-After para una solicitud rechazada | El siguiente token llega en 1 ÷ r = 0.2 s, pero la cabecera cuenta segundos enteros, así que la respuesta dice Retry-After: 1 |
La capacidad fija la ráfaga que puede enviar un cliente que se comporta bien tras un periodo de inactividad; la tasa de recarga fija la tasa sostenida. Elígelas por separado.
Un cliente que respeta Retry-After, con un poco de jitter, se estabiliza en la tasa de recarga; uno que reintenta de inmediato solo acumula más 429.
import math, time
class TokenBucket:
def __init__(self, rate, capacity):
self.rate = rate # tokens added per second
self.capacity = capacity # largest burst allowed
self.tokens = capacity # start full
self.updated = time.monotonic()
def allow(self, cost=1):
now = time.monotonic()
elapsed = now - self.updated
self.tokens = min(self.capacity, self.tokens + elapsed * self.rate)
self.updated = now
if self.tokens >= cost:
self.tokens -= cost
return True, 0
wait = (cost - self.tokens) / self.rate # seconds until enough tokens
return False, max(1, math.ceil(wait)) # for the Retry-After header
bucket = TokenBucket(rate=5, capacity=20) # one bucket per API key
allowed, retry_after = bucket.allow()HTTP 429, Retry-After y dónde aplicar los límites
HTTP tiene señales estándar para ambas situaciones. 429 Too Many Requests, definido en el RFC 6585 (2012), indica a un cliente que ha enviado demasiadas solicitudes en un tiempo determinado. 503 Service Unavailable dice que el servidor en su conjunto está sobrecargado temporalmente o en mantenimiento. Cualquiera de los dos puede llevar una cabecera Retry-After (RFC 9110), que da un número de segundos o una fecha a partir de la cual tiene sentido reintentar. Muchas API también devuelven cabeceras que anuncian el límite, la cuota restante y el momento del reinicio, tradicionalmente X-RateLimit-*; un borrador del IETF está estandarizando los campos RateLimit y RateLimit-Policy. Los clientes que se comportan bien respetan Retry-After, añaden jitter para que los clientes rechazados no vuelvan todos en el mismo instante, y tratan los 429 repetidos como un motivo para ir más despacio y no para reintentar con más fuerza (Sección 9.4).
Los límites se pueden aplicar en varias capas, y los sistemas maduros usan más de una: en el borde o en el API gateway, por dirección IP y clave de API, para detener el abuso y los clientes descontrolados antes de que cuesten nada; por inquilino, para que un cliente grande no deje sin recursos a los demás; y dentro de cada servicio, para proteger su propia capacidad y sus dependencias. Un límite global entre muchas instancias del gateway necesita estado compartido, normalmente contadores atómicos en Redis, lo que añade una llamada de red por solicitud; un compromiso habitual es dar a cada una de las N instancias un límite local de aproximadamente 1 ÷ N del total y aceptar cierta imprecisión. Los límites deben fijarse a partir de la capacidad medida y publicarse, para que los socios puedan diseñar teniéndolos en cuenta, y el número de rechazos debe monitorizarse, porque un aumento repentino es o un ataque, o un error en un cliente, o un límite que ya no se ajusta a la realidad.
Colas de mensajes fallidos
Algunos mensajes fallan siempre: un payload mal formado, un pedido para un restaurante que ya no existe, un bug que desencadena una entrada concreta. Si se reintenta indefinidamente, un mensaje así, un mensaje venenoso (poison message), bloquea una partición o desperdicia un worker para siempre. Una cola de mensajes fallidos (dead-letter queue) (presentada en la Sección 6.4) recibe los mensajes que han fallado un número determinado de veces (SQS lo llama número máximo de recepciones, maximum receive count, en una redrive policy), para que el resto del trabajo pueda continuar.
Una cola de mensajes fallidos es una herramienta para los defectos, no para la sobrecarga: si los mensajes acaban en ella porque los consumidores son demasiado lentos, la solución es la capacidad o el back pressure, no una segunda cola. Cada mensaje de una DLQ significa que algo que el negocio esperaba no ocurrió, así que la DLQ necesita un responsable, una alerta sobre su profundidad o antigüedad, herramientas para inspeccionar los mensajes con su error y su número de intentos, y una forma de volver a procesarlos una vez corregida la causa. Decide de antemano qué significa reprocesar con retraso para el orden y para los mensajes cuyo momento de negocio ya ha pasado: un reembolso puede reprocesarse con un día de retraso; una asignación de repartidor, no.
Comprueba lo que has entendido
Los workers procesan 200 mensajes por segundo y los mensajes pueden esperar como máximo 30 segundos. ¿Cuál es la mayor longitud de cola aceptable?
Según la ley de Little, 200 × 30 = 6000 mensajes; una cola más larga significa que algunos mensajes esperarán más de 30 segundos.
¿Por qué la antigüedad del mensaje más antiguo es una alarma mejor que la longitud de la cola?
La antigüedad mide el retraso que experimentan los usuarios y sube de inmediato si los consumidores se atascan, mientras que una longitud que parece normal puede ocultar una cola atascada o un vaciado lento.
Un token bucket tiene capacidad 50 y se recarga a 10 por segundo. ¿Cuántas solicitudes puede hacer un cliente en el primer segundo tras un largo periodo de inactividad?
Unas 60: los 50 tokens que ya hay en la cubeta más los 10 que se añaden durante ese segundo.
¿Cuál es la diferencia entre el descarte de carga y la limitación de tasa?
El descarte de carga protege al servidor de una sobrecarga total, la cause quien la cause; la limitación de tasa pone un tope a lo que puede enviar cada cliente para que la capacidad se reparta de forma equitativa.
Fuentes y lecturas recomendadas
¿Tienes una pregunta sobre esta sección? ArchitectBot, el asistente de estudio con IA del curso, responde primero a partir del curso.
Pregunta: “Nuestra cola de la salida a la venta de entradas llegó a 200 000 pedidos y los clientes esperaron 40 minutos para obtener un resultado. ¿Cómo deberíamos haber limitado la cola y qué debería ocurrir con los pedidos sobrantes?”