PostgreSQL vs Aurora PostgreSQL: sorpresas de costos durante picos (y cómo evitarlas)

¿Te fue útil?

El incidente terminó. La latencia volvió a la normalidad. Todos chocan manos en Slack. Entonces Finanzas aparece con un gráfico en forma de salto de esquí y pregunta por qué la factura de la base de datos parece haber intentado escapar de la gravedad terrestre.

Las cargas con picos no solo rompen sistemas; rompen supuestos. Y en AWS, pueden romper presupuestos de formas que se sienten personales. Hablemos de dónde difieren PostgreSQL en tus propias máquinas (o incluso autogestionado en EC2), RDS PostgreSQL y Aurora PostgreSQL bajo picos—y qué ajustes realmente previenen gastos sorpresa sin sabotear la fiabilidad.

La tasa por picos: dónde se fuga el dinero durante ráfagas

La mayoría de las historias de “sorpresa de costos” no son por alguien que compró el tipo de instancia equivocado. Son por una carga que cambia de forma por unos minutos, y la plataforma te cobra por todos los efectos secundarios: más I/O, más réplicas, más volumen de logs, más tormentas de reintentos, más tráfico entre zonas de disponibilidad, más amplificación de lectura por “arreglos” que no lo eran.

Bajo un pico, tu base de datos puede convertirse en un multiplicador. Un aumento de tráfico 3× se transforma en un aumento de I/O 10× porque las cachés fallan, las consultas derraman a disco y tu aplicación reintenta en timeouts como si audicionara para un papel en un ataque de denegación de servicio.

La distinción importante:

  • PostgreSQL tradicional (on-prem o EC2) generalmente te factura por capacidad aprovisionada. Cuando hay un pico, sueles pagar en dolor (latencia, saturación, caídas) más que en dólares—a menos que escales la infraestructura automáticamente.
  • Aurora PostgreSQL te factura por capacidad y por métricas de consumo que son fáciles de ignorar hasta que dejan de serlo. Los picos pueden traducirse directamente en gasto medido: I/O, crecimiento de almacenamiento, unidades de capacidad serverless, retención de backups y a veces patrones de red que no sabías que tenías.

Ninguna es “más barata”. Ambas pueden serlo. Lo que cambia es qué modo de fallo aparece primero: tiempo de inactividad o factura.

Datos e historia que explican las facturas actuales

Esto no es trivia. Son las razones por las que ciertas sorpresas de costo se repiten.

  1. PostgreSQL existe desde mediados de los años 90, y heredó supuestos de diseño de una era en la que el disco era lento, la RAM costaba y “la nube” significaba el clima.
  2. Amazon RDS se lanzó en 2009 para reducir el coste operativo de gestionar bases de datos—parches, backups y failovers. No eliminó la ingeniería de rendimiento; solo movió algunos controles.
  3. Aurora debutó en 2014 con una capa de almacenamiento distribuido diseñada para mejorar durabilidad y rendimiento. También creó nuevas superficies de facturación—especialmente I/O y comportamiento de almacenamiento.
  4. El almacenamiento de Aurora autoescalable en vez de obligarte a preaprovisionar tamaño de volumen. Eso es operativo agradable y financieramente peligroso si creces por bloat, retenciones descontroladas o una tabla olvidada.
  5. El modelo MVCC de PostgreSQL significa que las actualizaciones no sobrescriben filas; crean versiones nuevas. Durante picos, eso puede significar más WAL, más presión de vacuum y más churn de almacenamiento.
  6. La retención de WAL es una línea de presupuesto silenciosa en muchas ofertas gestionadas. Una réplica atascada o una transacción larga puede forzar crecimiento de retención e incremento de uso de almacenamiento.
  7. El pooling de conexiones se volvió mainstream en entornos Postgres principalmente porque el modelo proceso-por-conexión del backend es robusto pero no barato. Picos sin pooling a menudo equivalen a CPU quemada y desperdicio de memoria.
  8. Aurora Serverless ha pasado por dos generaciones (v1 y v2). v2 redujo algunas rarezas de escalado, pero la facturación sigue rastreando capacidad en el tiempo y puede subir más rápido de lo que piensas bajo tormentas de conexiones.
  9. La mayoría de los desastres de costos se correlacionan con incidentes de fiabilidad porque reintentos, failovers, fallos de caché y acciones de escalado de emergencia crean consumo medido. El costo suele ser la segunda alarma después de la latencia.

Una cita para mantenerte honesto: idea parafraseada de John Allspaw: Los incidentes ocurren porque los sistemas son complejos; la resiliencia viene de aprender y mejorar, no de culpar.

Dos modelos mentales: “una máquina con discos” vs “un servicio con medidores”

PostgreSQL (autogestionado): pagas sobre todo por estar listo

Cuando ejecutas Postgres tú mismo (bare metal, VMs o EC2), el costo está mayormente ligado a recursos aprovisionados: CPU, RAM y almacenamiento. Los picos no cambian mucho tu factura salvo que escales horizontalmente (nuevas réplicas) o verticalmente (instancias más grandes) de forma dinámica.

La ventaja es la previsibilidad. La desventaja es que pagas por holgura todo el año para sobrevivir el pico de 30 minutos en Black Friday—o no lo haces, y tus clientes experimentan el espíritu festivo mediante errores 500.

Aurora PostgreSQL: pagas para estar listo y por lo que haces

