One self-hosted console to run your entire business — commerce, ERP, HRM, CRM & manufacturing

Parte 4: Valkey, Colas y Logs, las Partes Silenciosas Que Se Rompen Primero

Parte 4 del caso de estudio NovaCommerce: 2.854 timeouts de Valkey, un worker que aún es un punto único de falla, una cola que retrasó tarjetas de mérito por 115 minutos, un proveedor de IA que dijo no, y un rate limiter que contaba el edificio.

Author

Anichur Rahaman

hace 1 día13 min read3 views
Parte 4: Valkey, Colas y Logs, las Partes Silenciosas Que Se Rompen Primero

Una prueba de carga en el backend a las 02:28 del 6 de octubre devolvió 2.854 errores de aplicación. Cada uno era la misma frase, lanzada mientras se conectaba: RedisException: Operation timed out. Los servidores web eran saludables y la base de datos era saludable. La parte que decía no era el servicio tranquilo sosteniendo sesiones, cache y colas.

La misma semana produjo un segundo número: 115. Ese es cuántos minutos la tarjeta de mérito más lenta esperó a ser recompilada después de un examen. El CPU del worker no era la causa, ni tampoco los WebSockets. El trabajo simplemente estaba en la línea equivocada.

La Parte 3 cubrió las bases de datos. Esta parte cubre las piezas que unen una flota stateless: Valkey, el único worker fijo, las colas, los rate limits, los logs y las señales que observamos. Se ven aburridas en un diagrama, y nos dieron muchas de nuestras sorpresas reales. Cada sección abajo es una falla, una causa y un cambio.

Esta es la parte 4 del caso de estudio de cinco partes "From One Server to Exam-Day Ready". NovaCommerce es un nombre ficticio; la arquitectura, los números y los errores son reales.

Una flota sin memoria necesita algo en donde recordar

Un nodo que puede ser eliminado en cualquier minuto no puede poseer nada. En esta plataforma los propietarios son una lista corta: MySQL gestionado para datos de aplicación, PostgreSQL gestionado con pgvector para embeddings de IA, almacenamiento Spaces para archivos, OpenSearch gestionado para logs, y Valkey gestionado para todo lo pequeño, rápido y compartido: sesiones, cache y colas.

Piensa en los cajeros de un supermercado. La gaveta de la caja y la lista de stock están en una oficina trasera. Un cajero puede irse a mitad de turno y el siguiente toma el carril, porque nada estaba en el bolsillo del cajero.

Diagrama en capas: pool frontend, pool backend y el worker fijo encima de una línea de red privada, y cinco almacenes debajo de ella: Valkey, MySQL, PostgreSQL con pgvector, Spaces y OpenSearch
Los nodos arriba no sostienen nada. Todo lo que debe sobrevivir a un nodo siendo eliminado vive en uno de los cinco almacenes de abajo.

Verificamos esto en lugar de asumirlo. El 7 de octubre los nodos backend escribieron 0 archivos al disco local en 24 horas. Las cargas, incluyendo PDFs de respuestas escritas de estudiantes, van directamente a Spaces; las sesiones y cache están en Valkey; los logs van a stdout. Implementado y verificado. Por eso los pools de autoscaling pueden eliminar cualquier nodo en cualquier momento.

EstadoDónde viveLo que nos compra
SesionesValkey (primary + standby)Cualquier nodo backend puede servir a cualquier estudiante
CacheValkeyUn cache compartido por cada nodo
Trabajos en colaValkeyLos envíos esperan de forma segura si el worker está caído
Cargas, PDFs de respuesta, mediosSpaces con CDNNingún archivo vive en el disco de un nodo
Logsstdout, enviados a OpenSearchSobreviven al nodo (backend hecho, frontend planeado)
Datos de aplicaciónMySQL gestionadoCubierto en la Parte 3

Un Valkey, a propósito

