Escalar una base de datos según el cuello de botella

  • datos

Una aplicación pequeña puede funcionar con una API y una base de datos. Cuando crece, las solicitudes compiten por CPU, memoria, disco, conexiones y bloqueos. “Un millón de usuarios” no describe por sí solo la carga: importan la concurrencia, la frecuencia de acceso y el costo de cada operación.

Primero, mejorar el trabajo existente

Antes de distribuir datos conviene estudiar consultas, índices y planes de ejecución. Un índice útil evita recorrer filas innecesarias, pero consume espacio y añade trabajo a las escrituras. Un pool reutiliza conexiones y limita sesiones simultáneas; hacerlo enorme puede agravar la saturación. Escalar verticalmente aumenta recursos, aunque tiene límites físicos y económicos.

La medición del cuello de botella orienta la técnica. Cada elección introduce sus propias condiciones y costos.
La medición del cuello de botella orienta la técnica. Cada elección introduce sus propias condiciones y costos.

Descargar lecturas

Una caché evita repetir consultas si se tolera su política de frescura. Las réplicas mantienen copias de datos y pueden atender lecturas. En un esquema con un primario escritor, agregar réplicas no reparte automáticamente las escrituras. Además, la replicación asíncrona introduce retraso: una lectura inmediata después de escribir puede no ver el cambio.

Por ejemplo, el usuario que acaba de editar su perfil puede requerir una lectura del primario o una estrategia explícita de lectura después de escritura. PostgreSQL documenta la diferencia entre réplicas utilizables para lectura y otras modalidades de standby, así como el comportamiento de la replicación.

Separar trabajo diferible

Al registrar un “me gusta”, la confirmación al usuario no tiene que esperar una notificación, la analítica y la actualización de recomendaciones. Una cola permite ejecutar esas tareas después con consumidores independientes. El cambio principal debe quedar guardado de manera confiable antes de confirmar; una cola en memoria no ofrece esa garantía por sí sola.

Los consumidores deben manejar reintentos y duplicados. Si guardar el cambio y publicar el evento son operaciones separadas, un fallo intermedio puede perder el evento; patrones como el outbox transaccional coordinan ambos pasos.

Particionar no es necesariamente distribuir

El particionamiento divide una tabla lógica en partes físicas, por ejemplo eventos por mes. Una consulta con rango de fechas compatible puede descartar particiones irrelevantes. No todas las consultas se benefician y demasiadas particiones tienen costos de gestión.

El sharding reparte subconjuntos de datos entre nodos o bases independientes. Elegir la clave determina cómo se enrutan peticiones y si la carga queda equilibrada. Relaciones, joins y transacciones entre shards se vuelven más complejos, al igual que el rebalanceo.

Elegir almacenamiento por carga

También conviene separar cargas especializadas: cuentas en una base relacional, sesiones en Redis, búsqueda en un motor de índices, archivos grandes en almacenamiento de objetos y analítica en un motor columnar. Esta combinación evita exigir a un solo sistema que optimice trabajos incompatibles, pero añade sincronización y operación de varios servicios.

Una evolución guiada por evidencia

Si el problema son lecturas repetidas, caché o réplicas pueden ayudar. Si es trabajo secundario dentro del request, una cola reduce esa espera. Si son grandes recorridos temporales, particionar puede encajar. El sharding tiene sentido cuando distribuir almacenamiento o escrituras justifica su costo. No existe una escalera obligatoria: cada cambio debe responder al cuello de botella observado.

Referencias