Aurora introduce una separación entre cómputo y almacenamiento que puede ser excelente para disponibilidad y escalado de lecturas. Pero el modelo económico es más “contador de servicios” que “arrendamiento”. Durante los picos, los medidores giran.

En Aurora, las categorías de sorpresa más comunes son:

  • Decisiones de escalado de cómputo (incluidas réplicas y capacidad serverless).
  • Consumo de I/O, incluidas lecturas causadas por fallos de caché y consultas ineficientes.
  • Crecimiento de almacenamiento por bloat, WAL, uso de temporales y patrones de retención de backups.
  • Efectos de red y cross-AZ, especialmente cuando arquitecturas mueven datos sin intención.

Broma #1: La facturación de Aurora es como una membresía de gimnasio que también cobra por cada paso en la cinta. Te pondrás en forma, pero también te dará curiosidad por caminar menos.

Sorpresas de costo por categoría (y cómo se manifiestan)

1) La sorpresa “añadimos réplicas”

Bajo un pico, los equipos a menudo añaden réplicas de lectura o escalan instancias. En Postgres autogestionado, eso suele significar nuevas instancias EC2 (o más grandes). En Aurora, las réplicas pueden crearse rápido, lo cual es bueno. Pero “rápido” también significa “fácil de hacer impulsivamente”.

Patrón de sorpresa de costo: las réplicas se vuelven persistentes. El pico termina, las réplicas se quedan, y ahora pagas por una nueva línea base.

Prevención:

  • Pon el conteo de réplicas bajo una política explícita: máximo N, reducir después de X minutos estables.
  • Instrumenta el lag de réplicas y la utilización del endpoint de lectura para saber si las réplicas realmente ayudaron.
  • Prefiere la optimización de consultas y el caching para picos repetibles; usa réplicas para patrones de lectura sostenida.

2) La sorpresa “el I/O se volvió no lineal”

Los picos amplifican la ineficiencia. Una consulta mediocre sin índice puede ser “aceptable” a 50 QPS y catastrófica a 500 QPS. En Aurora, la catástrofe a menudo llega como una línea de I/O en la factura más un incidente de latencia.

Causas comunes de I/O no lineal durante picos:

  • Cachés frías tras failover o eventos de escalado.
  • Escaneos grandes por planes malos (sensibilidad a parámetros, estadísticas obsoletas, índices equivocados).
  • Ordenamientos/hasheos que derraman a disco por work_mem insuficiente o conjuntos de resultados enormes.
  • Patrones N+1 que multiplican el trabajo por petición con mayor concurrencia.

Un detalle clave: algunas optimizaciones reducen CPU pero aumentan I/O, y puedes “optimizarte” hacia una factura mayor. Habrá una historia sobre eso.

3) Crecimiento de almacenamiento: autoescalar no significa auto-limpiar

El almacenamiento de Aurora crece automáticamente. Genial. Pero no se reduce automáticamente de la forma que la mayoría espera emocionalmente, especialmente si el crecimiento vino de bloat, tablas grandes o picos temporales de retención de WAL.

El crecimiento de almacenamiento durante picos puede provenir de:

  • Bloat MVCC cuando las actualizaciones/eliminaciones se disparan y vacuum no puede seguir el ritmo.
  • Transacciones largas que impiden que vacuum recupere tuplas.
  • Slots de replicación lógica que mantienen WAL alrededor.
  • Archivos temporales por derrames a disco (normalmente también un olor de rendimiento).

En Postgres autogestionado, ves el disco llenarse y entras en pánico. En Aurora, ves la factura después y entras en pánico con mejor iluminación.

4) Escalado serverless: la factura sigue la concurrencia, no tus intenciones

Aurora Serverless v2 puede ser adecuado para cargas con picos. También puede ser caro si los picos son causados por tormentas de conexiones, reintentos o comportamiento “charlatán” de la aplicación.

Mecanismo de sorpresa: la capacidad escala para satisfacer señales de demanda (conexiones, CPU, presión de memoria). Un pico creado por mal comportamiento del cliente se parece idéntico a un pico creado por crecimiento real del negocio. La base de datos no distingue por qué sufre.

5) Backups y retención: aburrido hasta que no lo es

Los costos de backup no suelen ser la línea más grande, pero se hacen visibles cuando el almacenamiento crece rápidamente o las políticas de retención son “configurar y olvidar”. Durante picos, si escribes más (workloads con mucho WAL), también puede aumentar la actividad de backups y snapshots según tu configuración.

El truco es que el gasto en backups suele ser una sorpresa retrasada—semanas después del incidente que causó el crecimiento de almacenamiento.

6) Cross-AZ y sorpresas de “red invisible”

Algunas arquitecturas generan tráfico significativo entre AZs: replicación, clientes en AZs distintas, analytics extrayendo grandes datasets o jobs por lotes moviendo datos fuera de la región. Durante picos, estos flujos también se amplifican.

Consejo práctico: mantén los clientes de la aplicación en la misma AZ que su endpoint primario de base de datos cuando sea posible, y sé intencional sobre dónde viven tus lectores. La fiabilidad Multi-AZ es buena; la charla accidental entre AZs no lo es.

Broma #2: Nada te enseña a amar el caching como pagar por leer los mismos datos 10 millones de veces en un día.

Guía de diagnóstico rápido