Cache, sesiones y colas comparten un Valkey gestionado 8 con un nodo standby. Su política de evicción es noeviction: cuando la memoria está llena, Valkey rechaza nuevas escrituras con un error en lugar de silenciosamente eliminar claves antiguas. Para un cache puro suena al revés. Pero esta instancia también sostiene sesiones y trabajos en cola, y preferimos un error ruidoso a un trabajo desapareciendo sin huella. Estamos lejos de ese borde: en la noche de examen del 7 de octubre usó aproximadamente 12.5% de su memoria.

2.854 timeouts: cuando la memoria compartida no pudo responder

Volviendo a las 02:28. La prueba estaba golpeando un Valkey de 4 GB con un primary y un standby. La memoria no era el problema. Bajo carga no podía aceptar nuevas conexiones lo suficientemente rápido, y cada solicitud que esperaba más tiempo que el timeout de conexión de 5 segundos se convirtió en un 500.

Parte de la presión era nuestra. Cada solicitud abría una conexión TLS fresca a Valkey, que medimos en aproximadamente 5.4 ms de CPU por solicitud (aproximadamente 3.3 ms para MySQL). Con los 65 a 70 solicitudes por segundo que un nodo backend puede servir, ese handshake solo cuesta aproximadamente un tercio de un núcleo. Esa es aritmética de reverso de envase, no un perfil.

La corrección fue una redimensión, hecha in-place: Valkey fue de 4 GB a 8 GB, dos nodos (primary más standby), con los datos preservados y el nombre del host sin cambios. Sin cambio de aplicación, sin cambio. Implementado.

Luego medimos de nuevo. Probado: una escalada gradual de 25 a 500 solicitudes por segundo, comenzando en dos nodos backend mientras el pool escalaba, dio 0 errores de aplicación y 0 5xx del backend. El load balancer mostró 143 errores, pero solo mientras los nodos estaban saturados de CPU: la señal esperada de "fuera de capacidad", no un bug.

La lección útil es que el cuello de botella se movió tres veces en pocos días, y cada movimiento necesitaba un tipo diferente de corrección.

OrdenCuello de botellaTipo de problemaCorrección
1Rate limiter de API codificado por IPCódigo y configuraciónCodificar por cuenta (Implementado)
2Conexiones ValkeyDimensionamiento de capa de datos4 GB a 8 GB, 2 nodos (Implementado)
3CPU del backend por nodoCapacidad simpleMás nodos, pre-escalados (Implementado)

Solo la última fila se resuelve agregando servidores. Para la segunda, más servidores habrían abierto aún más conexiones en el mismo Valkey.

Un worker, y los trabajos que solo él puede hacer

Todo alrededor del worker escala. El worker mismo no. Es un droplet fijo (4 vCPU, 8 GB) y lleva cuatro trabajos:

  • Colas. Default, exam, tres colas de notificación, OMR y reportes de inscripción, ejecutados por Horizon.
  • El scheduler. Los trabajos cronometrados deben ejecutarse exactamente una vez.
  • WebSockets. Reverb sirve las actualizaciones en vivo. Su puerto está abierto solo a los nodos backend, por etiqueta.
  • Todos los SMS. La puerta de SMS coloca en lista blanca una única dirección IP, así que SMS solo puede salir de esta máquina.

Por eso las colas, el scheduler, los WebSockets y OMR están apagados en los nodos web: un nuevo nodo escalado nunca debe ejecutar un trabajo dos veces. Lo que nuestro caso añade a la regla genérica es que SMS está atado a una dirección, no solo a un proceso.

También es un punto único de falla, y lo decimos claramente. Si este droplet muere, SMS, el scheduler y el procesamiento de examen se detienen. Los estudiantes siguen enviando, y sus envíos esperan de forma segura en Valkey, que es la razón por la que las colas viven allí. Pero nada los procesa hasta que un worker esté de vuelta.

En la noche del examen del 7 de octubre el worker alcanzó pico de 46% de CPU con 0 trabajos fallidos, así que este es un riesgo que planeamos, no una falla que hayamos visto. El plan, con etiquetas honestas:

