Parte 3: Una Base de Datos Más Pequeña, Más Barata, Altamente Disponible en 26 Minutos, y Por Qué la IA Obtuvo Su Propio PostgreSQL
Movimos un nodo MySQL grande a un cluster Standard más barato con standby en una ventana de 26 minutos, dividimos lecturas al standby y dimos a la IA su propio PostgreSQL con pgvector. La noche real del examen mostró que la base de datos nunca fue el cuello de botella.
A las 22:33 en la noche del 5 de octubre, apuntamos ambos load balancers a una etiqueta vacía. Quien abriera el sitio veía la página de mantenimiento. Detrás, cada estudiante, respuesta y resultado se estaba copiando a una nueva base de datos, y la ventana que habíamos acordado con el negocio era 26 minutos.
Nadie se había quejado de que la base de datos fuera lenta. El problema era su forma. Era un nodo MySQL grande con sin standby, así que el día que esa máquina fallara sería el día que toda la plataforma fallara. También era más grande, y más cara, que el trabajo necesario.
Este artículo es sobre la capa de datos de NovaCommerce, la plataforma que vende cursos y ejecuta exámenes en línea cronometrados. Cubre el cambio de 26 minutos, el cliente que olvidamos, cómo dividimos lecturas y escrituras, por qué la IA obtuvo su propio PostgreSQL, y lo que la noche del examen real del 7 de octubre dijo sobre todo eso. Sigue la Parte 2, donde la capa de aplicación aprendió a escalar.
Esta es la parte 3 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.
El tipo equivocado de grande
La base de datos antigua era un nodo Managed MySQL "Advanced" con 8 vCPU y 32 GB de RAM. Era grande como es grande un único puente enorme. Impresionante, y la única forma de cruzar.
Al principio del proyecto escribimos lo que Alta Disponibilidad tenía que significar para nosotros. Una línea decía: la base de datos sobrevive a la falla de un nodo. Un nodo no puede cumplir esa línea en ningún tamaño. El tamaño y la disponibilidad son preguntas diferentes, y la configuración antigua había respondido solo la primera.
Los datos eran aproximadamente 11 GB. Así que nos trasladamos a Managed MySQL "Standard" 8.4 con 4 vCPU y 16 GB por nodo, y dos nodos: un primary, y un standby que toma el control si falla el primary. En el nuevo cluster el buffer pool es 7 GB, max connections es 1.601 y el autoscale de almacenamiento está activado. Estado: Implementado, 5 de octubre.
Antes
Después
Plan
Advanced, nodo único
Standard 8.4, dos nodos (primary + standby)
Tamaño
8 vCPU / 32 GB
4 vCPU / 16 GB por nodo
Si falla un nodo
La base de datos está caída
El standby toma el control
Costo mensual
Más alto
Más bajo; primary + standby es aproximadamente $389 en precio de lista
Ser más barato y altamente disponible al mismo tiempo es raro, así que lo tomamos. Aquí está toda la capa de datos tal como se ve hoy. El resto del artículo la recorre.
Las escrituras van al primary, las lecturas se comparten entre el standby y el primary, y la IA, cache y archivos cada uno viven en su propio servicio.
El cambio de 26 minutos, paso a paso
La ventana fue planeada y acordada con el negocio. Este es el orden que seguimos el 5 de octubre.
22:33. Ambos load balancers apuntaban a una etiqueta vacía. Los estudiantes veían la página de mantenimiento.
Las colas se drenaron y el scheduler se detuvo, así que ningún trabajo podía escribir en el medio de la copia.
Cero escrituras en la base de datos antigua durante 30 segundos. Esa fue la puerta: la copia comenzó solo después de que el lado antiguo se calmara.
Copia con mysqldump en 4 flujos paralelos. Tomó 10 minutos.
Verificación de conteos: 341 tablas y 18.727.244 filas coincidían exactamente entre antiguo y nuevo.
La aplicación fue probada contra la nueva base de datos. Luego una nueva imagen dorada, un rollout de ambos pools y un nuevo worker.
22:59. Load balancers de vuelta, 26 minutos después de que comenzamos.
Siete pasos planeados dentro de la ventana, y una sorpresa 25 minutos después de que se cerrara.
Cómo probamos que la copia era correcta
Hacer coincidir los conteos de filas es una prueba débil, porque dos tablas pueden mantener el mismo número de filas y contenido diferente. Así que verificamos más que conteos, y seguimos verificando después de que el sitio estuviera de vuelta.
Verificación
Resultado
Tablas y filas, antiguo vs nuevo
341 tablas, 18.727.244 filas, coincidencia exacta
Contenido de cada tabla
Checksums de contenido completo en todas las tablas, después de la ventana
Tabla de resultados más grande
Comparado columna por columna: 2,76 millones de filas idénticas
Derechos del usuario de la app
CREATE TABLE rechazado, como debería ser
Primeros 4 minutos en vivo
Aproximadamente 2.900 solicitudes, 0 5xx, 110 trabajos de notificación, 0 fallidos
El cliente que olvidamos
A las 23:24, 25 minutos después de que el sitio volviera, encontramos algo que no habíamos planeado. Un panel de administración heredado, ejecutándose en un servidor antiguo separado, aún estaba escribiendo en la base de datos anterior.
Habíamos apagado todo lo que recordábamos: los nodos web, el worker, el scheduler. No habíamos apagado una cosa que habíamos olvidado que existía. Piensa en mudarse de casa. La oficina de correos reenvía tu correo, pero un amigo que aún tiene tu dirección anterior sigue escribiendo cartas allá.
Para entonces 48 filas habían llegado a la base de datos anterior. Las fusionamos en la nueva con un upsert condicional: insertar la fila, o actualizar solo cuando la copia antigua es más reciente. Se fusionaron 44 filas. Para 4 filas la base de datos nueva ya tenía una versión más reciente, y mantuvimos esas. Luego apuntamos el panel heredado a la nueva base de datos y cambiamos la contraseña de la app del cluster antiguo, así que nada podía escribir en él de nuevo. Estado: Implementado.
El cambio de contraseña importa tanto como la fusión. Un cliente olvidado entonces falla ruidosamente en lugar de llenar silenciosamente una base de datos que nadie lee. El cluster antiguo se mantuvo congelado durante 48 horas como nuestra forma de retroceder, y fue eliminado después de eso.
La lección es un elemento de lista de verificación, no una historia:
Antes de un cambio, lista cada cliente externo de la base de datos. Lee las fuentes de confianza del cluster antiguo, y pregunta para cada entrada: ¿quién es el propietario?
Cambia cada cliente en la misma ventana que la aplicación.
Después del cambio, cambia la contraseña antigua, así que un cliente que hayas perdido falla de inmediato.
Mantén el cluster antiguo congelado por un tiempo como tu retroceso.
Lecturas del standby: la división lectura/escritura
Por qué no una réplica de solo lectura
La respuesta de libro de texto a "las lecturas son pesadas" es un nodo réplica de solo lectura. Lo consideramos. En esta plataforma un nodo de solo lectura debe ser al menos tan grande como el primary, así que costaría aproximadamente lo mismo de nuevo. Mientras tanto ya pagamos por un standby que está inactivo, esperando una falla.
Así que implementamos algo más barato: el standby también es un lector, alcanzado a través de su nombre de host de réplica. Las lecturas se comparten entre el standby y el primary, y cada uno cubre al otro. El mismo hardware, sin nueva línea en la factura.
Opción
Costo extra
Estado
Nota
Todo en el primary
Ninguno
Línea base
Lo más simple, pero el primary hace cada lectura
Nodo réplica de solo lectura dedicado
Aproximadamente lo mismo que el primary de nuevo
Considerado
Debe ser al menos tan grande como el primary
Standby comparte lecturas con el primary
Ninguno
Implementado, 6 de octubre
Las lecturas pueden retrasarse un poco; las lecturas pegajosas lo cubren
Las reglas, en palabras simples
Activamos la división a las 00:13 el 6 de octubre. En forma de bosquejo, la configuración de base de datos Laravel dice esto:
Las lecturas van al standby, y el primary está listado como segundo host de lectura. Si uno no responde, el otro sí.
Pegajoso. Después de una escritura, la misma solicitud sigue leyendo desde el primary, así que un estudiante ve su propia respuesta. Un pequeño middleware mantiene la próxima solicitud en el primary también. Piensa en escribir una nota en una pizarra: vuelves a la pizarra para leerla, no a una fotocopia que aún podría estar imprimiendo.
Un timeout de conexión de 2 segundos. Un standby lento vuelve al primary rápidamente, y ningún estudiante espera en él.
El worker nunca usa el standby. Se queda en el primary, porque los trabajos necesitan datos frescos.
¿Realmente se divide?
Verificamos en un nodo en vivo en lugar de confiar en la configuración. Abrimos 8 conexiones frescas, dos veces. La primera pasada dividió 6 y 2 entre los dos servidores, la segunda dividió 4 y 4. El primary reportó read_only=0 y el standby read_only=1, así que ambos estaban siendo utilizados, y sabíamos cuál era cuál.
Una trampa
La producción carga clases de un mapa de clases autoritario. Una clase completamente nueva, como un nuevo middleware, que no está en el mapa no existe según la aplicación, y la solicitud devuelve un 500. Agrégalo al mapa de clases como parte del deploy.
Tamaño fijo, derechos estrechos, acceso por etiqueta
Redimensionamiento semanal: considerado. Una idea fue redimensionar la base de datos hacia arriba antes de los días de examen y hacia abajo después. El negocio eligió un 4 vCPU / 16 GB fijo en su lugar. Los números de la noche del examen de abajo respaldan esa elección.
Menor privilegio: implementado. El usuario de la aplicación de la base de datos tiene SELECT, INSERT, UPDATE y DELETE, y sin DDL. Puede cambiar filas, que debe, pero no puede crear, alterar o descartar una tabla. Lo probamos durante el cambio: CREATE TABLE fue rechazado.
Acceso por etiqueta: implementado. Cada base de datos admite la etiqueta de backend, el worker y las direcciones de oficina y salto. Un nuevo nodo de backend escalado por autoscaling se conecta sin paso manual.
Eso deja un elemento de aritmética. Cada nodo backend ejecuta 80 trabajadores php-fpm, y cada uno puede mantener una conexión MySQL. El total debe estar bajo los 1.601 permitidos.
Nodos backend
Trabajadores php-fpm por nodo
Conexiones en el peor caso
Límite MySQL
2
80
160
1.601
4
80
320
1.601
6
80
480
1.601
10 (máximo del pool)
80
800
1.601
Incluso en el máximo del pool usamos aproximadamente la mitad del límite, lo que deja espacio para el worker y sesiones de admin.
Valkey, en la capa de datos
Un Valkey gestionado, con standby, sostiene el cache, las sesiones y las colas, y funciona con la política noeviction. La Parte 4 va más profundo. Aquí está solo lo que pertenece a la capa de datos.
El 6 de octubre a las 02:28, una prueba de carga del backend devolvió 2.854 500s de aplicación. Cada uno fue "Operation timed out" mientras se conectaba a Valkey, con un timeout de conexión de 5 segundos. Cada solicitud abría una conexión TLS fresca, que costaba aproximadamente 5.4 ms de CPU por solicitud para Valkey, contra aproximadamente 3.3 ms para MySQL. Con 4 GB, Valkey no podía aceptar conexiones lo suficientemente rápido.
Implementado: lo redimensionamos in-place a 8 GB con 2 nodos. Datos preservados, host sin cambios. La reexaminación ejecutó de 25 hasta 500 solicitudes/s, comenzando en 2 nodos backend con el pool escalando: 0 errores de aplicación, 0 5xx del backend. En noches de examen usa aproximadamente 12.5% de su memoria. Cuesta más dinero, y eliminó una falla real.
Por qué la IA obtuvo su propio PostgreSQL
NovaCommerce tiene características de estudio con IA: aprendizaje adaptativo, análisis de debilidades y predicción de preguntas. Funcionan en embeddings, que son listas largas de números que describen el significado de una pregunta. Encontrar los más similares se llama búsqueda de similitud de vectores.
Mantenemos esos embeddings en un PostgreSQL Managed separado 17 con la extensión pgvector, con 2 vCPU y 4 GB. No en MySQL. Tres razones:
Una forma de consulta diferente. La búsqueda de vectores significa vectores grandes, índices de vecino aproximado más cercano y consultas intensivas en CPU. El tráfico de examen son lecturas y escrituras cortas.
Sin competencia con escrituras de examen. Una consulta de similitud pesada nunca debe tomar CPU de un estudiante guardando una respuesta. Dos servidores significa dos presupuestos.
La herramienta correcta. MySQL no tiene un índice de vector de primera clase para este trabajo.
Dos cargas de trabajo con formas diferentes y costos de falla diferentes, así que dos bases de datos con tamaños diferentes.
La reducción que revertimos
El nodo PostgreSQL es pequeño, e intentamos hacerlo más pequeño: 1 vCPU y 2 GB. Probado, luego revertido. Lo devolvimos a 2 vCPU y 4 GB por las escrituras que llegan durante los exámenes. Un pequeño ahorro no valía un nodo que lucha en la única hora que importa. Tamaño para la hora de examen, no para el día tranquilo.
Para la versión genérica de este consejo, como elegir un tipo de índice y mantener exacta la búsqueda de vectores filtrados, consulta nuestra guía de campo La Capa de Datos Bajo Carga. Este caso de estudio se mantiene con lo que hicimos.
Backups y recuperación
Un standby protege contra una máquina muerta. No protege contra un error: si alguien elimina una tabla, el standby la elimina un momento después. Para eso están los backups. Usamos ambos, y responden preguntas diferentes.
Qué sale mal
Lo que nos protege
Estado
Falla un nodo MySQL
El standby toma el control (failover del proveedor)
Implementado
Los datos se pierden o dañan
Backups diarios de las bases de datos gestionadas (proveedor)
Implementado
El cambio sale mal
El cluster antiguo mantenido congelado durante 48 horas, luego eliminado
Implementado
Se pierde un nodo web o falla un rollout
Imágenes doradas de los nodos web; se mantienen las últimas 3 imágenes buenas
Implementado
Se pierde el worker
Una imagen del worker; aún es un punto único de falla
Imagen implementada, standby worker planeado
La base de datos no era el cuello de botella
No movimos la base de datos porque fuera lenta. La movimos por disponibilidad y costo. Las mediciones decidieron qué no gastar: en la noche real del examen del 7 de octubre, con dos exámenes uno detrás del otro, la base de datos fue la parte tranquila del sistema.
Medida, noche del examen 7 de octubre
Valor
MySQL CPU, máx / promedio
24.5% / 13.4%
Consultas MySQL en ejecución, máx
5
MySQL lock waits
0
Memoria Valkey
12.5%
Pico del backend
Aproximadamente 69 solicitudes/s
CPU del nodo backend, promedio de 2 minutos, máx
47% (un nodo alcanzó 86% durante un minuto)
El cuello de botella fue CPU del backend por nodo, y la base de datos tenía aproximadamente 6x de margen. Nuestro modelo de capacidad dice que MySQL se convierte en el límite cerca de 450 solicitudes/s, contra un pico de 69 esa noche. Ese modelo proviene de un único examen de opción múltiple. Los exámenes escritos con cargas de PDF utilizan mucho más el worker y deben medirse por separado.
En términos de dinero, los tres almacenes de datos cuestan aproximadamente $689 al mes en precio de lista: MySQL aproximadamente $389, Valkey aproximadamente $240 y PostgreSQL $60. Eso es aproximadamente 70% de la factura mensual aproximada de $990 con el pool backend en 2 nodos, que es por qué crecemos un almacén solo después de medirlo.
Lo que aprendimos
Un nodo grande no es alta disponibilidad. La disponibilidad necesita un segundo nodo que pueda tomar el control, y un par más pequeño puede superar una máquina gigante en precio.
Un cambio necesita una puerta antes de la copia (cero escrituras), conteos durante él, y checksums después de él.
Lista cada cliente de la base de datos antigua antes del cambio, luego cambia la contraseña antigua después así que un cliente que hayas perdido falla de inmediato.
Usa el standby como lector antes de comprar una réplica. Agrega lecturas pegajosas y un timeout de conexión corto, y mantén los trabajos en el primary.
Dale al usuario de la app los derechos que necesita y nada más. Sin DDL.
Dale a la IA su propio PostgreSQL, y dimensiónalo para la hora del examen.
Mide antes de gastar. La base de datos tenía 6x de margen; el trabajo estuvo en el backend.
Las bases de datos son ahora tranquilas, así que las próximas preguntas son sobre todo lo que ejecuta en el fondo: el único worker, las colas, los logs y las señales que observamos. La Parte 4 cubre esos: Valkey, workers, logging y confiabilidad.