Cuando los costos suben, normalmente hay un incidente de rendimiento oculto. Diagnostica como un SRE: encuentra el cuello de botella, y luego mapea eso a la dimensión de facturación.

Primero: confirma qué cambió

  • Forma del tráfico: QPS, concurrencia, mezcla de peticiones. ¿Fueron más usuarios o más reintentos?
  • Topología de la base de datos: nuevas réplicas, failover, eventos de escalado, cambios de parámetros.
  • Despliegues: release de la app, cambio de esquema, nuevo índice, nueva ruta de consulta.

Segundo: localiza el cuello de botella

  • Limitado por CPU: alto uso de CPU, baja espera de I/O, consultas lentas por operadores intensivos en CPU.
  • Limitado por I/O: latencia de lectura/escritura elevada, fallos del buffer cache, crecimiento de archivos temporales.
  • Limitado por locks: sesiones bloqueadas, transacciones largas, fallos de serialización, deadlocks.
  • Limitado por conexiones: demasiadas conexiones, timeouts, autenticaciones frecuentes, presión de memoria.

Tercero: mapea el cuello de botella a los impulsores de costo

  • CPU bound → escalado de cómputo, clase de instancia mayor, aumento de ACU serverless.
  • I/O bound → cargos de I/O en Aurora, crecimiento de almacenamiento por churn, I/O por derrames temporales.
  • Lock bound → reintentos aumentados, carga amplificada, más escrituras (WAL), a veces más réplicas para “arreglar lecturas” incorrectamente.
  • Connection bound → escalado forzado, añadidos de réplicas, mayor ACU, peor comportamiento de caché tras reinicios/failovers.

Cuarto: detener el sangrado de forma segura

  • Habilita o ajusta el pooling de conexiones; limita conexiones máximas en el borde de la app.
  • Aplica rate-limits en los endpoints más ruidosos.
  • Apaga el comportamiento de “reintentar inmediatamente para siempre”; añade jitter y presupuestos.
  • Si debes añadir réplicas, define un temporizador de eliminación y mide la utilización real de los lectores.

Tareas prácticas: comandos, salidas y decisiones (12+)

Estas son las tareas que realmente ejecuto durante y después de picos. Cada una incluye: un comando, lo que significa una salida típica y la decisión que tomas.

Task 1: Identify top queries by total time (pg_stat_statements)

cr0x@server:~$ psql "$DATABASE_URL" -c "
SELECT queryid,
       calls,
       round(total_exec_time::numeric, 2) AS total_ms,
       round(mean_exec_time::numeric, 2) AS mean_ms,
       rows
FROM pg_stat_statements
ORDER BY total_exec_time DESC
LIMIT 10;"
 queryid  | calls | total_ms  | mean_ms | rows
----------+-------+-----------+---------+-------
 91283412 | 80000 | 980000.12 |   12.25 | 400000
 77221110 |  3000 | 410000.55 |  136.66 |   3000
(2 rows)

Qué significa: La primera consulta consumió la mayor cantidad de tiempo total, incluso si la latencia media no es horrible. Bajo picos, “moderadamente lenta pero masivamente frecuente” suele ser la villana.

Decisión: Empieza por la consulta con mayor tiempo total. Obtén su plan, añade el índice correcto o reduce el volumen de llamadas (batching, caching). No persigas la consulta más lenta a menos que sea frecuente.

Task 2: Get an execution plan with buffers (to see I/O)

cr0x@server:~$ psql "$DATABASE_URL" -c "
EXPLAIN (ANALYZE, BUFFERS, VERBOSE)
SELECT * FROM orders
WHERE customer_id = 12345
ORDER BY created_at DESC
LIMIT 50;"
                                                                QUERY PLAN
------------------------------------------------------------------------------------------------------------------------------------------
 Limit  (cost=0.56..25.12 rows=50 width=128) (actual time=2.114..5.882 rows=50 loops=1)
   Buffers: shared hit=120 read=450
   ->  Index Scan Backward using orders_customer_created_idx on public.orders  (cost=0.56..1200.00 rows=2500 width=128)
       Index Cond: (orders.customer_id = 12345)
       Buffers: shared hit=120 read=450
 Planning Time: 0.210 ms
 Execution Time: 6.050 ms

Qué significa: “shared read=450” indica lecturas físicas. Bajo picos, este número explota si las cachés están frías o la selectividad del índice es mala.

Decisión: Si las lecturas dominan, arregla patrones de acceso (mejores índices, conjuntos de resultados más pequeños, caching) y evita “simplemente añadir réplicas” como primera respuesta.

Task 3: Check cache hit ratio (rough signal, not religion)

cr0x@server:~$ psql "$DATABASE_URL" -c "
SELECT datname,
       round(100.0 * blks_hit / nullif(blks_hit + blks_read, 0), 2) AS cache_hit_pct
FROM pg_stat_database
WHERE datname = current_database();"
 datname | cache_hit_pct
---------+---------------
 appdb   |         93.41

Qué significa: Una caída respecto a tu línea base normal a menudo se correlaciona con mayor I/O en Aurora. La tasa de aciertos de caché no es una KPI, pero sí un detector de humo.

Decisión: Si bajó durante el pico, investiga desencadenantes de caché fría (failover, escalado, cambio en la mezcla de consultas) y considera patrones de warming o mantener un dimensionamiento de cómputo más estable.

Task 4: Detect connection storms and who’s doing it