PasoPor quéEstado
IP reservada, agregada a la lista blanca con la puerta de SMSUn worker de reemplazo envía SMS desde una dirección conocidaPlaneado
Pequeño worker standbyAlgo listo para tomar el relevo de colas y schedulerPlaneado
Imagen de burst worker solo para examen, comenzada antes de exámenes grandesCapacidad de cola extra cuando es necesarioPlaneado
Una imagen del workerUna reconstrucción comienza desde una imagen, no desde la memoriaImplementado

La IP reservada viene primero: sin ella, un worker standby enviaría SMS desde una dirección que la puerta no acepta.

La tarjeta de mérito que esperó 115 minutos

Después de un examen, un trabajo "recompute de mérito" refresca la tarjeta de mérito de cada estudiante. El 6 de octubre esas tarjetas comenzaron a aparecer 10 a 115 minutos tarde.

Lo que descartamos

Los sospechosos obvios eran el CPU del worker y el servidor WebSocket. Ninguno era la causa.

Qué fue

El trabajo de mérito es minúsculo: 0.34 segundos. Compartía una cola, servida por 2 workers, con un trabajo de IA que hace una llamada a modelo de lenguaje de aproximadamente 10 segundos por estudiante. Después de un examen, aproximadamente 2.000 de esos trabajos de IA fueron encolados, y cada trabajo de mérito se sentó detrás de ellos.

La aritmética muestra la escala. Un trabajo de IA toma tanto tiempo como aproximadamente 29 trabajos de mérito. Dos mil de ellos son aproximadamente 20.000 segundos de trabajo, y a lo largo de 2 workers eso es casi tres horas de línea. Una espera de 10 a 115 minutos encaja esa imagen.

Es el carril expreso en un supermercado: un pan no debería esperar detrás de un carrito lleno, y habíamos construido una única caja para todos.

Diagrama antes y después: una cola compartida donde trabajos de mérito minúsculos esperan detrás de aproximadamente 2.000 trabajos de IA de 10 segundos, versus carriles separados donde el carril de IA es controlado por un limitador compartido de 8 por minuto
Los mismos trabajos, topología diferente: a la derecha el trabajo de mérito nunca se encuentra con un trabajo de IA, y el carril de IA tiene su propio ritmo.

El cambio

Implementado: un carril de cola separado y su propio contenedor, solo para narrativas de IA, con 2 réplicas. Los trabajos de mérito y posición nunca más esperan a un modelo de lenguaje, y las tarjetas de mérito están listas en aproximadamente un minuto. Los trabajos de IA ya esperando fueron movidos al nuevo carril con un script atómico, así que cada trabajo estuvo en exactamente un lugar en cada momento.

La corrección fue topología, no potencia bruta. La regla genérica de una cola por tipo de trabajo está en Queues and Workers at Scale; lo que nos sorprendió fue qué inofensivo se veía el trabajo lento.

Luego el proveedor de IA dijo no

El nuevo carril protegió las tarjetas de mérito. No hizo que los trabajos de IA fueran más rápidos. El carril se encontró con el límite de tasa del propio proveedor, aproximadamente 7 a 10 llamadas por minuto, y una vez la cuenta del proveedor simplemente se quedó sin crédito.

Ambos son la misma falla: un servicio externo dice "no ahora". Un bucle de reintentos que lo golpea solo quema presupuesto y llena la tabla de trabajos fallidos, así que el carril fue rediseñado para ser paciente. Implementado y vivo en el worker desde el 6 de octubre:

MecanismoConfiguraciónLo que hace
Limitador de tasa compartido8 por minuto a lo largo de todos los workersMantiene las llamadas dentro de lo que el proveedor permite
Release, no failBackoff exponencial, 1 a 15 minutos, con jitterUn trabajo rechazado vuelve a la cola; jitter evita que retornen juntos
Ventana de reintentoHasta 12 horasEl trabajo sigue intentando mucho después de la prisa
Reembolso de presupuestoContador de presupuesto de IA mensualUna llamada rechazada no se cobra
FallbackTexto de plantillaEl estudiante ve una narrativa genérica mientras tanto

