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

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.

Author

Anichur Rahaman

hace 5 días13 min read5 views
Parte 2: Dos Load Balancers, Dos Pools, y Por Qué Agregar Servidores No Fue Suficiente

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.

Diagrama de arquitectura: los estudiantes llegan a un frontend load balancer y a un frontend autoscale pool cuyos nodos cada uno ejecutan nginx y dos contenedores Next.js; las llamadas de API van a un backend load balancer y a un pool backend cuyos nodos ejecutan nginx y php-fpm; los servicios de datos gestionados y un único worker fijo se encuentran fuera de los pools
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.

AntesDespués
Servidores1 droplet, 8 vCPU / 16 GB, 1 contenedor Next.jsPool de 2 a 10 nodos, 2 vCPU / 4 GB AMD compartido, 2 contenedores cada uno
Si un servidor muereEl sitio está caídoEl otro nodo sigue sirviendo
Medido con 2 nodosRenderizado limitado a aproximadamente un núcleoAproximadamente 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 frontendPool backend
Nodo2 vCPU / 4 GB AMD compartido, aproximadamente $28 al mes4 vCPU / 8 GB, aproximadamente $56 al mes
Mínimo / máximo2 / 102 / 10
Regla de scale-out70% CPU promedio, cooldown 5 minutos70% 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 observamosResultado
Pool backend2 a 3 nodos a las 21:29, cuando el promedio del pool alcanzó 75%
Pool frontend2 a 3 nodos a las 21:30, en 71%
Errores y timeouts0 errores de servidor, 0 timeouts; los nuevos nodos se unieron a los load balancers por sí solos
P95 general132 a 153 ms
API p95320 a 705 ms, mientras el backend se sentaba cerca de 90% antes de que el tercer nodo se uniera
HTTP 429Aproximadamente 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.

Gráfico esquemático: la carga real sube en t0, la métrica de CPU se pone al día solo 5 a 8 minutos después, un tercer nodo se une después de que la métrica cruza el objetivo, y un cuarto nodo se agrega después de que la carga ya ha parado
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:

  1. Cambiar un nodo y probarlo.
  2. Tomar una imagen del nodo.
  3. Apuntar la plantilla del pool a la imagen.
  4. 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.

Línea de tiempo del rollout guardado en cinco pasos: cambiar solo la imagen, esperar a que los nuevos nodos pasen el health check, esperar aproximadamente 40 segundos, drenar los nodos antiguos primero, luego el proveedor los elimina; debajo, los carriles muestran nodos antiguos sirviendo, drenando y desaparecidos mientras los nuevos nodos arrancan, esperan y sirven
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.
  1. Cambiar solo la imagen en la plantilla del pool.
  2. Esperar hasta que cada nodo nuevo responda /lb-health.
  3. Esperar aproximadamente 40 segundos para que el load balancer los admita.
  4. Drenar los nodos antiguos primero: su /lb-health devuelve 503.
  5. 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.

ÁreaLo que está en su lugar
AccesoSSH solo con clave y Fail2Ban en todos los nodos
RedCloud 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
ProcesosLos contenedores ejecutan sus procesos de servicio de solicitud como usuarios sin root
SecretosArchivos de env secretos con modo 600
Base de datosEl 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

PiezaEstado
Dos load balancers, dos pools de autoscaling, deploys de imagen dorada, swap y hardeningImplementado
Prueba de autoscaling a aproximadamente 750 solicitudes/s; primer rollout y su blip 503Probado, luego arreglado
Un load balancer para ambas capasRechazado
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 nodoPlaneado

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.

About the Author

Anichur Rahaman

Continue Reading