cr0x@server:~$ psql "$DATABASE_URL" -c "
SELECT usename, application_name, state, count(*)
FROM pg_stat_activity
GROUP BY 1,2,3
ORDER BY 4 DESC
LIMIT 10;"
 usename | application_name | state  | count
--------+-------------------+--------+-------
 app    | api               | active |   220
 app    | api               | idle   |   900
 app    | worker            | active |    80

Qué significa: 900 sesiones idle es un pool que no está haciendo pooling, o un cliente que cree que abrir conexiones es un hobby.

Decisión: Implementa PgBouncer (o RDS Proxy cuando corresponda), limita conexiones máximas y arregla el dimensionamiento del pool en la app. Si estás en Aurora Serverless v2, trata el conteo de conexiones como una señal de costo.

Task 5: Identify the longest-running transactions (vacuum blockers)

cr0x@server:~$ psql "$DATABASE_URL" -c "
SELECT pid,
       now() - xact_start AS xact_age,
       wait_event_type,
       state,
       left(query, 80) AS query
FROM pg_stat_activity
WHERE xact_start IS NOT NULL
ORDER BY xact_age DESC
LIMIT 10;"
 pid  |  xact_age  | wait_event_type | state  | query
------+------------+-----------------+--------+-------------------------------
 4412 | 02:14:09   | Client          | idle   | BEGIN;
 9871 | 00:09:33   | Lock            | active | UPDATE orders SET status='X'

Qué significa: Una transacción abierta 2 horas puede forzar retención de WAL y bloat, y puede aumentar almacenamiento/I/O después del pico.

Decisión: Arregla el patrón de la app (no dejar transacciones idle), establece timeouts de statement/idle y considera matar a los infractores durante incidentes.

Task 6: Find lock contention quickly

cr0x@server:~$ psql "$DATABASE_URL" -c "
SELECT blocked.pid AS blocked_pid,
       blocker.pid AS blocker_pid,
       now() - blocker.query_start AS blocker_age,
       left(blocker.query, 60) AS blocker_query
FROM pg_locks blocked
JOIN pg_stat_activity blocked_act ON blocked.pid = blocked_act.pid
JOIN pg_locks blocker ON blocker.locktype = blocked.locktype
  AND blocker.database IS NOT DISTINCT FROM blocked.database
  AND blocker.relation IS NOT DISTINCT FROM blocked.relation
  AND blocker.page IS NOT DISTINCT FROM blocked.page
  AND blocker.tuple IS NOT DISTINCT FROM blocked.tuple
  AND blocker.transactionid IS NOT DISTINCT FROM blocked.transactionid
  AND blocker.pid != blocked.pid
JOIN pg_stat_activity blocker ON blocker.pid = blocker.pid
WHERE NOT blocked.granted
LIMIT 5;"
 blocked_pid | blocker_pid | blocker_age | blocker_query
------------+------------+-------------+------------------------------
      12011 |      11888 | 00:03:12    | ALTER TABLE orders ADD COLUMN

Qué significa: Hacer DDL en pico a menudo genera colas de locks, que provocan reintentos, que generan carga, que generan costo.

Decisión: Mueve DDL bloqueante fuera de picos, usa patrones de migración más seguros y limita lock timeout para que los clientes fallen rápido en lugar de amontonarse.

Task 7: Check temp file usage (disk spills = extra I/O)

cr0x@server:~$ psql "$DATABASE_URL" -c "
SELECT datname,
       temp_files,
       pg_size_pretty(temp_bytes) AS temp_written
FROM pg_stat_database
WHERE datname = current_database();"
 datname | temp_files | temp_written
---------+------------+--------------
 appdb   |      18220 | 48 GB

Qué significa: 48 GB de escrituras temporales durante un pico es un neón brillante. A menudo se correlaciona con ordenamientos/hasheos caros y puede aumentar cargos de I/O en Aurora.

Decisión: Arregla la consulta y/o incrementa memoria con cautela (work_mem por sesión puede dispararse), y reduce concurrencia con pooling.

Task 8: Verify autovacuum is keeping up (bloat prevention)

cr0x@server:~$ psql "$DATABASE_URL" -c "
SELECT relname,
       n_dead_tup,
       n_live_tup,
       last_autovacuum,
       last_autoanalyze
FROM pg_stat_user_tables
ORDER BY n_dead_tup DESC
LIMIT 10;"
 relname | n_dead_tup | n_live_tup |     last_autovacuum      |     last_autoanalyze
---------+------------+------------+--------------------------+--------------------------
 events  |   9200000  |  11000000  |                          | 2025-12-30 08:12:40+00

Qué significa: Muchísimas tuplas muertas y sin autovacuum reciente indica que vacuum se está quedando atrás—clásico crecimiento de almacenamiento y regresión de rendimiento post-pico.

Decisión: Ajusta autovacuum para tablas calientes, añade índices que reduzcan churn y elimina transacciones largas que bloquean la limpieza.

Task 9: Estimate table bloat (quick-and-dirty)

cr0x@server:~$ psql "$DATABASE_URL" -c "
SELECT relname,
       pg_size_pretty(pg_total_relation_size(relid)) AS total_size,
       n_dead_tup
FROM pg_stat_user_tables
ORDER BY pg_total_relation_size(relid) DESC
LIMIT 10;"
 relname | total_size | n_dead_tup