Los estudiantes nunca ven un error. El texto de plantilla está allí cuando abren la página, y la narrativa real lo reemplaza cuando llega su turno.

Fue probado para de verdad dentro de un día. La cuenta del proveedor se quedó sin crédito, aproximadamente 3.800 narrativas se acumularon como trabajos retrasados, y ninguno de ellos falló. La tarde siguiente se agregó crédito, y la primera narrativa nueva se escribió dentro de minutos, sin reinicio ni replay manual.

La aritmética muestra por qué la ventana es horas: 2.000 narrativas a 8 por minuto es aproximadamente cuatro horas de trabajo. Una ventana de unos pocos minutos dejaría caer la mayoría.

Compartir el limitador importa. Dos réplicas que cada una se pacen a sí misma a 4 por minuto funcionan hasta que alguien agrega una tercera. Un contador compartido mantiene el total honesto sin importar cuántos workers existan.

Rate limits: cuenta al estudiante, no al edificio

Un rate limiter es solo tan bueno como lo que cuenta. Nos equivocamos dos veces, en dos capas, y ambas veces el síntoma fue estudiantes recibiendo HTTP 429 en el medio de un examen.

CuándoQué se contóQué salió malCorrección
25 de septiembreLimitador de API global, codificado por IP (guardia por defecto vacío para estudiantes basados en token)Una escuela u operador de telefonía móvil pone cientos de estudiantes detrás de una IP NAT: un bucket compartidoCodificar por cuenta de estudiante o instructor, IP solo para invitados (Implementado)
7 de octubreThrottles de ruta como "30 por minuto", aún por IP del clienteEl backend vio una IP del proxy frontend para todos: 74% de llamadas a un endpoint de dashboard recibieron 429Contar por estudiante por ruta, invitados por IP; 0 tales 429s después (Implementado)

Cada llamada de API del navegador pasa por el proxy frontend, así que una regla por IP trata la sala de examen entera como un visitante.

Ambas correcciones vinieron con pruebas que fallan sin la corrección; la segunda se desplegó un nodo a la vez con cero downtime. Planeado: pasar la IP real del estudiante desde el frontend al backend en un encabezado firmado, así que cada regla por IP y cada línea de log es exacta.

El proxy de confianza obsoleto

Un riesgo más pequeño se sentaba cerca. El backend aún confiaba en la IP del servidor frontend antiguo, el que habíamos eliminado. Si la nube alguna vez asignara esa dirección a alguien más, podrían falsificar IPs de estudiantes. La removimos el 7 de octubre con el método drain-one-node, de nuevo con cero downtime. Implementado. Una lista de confianza es una lista de promesas; elimina los de cuyo dueño se fue.

Logs que sobreviven al nodo

Con autoscaling, la máquina que quieres inspeccionar a menudo se ha ido. Así que los logs deben dejar el nodo mientras se escriben. Nuestros contenedores escriben solo a stdout y stderr, y Fluent Bit envía los logs del contenedor backend a OpenSearch gestionado, el stack estilo ELK: un lugar buscable que se mantiene después de que el nodo se ha ido. Implementado en los nodos backend. El método genérico está en Observability for High-Volume Systems.

Se pagó por sí solo en el trabajo de capacidad. El promedio de dos minutos de DigitalOcean puso el pico del backend del 7 de octubre en 63 solicitudes por segundo; los logs por minuto dijeron aproximadamente 69. Los promedios ocultan picos, y un modelo de capacidad necesita el pico.

