Parte 2: Dos Load Balancers, Dos Pools, y Por Qué Agregar Servidores No Fue Suficiente
Cómo reconstruimos la capa de aplicación de una plataforma de examen: dos load balancers, dos pools de autoscaling, un frontend Next.js con dos contenedores por nodo, un backend basado en etiqueta, una métrica de CPU que se retrasa 5 a 8 minutos, y un rollout con 219 sondas y 0 errores.
Una hora después de que cambiamos el DNS al nuevo frontend, el servidor anterior aún recibía aproximadamente el 36% del tráfico. Habíamos bajado el TTL a 300 segundos. Habíamos probado la nueva configuración a través de una entrada de archivo hosts. Y aún más de un tercio de los visitantes pasaba directamente por la nueva puerta principal, porque sus resolvedores recordaban la dirección anterior.
Ese número es por qué el servidor anterior se mantuvo vivo como nuestra forma de retroceder. También resume esta etapa: cada paso se veía simple en papel, y cada paso ocultaba una sorpresa medida.
En la Parte 1 encontramos que nuestros primeros cuellos de botella eran un rate limiter y una inundación de conexiones Valkey, no una falta de servidores. Solo después de eso agregar servidores se convirtió en la herramienta correcta, e incluso entonces necesitaba una arquitectura. Este artículo la cubre: dos load balancers, dos pools de autoscaling, un frontend reconstruido, un backend donde cualquier nodo puede ser reemplazado, y un método de deploy que no pierde solicitudes. La primera versión de ese método lo hizo.
Esta es la parte 2 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.
Por qué más servidores no fue la primera respuesta
En la Parte 1 el cuello de botella se movió tres veces. Primero un rate limiter que contaba estudiantes por dirección IP, así que toda una escuela compartía un bucket y recibía HTTP 429. Luego Valkey, donde una prueba de carga devolvió 2.854 errores de aplicación porque cada solicitud abría una conexión TLS fresca a un servidor que no podía aceptarlas lo suficientemente rápido. Solo el tercero, CPU del backend plana, es el tipo que más servidores resuelven: un nodo de 4 vCPU / 8 GB sirvió aproximadamente 65 a 70 solicitudes por segundo de la mezcla API real.
Agregar nodos a los dos primeros los habría hecho peor. Cada nuevo nodo ejecutaría el mismo código limitador y abriría sus propias conexiones al mismo Valkey. Habríamos pagado por más servidores y visto los mismos errores, solo más rápido. Así que este artículo es sobre el tercer cuello de botella, hecho correctamente.
Dos load balancers, no uno
Nuestro primer bosquejo usaba un único load balancer para todo. Uno es más barato que dos, y hay menos para cuidar. Estado: Considerado, luego Rechazado.
Un load balancer de DigitalOcean no puede enrutar por nombre de host o ruta de URL, y cada uno apunta exactamente a una etiqueta, lo que significa un grupo de servidores. Los nodos frontend y backend son grupos diferentes, con diferentes tamaños, health checks y reglas de escalado. Es una recepción que le da a cada visitante la misma lista de habitaciones.
DNS ya dividía el sitio web y la API en dominios separados, así que la división salió gratis: un load balancer por capa, cada uno enfrente de su propio pool. Estado: Implementado. Los dos juntos cuestan $48 al mes.
Sin pool caliente, así que mantuvimos los cocineros en la cocina
Al principio escribimos un requisito atractivo: un standby caliente que pueda tomar tráfico en 3 a 4 segundos. No existe aquí. Los pools de autoscaling de DigitalOcean no tienen pool caliente, y un nuevo droplet necesita minutos para ser creado, iniciado, verificado y admitido por el load balancer. Un taxi que ya está esperando en tu puerta es un producto diferente a un taxi que tienes que llamar.
Así que hicimos dos cosas aburridas en su lugar.
Capacidad de repuesto que ya está sirviendo. Ambos pools tienen un mínimo de 2 nodos, y ambos están dentro del load balancer todo el tiempo. Perder uno deja al otro ya tomando tráfico.
Pre-scaling. Aproximadamente 45 minutos antes de un examen grande elevamos el mínimo del pool, así que los nodos extra están iniciados, verificados y sirviendo antes de que el primer estudiante haga clic en Iniciar. Un nodo backend extra cuesta aproximadamente $0.08 por hora.
También consideramos Kubernetes (DOKS) para escalado a nivel de segundos. Significaría mucho más para ejecutar, así que lo dejamos para más tarde. Estado: Considerado, no implementado.
El frontend: ocho CPUs y un único cajero
El frontend antiguo era un único droplet de 8 vCPU / 16 GB ejecutando un contenedor Next.js, con TLS por certbot en el droplet y sin load balancer. Si moría, el sitio estaba caído. También desperdiciaba dinero silenciosamente: un proceso Node.js se renderiza en aproximadamente un núcleo, así que la mayoría de esos ocho CPUs no hacía nada. Un supermercado con ocho cajas y un único cajero.
El nuevo diseño ejecuta varios nodos pequeños e idénticos en lugar de uno grande. La Figura 1 muestra toda la capa de aplicación; la recorremos de izquierda a derecha.
Dos capas, cada una con su propio load balancer y pool. El worker es el único servidor fijo fuera de ambos pools.
Un nodo frontend
Cada nodo ejecuta nginx enfrente de dos contenedores Next.js idénticos, uno por vCPU. Las configuraciones que importan:
Los puertos de contenedores se vinculan solo a localhost, así que solo nginx puede llegar a un contenedor.
nginx equilibra entre los dos contenedores con least_conn: cada solicitud va al contenedor con la menor cantidad de conexiones activas.
Cada contenedor tiene un límite de memoria de 1.5 GB, restart: always y logs rotativos.
nginx toma la IP real del cliente del load balancer, así que los límites por estudiante siguen funcionando en lugar de ver una dirección para todos.
Un micro-cache sirve archivos estáticos, imágenes y la página de inicio sin despertar Next.js.
/lb-health solo pasa cuando Next.js responde, así que un nodo con una aplicación muerta sale de la rotación aunque nginx esté vivo.
HTTPS termina en el frontend load balancer, y el cloud firewall permite que un nodo frontend acepte puerto 80 solo desde ese load balancer.
Cambiar DNS, con una forma de retroceder
Construimos el nuevo frontend junto al servidor antiguo y lo probamos a través de una entrada de archivo hosts en nuestras propias máquinas, así que el dominio real apuntaba al nuevo load balancer para nosotros y para nadie más. Comparamos 12 páginas reales con el servidor anterior, bajamos el TTL de DNS a 300 segundos e hicimos el cambio.
El servidor anterior se mantuvo para retroceso, porque apuntar DNS hacia atrás habría sido el deshacer completo. Lo necesitamos más tiempo del esperado: después de una hora aún recibía aproximadamente el 36% del tráfico desde DNS cacheado. Un TTL es una solicitud, no una orden. Destruir el servidor temprano habría enviado aproximadamente un tercio de los visitantes a una dirección que ya no respondía.
La prueba de carga, y un nodo más barato
Dos nodos frontend sirvieron aproximadamente 356 solicitudes por segundo con un p95 de 0.05 segundos y 0 errores, con CPU del 55 a 60%. El pool primero usó droplets de CPU dedicada. Esa prueba mostró mucho espacio libre, así que nos movimos a droplets compartidos de AMD con 2 vCPU / 4 GB, aproximadamente $28 al mes cada uno. Una medición, no una suposición, hizo que el nodo más barato fuera seguro.
Antes
Después
Servidores
1 droplet, 8 vCPU / 16 GB, 1 contenedor Next.js
Pool de 2 a 10 nodos, 2 vCPU / 4 GB AMD compartido, 2 contenedores cada uno
Si un servidor muere
El sitio está caído
El otro nodo sigue sirviendo
Medido con 2 nodos
Renderizado limitado a aproximadamente un núcleo
Aproximadamente 356 solicitudes/s, p95 0.05 s, 0 errores, CPU 55 a 60%
El backend: de IDs de droplet a una etiqueta
Antes, el backend load balancer apuntaba a dos droplets por ID. Un tercer servidor nunca podría unirse por sí solo, porque el load balancer solo conocía dos nombres. Cambiamos el objetivo de IDs a una etiqueta. Es la diferencia entre una lista de invitados y un distintivo de personal: cualquiera que use el distintivo entra.
El cambio fue una actualización de API que mantuvo cada otra configuración del load balancer, con 0 errores durante el cambio. A partir de entonces, cualquier nodo que cree el pool lleva la etiqueta, se une al load balancer por sí solo, y cae bajo las mismas reglas de firewall basadas en etiquetas y de acceso a la base de datos. HTTPS corre de extremo a extremo: el load balancer re-encripta a cada backend sobre la red privada. Cada nodo arranca desde una imagen dorada.
Pool frontend
Pool backend
Nodo
2 vCPU / 4 GB AMD compartido, aproximadamente $28 al mes
4 vCPU / 8 GB, aproximadamente $56 al mes
Mínimo / máximo
2 / 10
2 / 10
Regla de scale-out
70% CPU promedio, cooldown 5 minutos
70% CPU promedio (luego 55%), cooldown 5 minutos
Health check
/lb-health pasa cuando Next.js responde
/lb-health respondido por nginx solo
Nodos stateless, y el único servidor que no lo es
Los nodos web ya eran stateless, y verificamos de nuevo antes de confiar un pool a ellos. Las sesiones, el cache y las colas viven en Valkey gestionado, los logs van a stderr, y las cargas, incluyendo PDFs de respuestas escritas de estudiantes, van al almacenamiento Spaces. El 7 de octubre lo verificamos: los nodos backend escribieron 0 archivos al disco en 24 horas. Por eso cualquier nodo puede ser eliminado en cualquier momento.
Cada nodo backend ejecuta solo web (nginx) y app (php-fpm). Las colas, el scheduler, el servidor WebSocket y OMR están apagados en nodos web, así que los servidores extra nunca ejecutan un trabajo dos veces. Todos viven en el único worker fijo fuera de ambos pools, un punto único de falla conocido que la Parte 4 vuelve a tratar.
Health checks y drenaje
En el backend, /lb-health es respondido por nginx solo y nunca inicia el framework, así que un health check no puede ser ralentizado por PHP o la base de datos. También nos da un interruptor de drenaje. Para sacar un nodo de servicio, hacemos que /lb-health devuelva 503. El load balancer deja de enviar nuevas solicitudes a él dentro de aproximadamente 30 segundos, y las solicitudes ya en ejecución terminan normalmente.
La prueba de autoscaling y la mentira de cinco minutos
El 5 de octubre, entre las 21:17 y las 21:33, probamos el autoscaling en sí. k6 reprodujo la mezcla real de solicitudes de pico de examen, solo solicitudes GET así que nada se escribió: 300 a 450 solicitudes por segundo, luego aproximadamente 750 solicitudes por segundo durante 6 minutos.
Lo que observamos
Resultado
Pool backend
2 a 3 nodos a las 21:29, cuando el promedio del pool alcanzó 75%
Pool frontend
2 a 3 nodos a las 21:30, en 71%
Errores y timeouts
0 errores de servidor, 0 timeouts; los nuevos nodos se unieron a los load balancers por sí solos
P95 general
132 a 153 ms
API p95
320 a 705 ms, mientras el backend se sentaba cerca de 90% antes de que el tercer nodo se uniera
HTTP 429
Aproximadamente 4%: toda la carga vino de una IP de prueba, así que los límites por IP hicieron su trabajo
Mira el API p95, 320 a 705 ms. Ese es el precio de esperar. El backend se sentó cerca de 90% de CPU hasta que se unió el tercer nodo, porque la métrica de CPU de DigitalOcean se retrasa 5 a 8 minutos de la carga real. En la prueba anterior del backend, el primer nodo extra apareció aproximadamente 7 minutos después de que CPU alcanzó 99%. El autoscaler no está roto. Está leyendo noticias viejas.
El rezago trabaja en la otra dirección también. Cuando la carga se detuvo, la métrica aún se veía alta, y el backend brevemente escaló a 4 nodos. Es un grifo de ducha: lo giras más porque nada ha cambiado todavía, y luego el agua caliente llega de una vez.
La carga es un escalón, la métrica es una curva lenta, y el autoscaler sigue la curva. Esquema del efecto, no una grabación.
El argumento general está en Autoscaling for Traffic Spikes; aquí tiene nuestros propios números detrás. Para un examen programado elevamos el mínimo aproximadamente 45 minutos antes y lo bajamos solo después. El autoscaling se mantiene como la red de seguridad para lo no planeado. Estado de la prueba: Probado.
Deployar reemplazando, nunca editando
Los nodos del pool son desechables, así que nadie edita uno a mano: desaparecería en el próximo scale-in, y el próximo nodo nuevo arrancaría desde la imagen, no desde la edición. Nuestro flujo:
Cambiar un nodo y probarlo.
Tomar una imagen del nodo.
Apuntar la plantilla del pool a la imagen.
DigitalOcean crea los nuevos nodos, luego elimina los antiguos.
Mantenemos las últimas 3 imágenes buenas para retroceso y nunca lanzamos una plantilla durante horas de examen. Pasamos cada configuración en cada actualización del pool, así que nada se reinicia silenciosamente. El límite de droplets de la cuenta de 25 también tuvo que ser elevado antes de que ambos pools pudieran alcanzar su máximo durante un rollout, cuando existen nodos viejos y nuevos juntos.
El blip 503, y el rollout guardado
El primer rollout backend era un simple cambio de plantilla, y produjo un breve estallido de errores 503. DigitalOcean eliminó los droplets antiguos mientras el load balancer aún estaba enrutando solicitudes hacia ellos. Es como cerrar el comedor antiguo mientras el anfitrión aún está sentando a los invitados allí.
La corrección fue dejar de confiar en el orden de eventos del proveedor y ejecutar el rollout como una secuencia guardada. La Figura 2 lo muestra.
Los nodos antiguos se drenan antes de ser eliminados, así que el load balancer nunca envía una solicitud a un nodo que está a punto de desaparecer.
Cambiar solo la imagen en la plantilla del pool.
Esperar hasta que cada nodo nuevo responda /lb-health.
Esperar aproximadamente 40 segundos para que el load balancer los admita.
Drenar los nodos antiguos primero: su /lb-health devuelve 503.
DigitalOcean elimina los nodos antiguos después de su cooldown, sin nada que sirva.
El rollout completo posterior de ambos pools, el 7 de octubre, se ejecutó con una sonda de uptime cada 2 segundos en páginas reales. El resultado: 219 sondas y 0 errores. Estado: Probado, luego arreglado.
Swap y hardening en cada nodo
La memoria era el riesgo silencioso en el frontend. Esos nodos no tenían swap, y cada contenedor puede usar hasta 1.5 GB de un nodo de 4 GB. El 7 de octubre agregamos un archivo swap de 2 GB con swappiness 10, así que el kernel prefiere RAM, e lo horneamos en la imagen frontend. Los nodos backend ya tenían 4 GB de swap. El mismo conjunto de cambios cubrió hardening en cada nodo.
Área
Lo que está en su lugar
Acceso
SSH solo con clave y Fail2Ban en todos los nodos
Red
Cloud firewalls por etiqueta: los nodos frontend aceptan puerto 80 solo desde su load balancer; los nodos backend aceptan 80 y 443 solo desde el suyo
Procesos
Los contenedores ejecutan sus procesos de servicio de solicitud como usuarios sin root
Secretos
Archivos de env secretos con modo 600
Base de datos
El usuario de la aplicación tiene solo SELECT, INSERT, UPDATE y DELETE, sin DDL
Los firewalls siguen la etiqueta, así que un nodo que el pool crea en el medio de la noche obtiene sus reglas en el momento en que existe. Nadie tiene que recordar nada. Estado: Implementado el 7 de octubre.
Lo que aprendimos
Agregar servidores no es arquitectura. El limitador y Valkey necesitaban un cambio de código y una corrección de capa de datos; más nodos solo habrían copiado el problema.
¿Sin pool caliente? Construye la calidez tú mismo. Capacidad de repuesto ya sirviendo, más un mínimo elevado aproximadamente 45 minutos antes de un examen grande.
Pre-escala para exámenes programados. La métrica de CPU se retrasa 5 a 8 minutos, así que el autoscaling reactivo es la red de seguridad, no el plan.
Haz los nodos desechables. Nodos web stateless, una etiqueta en lugar de IDs, reemplazo por imagen en lugar de edición.
Drena antes de eliminar. Un rollout guardado es un procedimiento, no una esperanza: 219 sondas, 0 errores.
Mantén la cosa antigua hasta que su tráfico se haya ido. Un TTL es una solicitud; una hora después el servidor antiguo aún tenía 36%.
Una regla más, aprendida en una noche de examen real y contada en una parte posterior: el scale-in no drena por sí solo.
Dónde está cada pieza
Pieza
Estado
Dos load balancers, dos pools de autoscaling, deploys de imagen dorada, swap y hardening
Implementado
Prueba de autoscaling a aproximadamente 750 solicitudes/s; primer rollout y su blip 503
Probado, luego arreglado
Un load balancer para ambas capas
Rechazado
Standby caliente en segundos; Kubernetes (DOKS)
Considerado; DOKS para después
Activos estáticos desde el CDN; Fluent Bit en nodos frontend, cuyos logs nginx desaparecen con el nodo
Planeado
La capa de aplicación ahora puede crecer y ser reemplazada sin que nadie se dé cuenta. La siguiente pregunta es si la respuesta de un estudiante sobrevive: la base de datos. En la Parte 3 movemos MySQL en una ventana planificada de 26 minutos, y conocemos al panel de administración heredado que seguía escribiendo en la base de datos equivocada.