---------+------------+-----------
 events  | 180 GB     | 9200000
 orders  |  95 GB     |  120000

Qué significa: Una tabla masiva con muchas tuplas muertas es un multiplicador de almacenamiento e I/O.

Decisión: Considera particionar, ajustes de autovacuum más agresivos o mantenimiento periódico (como VACUUM (FULL) en casos extremos, pero planifica el downtime/impacto). En Aurora, haz esto antes de que el crecimiento de almacenamiento parezca permanente.

Task 10: Check replication lag (replicas that can’t keep up)

cr0x@server:~$ psql "$DATABASE_URL" -c "
SELECT application_name,
       client_addr,
       state,
       write_lag,
       flush_lag,
       replay_lag
FROM pg_stat_replication;"
 application_name | client_addr |  state  | write_lag | flush_lag | replay_lag
-----------------+-------------+---------+-----------+-----------+------------
 aurora-replica-1 | 10.0.2.55   | streaming | 00:00:01 | 00:00:02 | 00:00:05

Qué significa: Un lag pequeño está bien. Lag grande durante picos puede causar reintentos, lecturas obsoletas y crecimiento de retención de WAL.

Decisión: Si el lag crece, reduce la presión de escrituras, arregla transacciones largas y no enrutes lecturas sensibles a latencia a réplicas rezagadas.

Task 11: Identify WAL volume growth (write amplification)

cr0x@server:~$ psql "$DATABASE_URL" -c "
SELECT pg_size_pretty(pg_wal_lsn_diff(pg_current_wal_lsn(), '0/0')) AS wal_since_boot;"
 wal_since_boot
----------------
 320 GB

Qué significa: Un gran volumen de WAL puede correlacionarse con mayor churn de almacenamiento, presión de replicación y sobrecarga de backups.

Decisión: Reduce actualizaciones innecesarias, agrupa escrituras y evita patrones de actualización en el lugar que churn filas grandes. Considera ajustar fillfactor para patrones de actualización intensos.

Task 12: Check for missing indexes via slow queries (triage)

cr0x@server:~$ psql "$DATABASE_URL" -c "
SELECT schemaname, relname, seq_scan, idx_scan,
       pg_size_pretty(pg_relation_size(relid)) AS table_size
FROM pg_stat_user_tables
WHERE seq_scan > 10000 AND idx_scan = 0
ORDER BY seq_scan DESC
LIMIT 10;"
 schemaname | relname | seq_scan | idx_scan | table_size
------------+---------+----------+----------+-----------
 public     | events  |   820000 |        0 | 120 GB

Qué significa: Una tabla grande con montones de escaneos secuenciales es una fuente probable de picos de I/O.

Decisión: Añade índices dirigidos y reescribe consultas. Valida con EXPLAIN y estadísticas parecidas a producción; no indexes a ciegas.

Task 13: Verify Postgres settings that cause surprise memory blowups

cr0x@server:~$ psql "$DATABASE_URL" -c "SHOW work_mem; SHOW max_connections; SHOW shared_buffers;"
 work_mem
---------
 64MB
 max_connections
-----------------
 2000
 shared_buffers
----------------
 8GB

Qué significa: work_mem 64MB con 2000 conexiones no es “seguro en 128GB RAM”. Es “por favor disfrute swapping y derrames a disco”.

Decisión: Reduce max_connections (pool), establece work_mem sensato y escala cómputo para concurrencia real en lugar de teórica.

Task 14: On a Linux Postgres host, confirm I/O wait and saturation (self-managed)

cr0x@server:~$ iostat -xz 1 3
Linux 6.1.0 (db01)  12/30/2025  _x86_64_  (8 CPU)

avg-cpu:  %user   %nice %system %iowait  %steal   %idle
          22.10    0.00    6.12   32.55    0.00   39.23

Device            r/s     w/s   rkB/s   wkB/s  await  svctm  %util
nvme0n1         980.0   410.0 52000.0 18000.0  18.2   0.9   98.7

Qué significa: Alta iowait y ~99% de util sugiere que estás limitado por I/O. En EC2/on-prem esto suele ser el verdadero limitador; en Aurora lo ves como latencia + medición de I/O.

Decisión: Reduce demanda de I/O (índices, arreglos de consultas) o aumenta rendimiento de almacenamiento (discos más rápidos/IOPS). No añadas CPU si el disco es la pared.

Task 15: On a Linux Postgres host, find top talkers (connections and ports)

cr0x@server:~$ ss -tn sport = :5432 | head
State Recv-Q Send-Q Local Address:Port Peer Address:Port
ESTAB 0      0      10.0.1.10:5432     10.0.4.22:52144
ESTAB 0      0      10.0.1.10:5432     10.0.4.22:52146
ESTAB 0      0      10.0.1.10:5432     10.0.9.17:60311

Qué significa: Puedes identificar rápidamente qué hosts de la app están abriendo más conexiones TCP durante la tormenta.

Decisión: Limita la creación de conexiones en la fuente: settings del pool en la app, poolers como sidecar o shedding de carga. Evita que “escalar horizontalmente” se vuelva “multiplicación horizontal de conexiones”.

Tres microhistorias corporativas del campo

Microhistoria 1: El incidente causado por una suposición equivocada