Hay una brecha. Los nodos frontend aún mantienen sus logs nginx en disco local, aproximadamente 440 MB al día, y los pierden cuando un nodo se elimina. Planeado: Fluent Bit en los nodos frontend también. Hasta entonces, el frontend es la única capa donde una scale-in borra la evidencia.

Lo que observamos, y qué hacemos al respecto

Nuestra consola de ops es de solo lectura: toma muestras y nunca cambia nada. Registra CPU por nodo y por contenedor, pool CPU, hebras MySQL ejecutándose, conexiones y lock waits, memoria Valkey, clientes y operaciones por segundo, y salud de cola. El propio monitoreo de DigitalOcean agrega solicitudes por segundo y clases de respuesta en los load balancers.

Durante un examen no miramos todo eso. Leemos un resumen una vez por minuto: estudiantes iniciados y enviados, solicitudes y 5xx por nodo, pool CPU, carga de worker y trabajos fallidos. Pocos números, cada uno atado a una decisión.

Bucle de izquierda a derecha: señales desde la consola de ops, métricas de load balancer y logs OpenSearch, un resumen una vez por minuto, luego acciones como drenar un nodo, revertir la imagen y pre-escalar, luego re-medir
Las señales alimentan un resumen, el resumen dispara uno de unos pocos movimientos conocidos, y cada incidente regresa como una regla de runbook.

Actuar sobre una señal

El bucle tiene un pequeño conjunto de movimientos, descritos en la Parte 2:

  • Drenar un nodo. Hacer que /lb-health devuelva 503, y el load balancer deja de enviar nuevas solicitudes dentro de aproximadamente 30 segundos.
  • Revertir la imagen. Mantenemos las últimas tres imágenes buenas, y nunca lanzamos una plantilla durante horas de examen.
  • Pre-escalar. Elevar el mínimo del pool aproximadamente 45 minutos antes de un examen grande. El escalado reactivo es solo la red de seguridad, porque la métrica de CPU de DigitalOcean se retrasa 5 a 8 minutos la carga real.

En el rollout completo de ambos pools el 7 de octubre, una sonda de uptime golpeó páginas reales cada 2 segundos: 219 sondas, 0 errores. Ese es el bucle cerrándose: cambiar, observar, medir.

El error de scale-in

Durante el examen real del 7 de octubre, alguien bajó el mínimo del pool backend. DigitalOcean removió un nodo sirviendo sin drenarlo, y aproximadamente 360 solicitudes fallaron en 2 minutos.

Fue un desliz humano y un comportamiento de plataforma al mismo tiempo: el scale-in de autoscale no drena. La regla ahora está en el runbook (Implementado como regla de runbook): elevar el mínimo antes de un examen, y bajarlo solo después. Sabemos su límite: una regla de runbook depende de alguien recordarla en el medio de un examen. Es una guardia, no una cerradura.

Lo que aprendimos

  • Los nodos stateless son solo tan seguros como los almacenes compartidos detrás de ellos. Dimensiona Valkey para conexiones, no solo para memoria.
  • Una cola por tipo de trabajo. Un trabajo de 0.34 segundos nunca debe esperar detrás de uno de 10 segundos.
  • Cuando un proveedor te limita, controla el ritmo de las llamadas, retrocede con jitter, reintenta durante horas, reembolsa el presupuesto y muestra un fallback.
  • Verifica lo que cada rate limiter cuenta. El estudiante es la unidad, no el edificio y no el proxy.
  • Envía los logs fuera del nodo mientras se escriben, y cierra la brecha del frontend antes de que cueste un incidente.
  • Escribe tus puntos únicos de falla con un estado al lado de cada uno. El nuestro es un worker, con correcciones planeadas y sin pretensiones.
  • El scale-in de autoscale no drena. Eleva el mínimo antes de un examen.

En la Parte 5, la última de la serie, lo juntamos todo: la noche de examen real del 7 de octubre con números de producción, el modelo de capacidad, el costo mensual, y la prueba de carga y soak que aún nos debemos.

About the Author

Anichur Rahaman

Continue Reading