Una empresa SaaS de tamaño medio migró de PostgreSQL autogestionado en EC2 a Aurora PostgreSQL. La propuesta era sólida: menos ops, mejor durabilidad de almacenamiento, gestión de réplicas más simple. El equipo también tenía un pico recurrente: cada hora en punto, los clientes actualizaban dashboards y jobs en segundo plano se activaban.

La suposición equivocada fue sutil: “El almacenamiento de Aurora se autoescalará, así que no tenemos que pensar en disco.” Dejaron de vigilar métricas de bloat y vacuum con la misma agresividad que antes. No por pereza—por alivio. Las alarmas de disco estaban psicológicamente asociadas al mundo viejo.

Durante una semana particularmente intensa, el pico horario se convirtió en un patrón diario de churn alto de escrituras. Una tabla que almacenaba estado de eventos se actualizaba frecuentemente y autovacuum no alcanzaba. Transacciones analíticas de larga duración (solo lectura, pero abiertas por mucho tiempo) impidieron la limpieza. El almacenamiento creció rápido y luego se mantuvo grande.

El incidente empezó como latencia: más lecturas necesarias por churn de caché e índices fragmentados. Luego se volvió un evento de costo: el medidor de I/O subió y la línea de almacenamiento saltó. Finanzas no estaba equivocada al sorprenderse; el sistema se comportó como estaba diseñado: siguió funcionando y siguió almacenando.

La solución no fue una configuración mágica de Aurora. Fue higiene clásica de Postgres: matar sesiones idle-in-transaction, ajustar autovacuum para la tabla caliente, añadir un índice más selectivo para reducir escaneos y cambiar el modelo de datos para reducir churn de actualizaciones. Después de eso, el crecimiento de almacenamiento se ralentizó. La factura dejó de subir. Y el equipo reaprendió una verdad molesta: gestionada no significa gestionada la data.

Microhistoria 2: La optimización que salió mal

Otra compañía tenía una API de solo lectura y picos recurrentes. Alguien hizo algo sensato: añadieron un índice para acelerar una consulta lenta. Funcionó. La latencia p95 bajó. El equipo celebró y siguió adelante.

Dos semanas después, otra gráfica se veía peor: los costos de I/O en Aurora subieron durante los picos, aunque la latencia parecía estar bien. El culpable fue el nuevo índice combinado con un patrón de escritura “inocente”: la columna indexada se actualizaba con frecuencia. Cada escritura ahora actualizaba el índice, aumentando la amplificación de escritura y el volumen de WAL.

Bajo condiciones de pico, la amplificación de escrituras más la mayor concurrencia causaron más trabajo en background: más WAL, más presión de replicación, más churn de caché. El sistema se mantenía dentro de los SLO la mayor parte del tiempo, pero hacía más trabajo total. El medidor se dio cuenta. Siempre se da cuenta.

Lo arreglaron retrocediendo: el endpoint no necesitaba actualizar esa columna indexada en cada petición. Movieron la actualización a un proceso por lotes y añadieron una tabla separada para atributos que cambian con frecuencia. Mantuvieron el índice, pero cambiaron el comportamiento de escritura. El costo bajó sin perder rendimiento.

Moral: una “optimización de rendimiento” que ignora patrones de escritura es solo una optimización de costo para tu proveedor de nube.

Microhistoria 3: La práctica aburrida pero correcta que salvó el día

Una empresa relacionada con pagos tenía requisitos estrictos de fiabilidad y una cultura sorprendentemente aburrida sobre planificación de capacidad. Ejecutaban Aurora PostgreSQL, pero lo trataban como un sistema de producción, no como un truco mágico. Cada trimestre hacían un ejercicio de picos: reproducían tráfico, medían la mezcla de consultas y verificaban dashboards de costo y rendimiento.

Durante un ejercicio, notaron un patrón: después de un failover, las cachés estaban frías y el I/O subía durante unos 15 minutos. No era catastrófico, pero sí costoso y podía empeorar durante un incidente real. En lugar de aceptarlo, construyeron una rutina de calentamiento: un conjunto controlado de consultas representativas ejecutadas a baja tasa después del failover para cebar cachés y estabilizar el rendimiento.

También tenían una regla estricta: cualquier acción de escalado de emergencia necesitaba un ticket de reducción de escala con una fecha límite y un responsable. Si alguien añadía una réplica o aumentaba el tamaño de instancia, había un recordatorio automático y una revisión obligatoria. Sin excepciones, porque “temporal” es la palabra más duradera en infraestructura.

Meses después, un pico real llegó: una integración con un socio se volvió viral durante la noche. El sistema aguantó la carga. Las rutinas de calentamiento redujeron el churn de I/O por arranque frío durante un failover menor. La política de reducción de escala evitó el creep de réplicas. El CFO nunca tuvo que aprender qué es un “request de I/O”, que es la verdadera definición del éxito operativo.

Errores comunes: síntoma → causa raíz → solución

1) Síntoma: los costos de I/O en Aurora suben durante picos, aunque la CPU se vea bien

Causa raíz: Fallos de caché, escaneos secuenciales, derrames a disco o regresiones de plan causan muchas lecturas físicas.

Solución: Usa pg_stat_statements + EXPLAIN (ANALYZE, BUFFERS) para encontrar consultas con muchas lecturas; añade índices, reduce conjuntos de resultados y evita derrames temporales. No escales cómputo como primera medida si estás limitado por I/O.

2) Síntoma: el almacenamiento crece rápido y no parece reducirse después

Causa raíz: Bloat MVCC, retención de WAL (slots de replicación / réplicas rezagadas), transacciones largas o migraciones grandes.

Solución: Elimina transacciones largas, ajusta autovacuum, arregla lag de replicación y planifica remediación de bloat. Espera que la “reducción” requiera trabajo deliberado, no esperanza.

3) Síntoma: añadiste réplicas pero la latencia del escritor empeoró

Causa raíz: El cuello de botella eran las escrituras o locks, no las lecturas. Las réplicas añaden complejidad y a veces presión extra (tormentas de conexiones, errores de enrutamiento).

Solución: Confirma la separación lectura/escritura y los principales eventos de espera antes de añadir réplicas. Si las escrituras están calientes, optimiza escrituras, reduce contención y agrupa operaciones.

4) Síntoma: Aurora Serverless v2 escala agresivamente durante ráfagas menores

Causa raíz: Tormentas de conexiones, bucles de reintento o endpoints charlatanes crean señales de demanda que parecen carga real.

Solución: Añade pooling, limita tasa, implementa presupuestos de reintentos con jitter y reduce consultas por petición.

5) Síntoma: después de un failover, los costos y la latencia suben 10–30 minutos

Causa raíz: Cachés frías e inestabilidad de planes después de cambios de rol; también tormentas de reconexión de la app.

Solución: Rutinas de calentamiento, escalonar reconexiones, imponer backoff y mantener índices y estadísticas saludables.

6) Síntoma: el uso de temporales explota durante picos

Causa raíz: Ordenamientos/hasheos que derraman por límites de memoria + alta concurrencia.

Solución: Reduce conjuntos de resultados, añade índices, reescribe consultas y limita concurrencia con un pooler. Ajusta work_mem con cuidado y con límites de conexiones.

7) Síntoma: “Escalamos, pero no ayudó mucho”

Causa raíz: Escalaste la dimensión equivocada. El problema suele ser locks, I/O o ruido de red—no CPU.

Solución: Usa análisis de wait, métricas de lecturas de buffer y diagnóstico de locks. Escala solo después de poder nombrar el cuello de botella en una frase.

8) Síntoma: las facturas se mantienen altas después de que el pico terminó

Causa raíz: Creep de réplicas, clase de instancia no revertida, crecimiento de almacenamiento o línea base incrementada por nuevas funciones.

Solución: Aplica políticas de reducción de escala, realiza revisiones semanales de “diferencia de factura” y mide QPS/mezcla de consultas vs el mes anterior. Trata las regresiones de costo como regresiones de rendimiento.

Listas de verificación / plan paso a paso

Plan paso a paso para prevenir sorpresas de costo por picos

  1. Define el pico: QPS pico, concurrencia máxima y los endpoints que importan. Si no puedes definirlo, no puedes presupuestarlo.
  2. Instrumenta las métricas correctas: consultas top por tiempo total, lecturas de buffers, bytes temporales, conteos de conexiones, esperas por locks, lag de réplicas, volumen de WAL.
  3. Establece barreras: max_connections, tamaño de pool, statement_timeout, idle_in_transaction_session_timeout.
  4. Escribe presupuestos de reintento: reintentos con backoff exponencial + jitter, y un tope por petición para que tu app no convierta un timeout en diez.
  5. Precalcula donde vale la pena: cache y materializa lecturas caras que ocurren durante picos. Prefiere cómputo predecible sobre I/O impredecible.
  6. Usa réplicas intencionalmente: solo cuando el cuello de botella sea realmente lectura; establece una regla de reducción automática o una tarea explícita de limpieza post-incidente.
  7. Ajusta autovacuum para tablas calientes: basealo en el churn observado, no en valores por defecto. Los valores por defecto son conservadores, no generosos.
  8. Planifica failovers: tormentas de conexiones y cachés frías son parte de la vida; las rutinas de calentamiento y el backoff de reconexión deben ser probados.
  9. Presupuesta por dimensión: horas de cómputo, horas de réplicas, I/O, crecimiento de almacenamiento y retención de backups. Los picos tocan más de una dimensión.
  10. Ejecuta un drill de picos trimestral: reproduce tráfico, confirma rendimiento, confirma controladores de costo. Si solo verificas uno, la sorpresa vendrá del otro.

Lista de respuesta de emergencia (durante un pico)

  • Confirma si la carga es tráfico real o reintentos (revisa tasa de errores de la app y contadores de reintentos).
  • Revisa conexiones y sesiones activas; habilita pooling o reduce tamaños de pool inmediatamente si es necesario.
  • Encuentra las consultas top por tiempo total y busca regresiones de plan; aplica índices seguros o arreglos de consulta.
  • Revisa acumulación de locks; detén DDL bloqueante; mata a los peores si es seguro.
  • Si añades réplicas o escalas, crea el ticket de reducción de escala en ese mismo momento, con fecha límite.

Checklist post-incidente (al día siguiente, con menos emociones)

  • Compara la ventana del pico con la línea base: mezcla de consultas, bytes temporales, cache hit, volumen de WAL.
  • Identifica el “primer dominó” (despliegue, migración, tráfico de un socio, alineación de cron jobs).
  • Escribe un cambio preventivo que reduzca la amplificación (pooling, caching, reescritura de consultas).
  • Audita cambios de topología (réplicas, tamaños de instancia) y revierte cualquier capacidad temporal.
  • Revisa drivers de crecimiento de almacenamiento (bloat, retención de WAL, transacciones largas) y programa remediación.

Preguntas frecuentes

¿Aurora siempre es más caro que PostgreSQL estándar?

No. Aurora puede ser más barato cuando valoras durabilidad gestionada, failover rápido y operaciones predecibles—especialmente si de otro modo sobredimensionarías EC2 y almacenamiento para fiabilidad. Puede ser más caro cuando tu carga es intensiva en I/O o ineficiente, porque la facturación basada en consumo expone el desperdicio de inmediato.

¿Cuál es la causa más común de picos de costo?

Amplificación de carga: reintentos + tormentas de conexiones + consultas ineficientes. Un pequeño aumento de latencia dispara reintentos, lo que aumenta la carga y la latencia. El medidor gira mientras depuras.

¿Debería usar Aurora Serverless v2 para cargas con picos?

Úsalo cuando tus picos sean legítimos y ya hayas controlado el comportamiento de conexiones. Si tus picos son mayormente autoinfligidos (timeouts, reintentos, peticiones charlatanas), serverless escalará fielmente con el caos y te cobrará por ello.

¿Las réplicas de lectura reducen los cargos de I/O en Aurora?

A veces, pero no automáticamente. Las réplicas pueden distribuir la carga de lectura, pero también pueden crear más trabajo total si enrutas mal, calientas cachés repetidamente o mantienes réplicas infrautilizadas. Mide I/O de lectura y distribución de consultas antes y después.

¿Por qué no se redujo el almacenamiento después de eliminar datos?

En PostgreSQL, los deletes crean tuplas muertas; el espacio se vuelve reutilizable internamente, no necesariamente se devuelve al almacenamiento subyacente rápidamente o en absoluto sin operaciones pesadas. En Aurora, la historia de “almacenamiento autoescalable” no significa contracción instantánea. Planifica la recuperación de espacio como un proyecto, no como un deseo.

¿Cuáles son los mejores guardarraíles para controlar picos?

Pooling de conexiones, max_connections sensato, statement timeouts y presupuestos de reintento. Estos reducen la cascada clásica donde la base de datos se vuelve más lenta, los clientes entran en pánico y el sistema se come a sí mismo.

¿Cómo sé si estoy limitado por CPU o por I/O?

Mira planes de consulta con BUFFERS, temp bytes y iowait a nivel host (para autogestionado). Los incidentes CPU-bound muestran CPU alta con relativamente pocas lecturas físicas; los I/O-bound muestran lecturas de buffer significativas y derrames temporales, a menudo con CPU moderada.

¿Puedo limitar costos de Aurora directamente?

Puedes limitar comportamientos que generan costos: limitar réplicas, fijar min/max serverless, limitar conexiones y limitar reintentos. La plataforma no te impedirá consumir I/O; tu arquitectura y guardarraíles deben hacerlo.

¿Cuál es la “primera solución” más segura cuando llega un pico?

Reduce la amplificación. Aplica rate-limit o shedding en el endpoint peor, habilita pooling y frena reconexiones/reintentos. Luego optimiza consultas. El escalado viene después, una vez que sepas para qué estás escalando.

¿Cuándo debería elegir PostgreSQL plano sobre Aurora?

Elige autogestionado (o Postgres gestionado simple) cuando quieras un “pago por capacidad” predecible, tengas madurez operacional y tu carga sea lo suficientemente estable como para que sobredimensionar sea aceptable. Elige Aurora cuando valores failover rápido y durabilidad gestionada del almacenamiento y estés dispuesto a diseñar controles de costo alrededor del consumo.

Conclusión: siguientes pasos que realmente reducen sorpresas

Los picos revelan la verdad. Tu factura de base de datos es solo una de las maneras en que lo delata.

Si decides entre PostgreSQL y Aurora PostgreSQL, no lo reduzcas a “gestionado vs no gestionado”. Redúcelo a qué modo de fallo quieres combatir primero. Con Postgres autogestionado, lucharás contra la saturación y la capacidad. Con Aurora, lucharás contra la amplificación y la medición. Puedes ganar cualquiera de las dos batallas, pero necesitas reflejos distintos.

Pasos prácticos:

  1. Instrumenta la realidad a nivel de consultas: habilita pg_stat_statements y crea un dashboard para consultas top por tiempo total, bytes temporales y lecturas de buffers.
  2. Implementa pooling y presupuestos de reintento: trata conexiones y reintentos como controles de costo y de fiabilidad a la vez.
  3. Ajusta vacuum en serio: identifica tablas con mucho churn y dale a autovacuum los recursos y umbrales para seguir el ritmo.
  4. Escribe una política de escalado: réplicas y cambios de tamaño de instancia deben tener un plan de reducción con un responsable y fecha límite.
  5. Haz un drill de picos: simula tu peor hora, luego revisa tanto gráficas de latencia como controladores de costo. Si solo revisas una, la otra te sorprenderá.

Haz eso, y tu próximo pico seguirá siendo estresante—producción siempre lo es—pero al menos no vendrá con un segundo incidente entregado en PDF.

← Anterior
pveperf de Proxmox muestra datos absurdos: cómo medir el rendimiento correctamente
Siguiente →
Enlaces ancla que parecen sitios de documentación: iconos al pasar, offsets y encabezados clicables

Deja un comentario