Debian 13: PostgreSQL se siente “aleatoriamente lento” — las 8 comprobaciones que revelan el verdadero cuello de botella

¿Te fue útil?

“Aleatoriamente lento” es lo que la gente dice cuando aún no tiene una métrica que explique el problema. Tu aplicación va bien durante una hora, luego un pago tarda 12 segundos, y después todo vuelve a la normalidad. Nadie cambió nada (excepto las cosas que definitivamente cambiaron). Estás en Debian 13, PostgreSQL está “saludable”, y aun así la base de datos parece estar pensando en sus elecciones de vida.

Esto no es un problema de sensaciones. Casi siempre es uno de unos pocos cuellos de botella que rotan: latencia de E/S, presión de WAL, esperas por bloqueos, reclaim de memoria, autovacuum, robo de CPU (steal), búsquedas DNS o tormentas de conexiones. El truco es dejar de mirar top y comenzar a ejecutar comprobaciones que separen “consulta lenta” de “sistema lento”.

Guía rápida de diagnóstico (primero/segundo/tercero)

Si estás de guardia, no tienes tiempo para un seminario de filosofía. Necesitas una secuencia que reduzca el espacio de búsqueda rápido, sin “arreglar” accidentalmente el síntoma reiniciando cosas.

Primero: demuestra si es PostgreSQL o el host

  1. Comprueba si las sesiones están esperando. Si tienes mucha actividad en wait_event, probablemente es contención o E/S, no “SQL lento”.
  2. Comprueba la latencia del almacenamiento ahora mismo. Si las lecturas/escrituras suben a decenas o cientos de milisegundos, PostgreSQL parecerá “aleatoriamente lento” sin importar lo buenos que sean tus índices.
  3. Comprueba la cola de ejecución de CPU y el steal. Si estás escaso de CPU (o en un host VM ruidoso), las consultas se estancan incluso cuando la E/S está bien.

Segundo: identifica la categoría del cuello de botella

  1. Bloqueos: las sesiones bloqueadas se acumulan; una transacción mala mantiene un bloqueo y arruina el día de todos.
  2. WAL / commits: “actualizaciones simples son lentas” con mucho tiempo en commit; la latencia apunta al flush de WAL/cache del dispositivo.
  3. Autovacuum: lentitud periódica; la E/S aumenta; la actividad de vacuum o analyze coincide con las quejas de la app.
  4. Conexiones: picos en la creación de conexiones; saltos en CPU/latencia; la base de datos pasa tiempo aceptando/registrando/autenticando.

Tercero: confirma con una métrica “verdad fundamental”

  • Para E/S: await y la utilización en iostat, además de pg_stat_io de PostgreSQL si está disponible.
  • Para bloqueos: pg_locks con el mapeo bloqueador/bloqueado.
  • Para WAL: pg_stat_wal, checkpoints y pg_stat_bgwriter; correlaciona con las escrituras del dispositivo.
  • Para autovacuum: pg_stat_progress_vacuum e indicadores de bloat en tablas.

Una vez que conozcas la categoría, las correcciones se vuelven mecánicas. Hasta entonces, cualquier cambio “de ajuste” es solo una nueva forma de equivocarse.

Hechos y contexto interesantes (por qué sucede esto)

  • Los valores por defecto de durabilidad de PostgreSQL son conservadores. fsync=on y los flushes síncronos de WAL hacen que las mentiras del almacenamiento sean tu problema de latencia, no PostgreSQL.
  • Linux usa con gusto la RAM “libre” para page cache. Eso es bueno… hasta que la presión de memoria dispara reclaim, lo que puede parecer paradas aleatorias.
  • Autovacuum existe porque PostgreSQL usa MVCC. Las versiones antiguas de filas no desaparecen hasta que el vacuum las limpia. Ignóralo y tu lentitud “aleatoria” se vuelve permanente.
  • El ajuste de checkpoints tiene una larga historia de malentendidos. La gente aumenta checkpoint_timeout para “reducir E/S” y crea por accidente gigantescos picos de checkpoint más tarde.
  • Arquitecturas con conexión por petición fueron comunes en pilas web antiguas. Siguen apareciendo en sistemas modernos por pools mal configurados, y siguen dañando.
  • PostgreSQL 9.6 introdujo mejoras importantes en autovacuum. La base de datos mejoró en autogestión, pero aún necesita diseño de tablas sensato y monitorización.
  • El NVMe moderno puede ser rápido y aun así causar paradas. Picos de latencia por recolección de basura del firmware o comportamiento del caché de escritura pueden convertir un “almacenamiento rápido” en “almacenamiento caprichoso”.
  • Los valores por defecto de Debian priorizan la estabilidad. Eso es genial para no explotar; también significa que debes elegir activamente ajustes de rendimiento para un host de base de datos.
  • La compresión y las sumas de comprobación de WAL son trade-offs. Pueden mejorar los patrones de E/S o la seguridad, pero también cambian las características de CPU y latencia.

Una cita para tener a mano cuando te tientes a adivinar: La esperanza no es una estrategia. — General Gordon R. Sullivan.

Las 8 comprobaciones que revelan el verdadero cuello de botella

Comprobación 1: ¿Las consultas son lentas o están esperando?

“Lento” suele ser “esperando”. PostgreSQL distingue trabajo activo de CPU de espera por bloqueos, E/S, flush de WAL, lectura del cliente y una docena de otras cosas. La forma más rápida de dejar de adivinar es mirar wait_event_type y wait_event.

Si la mayoría de las sesiones están active sin esperas, tienes problemas de CPU o de planes de consulta. Si la mayoría están esperando por Lock, la causa raíz es contención de bloqueos. Si esperan por IO o WAL, es almacenamiento y comportamiento del camino de escritura.

Comprobación 2: ¿El host muestra picos de latencia en disco?

“Lentitud aleatoria” es la descripción en lenguaje natural de la varianza de latencia. Las bases de datos odian la varianza. Unas pocas escrituras de 200 ms no parecen graves en gráficos de throughput, pero convierten commits en timeouts y hacen que tu app parezca embrujada.

En Debian 13, usa iostat para visibilidad inmediata. Buscas await y %util. %util alto con await creciente significa que el dispositivo está saturado o acumulando colas. %util bajo con await alto suele indicar problemas subyacentes de almacenamiento (virtualización, controlador, multipath, flushes del caché de escritura).

Comprobación 3: ¿Es WAL el verdadero cuello de botella (latencia de commit)?

Muchos equipos persiguen “consultas lentas” cuando el verdadero dolor son los commits. Si tu carga de trabajo es intensiva en escrituras, cada transacción necesita WAL. Si la latencia de flush de WAL sube, todo lo que hace commit se vuelve lento — incluso las actualizaciones pequeñas.

Síntomas: inserts/updates que normalmente toman milisegundos ocasionalmente tardan segundos, y las lecturas no se ven tan afectadas. Eso apunta lejos de planes de consulta y hacia la ruta de escritura.

Comprobación 4: ¿Están los checkpoints y el background writer provocando picos de E/S?

Los checkpoints son necesarios. También son una fuente común de paradas periódicas cuando están mal ajustados o cuando el almacenamiento no puede absorber ráfagas. PostgreSQL intenta esparcir la E/S de checkpoint, pero todavía puede acumularse bajo presión.

Si ves un “cada N minutos todo se ralentiza” regular, sospecha del comportamiento de checkpoints, autovacuum o trabajos externos (backups, scrubs, rotación de logs). No optimices a ciegas. Mide.

Comprobación 5: ¿Los bloqueos se están acumulando detrás de una transacción?

La contención por bloqueos es la ralentización más “aleatoria” porque depende del timing. Una transacción larga que mantiene un bloqueo por 30 segundos no es un problema… hasta los segundos equivocados.

Tu objetivo es identificar al bloqueador y la lista de víctimas, luego decidir si matar al bloqueador, arreglar la ruta de código o cambiar el aislamiento/comportamiento de locking.

Comprobación 6: ¿Autovacuum compite contigo (o falla silenciosamente)?

Autovacuum es como sacar la basura: solo lo notas cuando nadie lo hizo. Cuando es demasiado agresivo, compite por E/S y CPU con tu carga. Cuando es demasiado débil, el bloat crece, los índices se hinchan y el rendimiento se degrada a cámara lenta.

En un sistema ocupado, un autovacuum mal ajustado puede causar picos periódicos de E/S que parecen aleatorios para los equipos de aplicación pero son extremadamente regulares para quien los grafica.

Comprobación 7: ¿La presión de memoria provoca reclaim o swapping?

PostgreSQL más la page cache de Linux más “un sidecar más” pueden empujar un host al reclaim. Verás uso de CPU, pero no es CPU útil; es el kernel intentando encontrar memoria. La base de datos “se ralentiza aleatoriamente” porque literalmente espera a que haya páginas de memoria disponibles.

Si alguna vez ves actividad de swap en un host PostgreSQL y no lo planeaste explícitamente, trátalo como una alarma de humo. Puede que no sea un incendio, pero no lo ignores.

Comprobación 8: ¿Tu red/autenticación/DNS está inyectando latencia?

Sí, DNS puede ralentizar PostgreSQL. También pueden hacerlo búsquedas DNS inversas en el logging, fallos de LDAP en autenticación o pérdida de paquetes entre la app y la BD. La base de datos puede estar bien mientras las conexiones se quedan atascadas o la autenticación pausa. “Aleatoriamente lento” desde la perspectiva de la app, perfectamente consistente desde la perspectiva de la red.

Además: tormentas de conexiones. Cuando un pool falla y cada petición abre su propia conexión, PostgreSQL pasa tiempo gestionando procesos y autenticación. Tus consultas no se volvieron más lentas; tu sistema está más ocupado haciendo el trabajo equivocado.

Broma #1: “Lentitud aleatoria” es lo que dice un sistema cuando quiere que instales monitorización pero sigues ofreciéndole buenas vibras.

Tareas prácticas: comandos, significado del resultado, decisiones

A continuación hay tareas prácticas que puedes ejecutar en Debian 13 y en PostgreSQL. Cada una incluye: un comando, qué significa la salida y la decisión que tomas a partir de ello. Ejecuta estas durante una ralentización si puedes; de lo contrario, ejecútalas ahora para establecer líneas base.

Tarea 1: Ver quién está esperando y en qué

cr0x@server:~$ sudo -u postgres psql -X -c "SELECT now(), state, wait_event_type, wait_event, count(*) FROM pg_stat_activity GROUP BY 1,2,3,4 ORDER BY count(*) DESC;"
              now              | state  | wait_event_type | wait_event | count
-------------------------------+--------+-----------------+------------+-------
 2025-12-29 10:14:02.12345+00 | active |                 |            |    12
 2025-12-29 10:14:02.12345+00 | active | IO              | DataFileRead |     7
 2025-12-29 10:14:02.12345+00 | active | Lock            | relation   |     5
 2025-12-29 10:14:02.12345+00 | idle   | Client          | ClientRead |    30

Qué significa: Estás categorizando el dolor. IO/DataFileRead sugiere latencia de lectura o fallos de caché. Lock/relation sugiere contención. Mucho ClientRead significa que los clientes están inactivos, no la BD.

Decisión: Si las esperas se concentran en un tipo, ve directamente a la comprobación relevante (E/S, bloqueos, WAL). Si está mayormente active sin esperas, enfócate en CPU y planes de consulta.

Tarea 2: Identificar la consulta actualmente en peor estado

cr0x@server:~$ sudo -u postgres psql -X -c "SELECT pid, now()-query_start AS runtime, state, wait_event_type, wait_event, left(query, 120) AS query FROM pg_stat_activity WHERE state <> 'idle' ORDER BY runtime DESC LIMIT 5;"
 pid  |  runtime  | state  | wait_event_type |  wait_event  |                         query
------+-----------+--------+-----------------+--------------+--------------------------------------------------------
 8421 | 00:02:31  | active | Lock            | transactionid| UPDATE orders SET status = $1 WHERE id = $2
 9112 | 00:01:04  | active | IO              | DataFileRead | SELECT ... FROM order_items WHERE order_id = $1

Qué significa: Si la consulta con mayor runtime está esperando por un bloqueo, no es “SQL lento”; está bloqueada. Si espera por E/S, buscas almacenamiento o comportamiento de caché.

Decisión: Para esperas por bloqueo, encuentra al bloqueador (Tarea 7). Para esperas por E/S, comprueba la latencia del dispositivo (Tarea 4) y la proporción de acierto de caché (Tarea 10).

Tarea 3: Comprobación rápida de carga del host y cola de ejecución

cr0x@server:~$ uptime
 10:14:10 up 38 days,  2:11,  3 users,  load average: 18.42, 17.90, 16.12

Qué significa: El promedio de carga no es uso de CPU; es tareas ejecutables + no interrumpibles. Una carga de 18 en una máquina de 8 núcleos suele significar que estás en cola por CPU o atrapado en esperas de E/S.

Decisión: Correlaciona con la Tarea 4 (E/S) y la Tarea 5 (CPU). Carga alta + mucha espera de E/S apunta a almacenamiento. Carga alta + mucha CPU apunta a cómputo o planes de consulta.

Tarea 4: Medir latencia y saturación de disco con iostat

cr0x@server:~$ sudo iostat -xz 1 5
Linux 6.12.0-debian (db01) 	12/29/2025 	_x86_64_	(16 CPU)

avg-cpu:  %user   %nice %system %iowait  %steal   %idle
          22.11    0.00    6.90   18.30    0.00   52.69

Device            r/s     rkB/s   rrqm/s  %rrqm r_await rareq-sz     w/s     wkB/s   w_await wareq-sz  aqu-sz  %util
nvme0n1         820.0  55200.0     0.0    0.0   12.4    67.3     610.0   48800.0   45.8    80.0    42.3  98.7

Qué significa: w_await alrededor de ~46ms con %util ~99% es una señal roja. Los commits y checkpoints de PostgreSQL se sentirán lentos. %iowait también está alto.

Decisión: Trata el almacenamiento como sospechoso principal. Siguientes pasos: comprobar si es el mismo dispositivo que PGDATA, confirmar ajustes de caché de escritura, investigar E/S competidora y verificar opciones de montaje del sistema de archivos.

Tarea 5: Comprobar rápidamente saturación de CPU vs espera de E/S

cr0x@server:~$ mpstat -P ALL 1 3
Linux 6.12.0-debian (db01) 	12/29/2025 	_x86_64_	(16 CPU)

12:14:20 PM  CPU   %usr  %sys  %iowait  %steal  %idle
12:14:21 PM  all   28.0   7.0    2.0     0.0    63.0
12:14:21 PM   3   92.0   5.0    0.0     0.0     3.0
12:14:21 PM   7   88.0   6.0    0.0     0.0     6.0

Qué significa: Unos pocos CPUs calientes pueden indicar un cuello de botella de un solo hilo (a menudo una consulta, una construcción de índice, un worker de autovacuum, o una ruta de escritor WAL). %steal distinto de cero en VMs indica vecinos ruidosos.

Decisión: Si la CPU está al máximo con baja espera de E/S, inspecciona las consultas y planes. Si el steal es alto, involucra al equipo de virtualización/plataforma.

Tarea 6: Detectar presión de memoria e intercambio

cr0x@server:~$ free -h
               total        used        free      shared  buff/cache   available
Mem:            64Gi        51Gi       1.2Gi       1.1Gi        12Gi        7.8Gi
Swap:            8Gi       2.5Gi       5.5Gi

Qué significa: El uso de swap en un host DB es un impuesto de rendimiento. Puede ser histórico (se swappeó una vez y nunca se volvió a cargar) o thrashing activo.

Decisión: Confirma la actividad de swap con vmstat (Tarea 12). Si hay swapping activo, reduce la presión de memoria: baja shared_buffers si es excesivo, reduce el número de conexiones, detén cargas co-ubicadas o añade RAM.

Tarea 7: Encontrar bloqueadores y víctimas (contención de bloqueo)

cr0x@server:~$ sudo -u postgres psql -X -c "
WITH locked AS (
  SELECT pid, locktype, relation::regclass AS rel, transactionid, mode, granted
  FROM pg_locks
),
activity AS (
  SELECT pid, usename, application_name, client_addr, state, now()-xact_start AS xact_age, left(query,120) AS query
  FROM pg_stat_activity
)
SELECT a.pid AS blocked_pid, a.usename, a.application_name, a.xact_age, a.query,
       b.pid AS blocker_pid, b.usename AS blocker_user, b.application_name AS blocker_app, b.xact_age AS blocker_xact_age, b.query AS blocker_query
FROM locked l1
JOIN locked l2 ON l1.locktype = l2.locktype
  AND coalesce(l1.rel::text,'') = coalesce(l2.rel::text,'')
  AND coalesce(l1.transactionid::text,'') = coalesce(l2.transactionid::text,'')
  AND l1.pid <> l2.pid
JOIN activity a ON a.pid = l1.pid
JOIN activity b ON b.pid = l2.pid
WHERE NOT l1.granted AND l2.granted
ORDER BY a.xact_age DESC;
"
 blocked_pid | usename | application_name | xact_age |                        query                        | blocker_pid | blocker_user | blocker_app | blocker_xact_age |           blocker_query
------------+---------+------------------+----------+-----------------------------------------------------+------------+--------------+-------------+------------------+----------------------------------
       8421 | app     | api              | 00:02:31 | UPDATE orders SET status = $1 WHERE id = $2         |       7710 | app          | worker      | 00:18:09         | UPDATE orders SET ... WHERE ...

Qué significa: Una transacción larga está bloqueando a otras. El bloqueador lleva 18 minutos en transacción. Rara vez eso está bien en OLTP.

Decisión: Decide si terminar al bloqueador (con cuidado: hará rollback), o arreglar el comportamiento de la app (índice faltante que provoca update largo, alcance de transacción demasiado amplio, espera por llamada externa dentro de la transacción).

Tarea 8: Inspeccionar la salud de WAL y checkpoints

cr0x@server:~$ sudo -u postgres psql -X -c "SELECT * FROM pg_stat_bgwriter;"
 checkpoints_timed | checkpoints_req | checkpoint_write_time | checkpoint_sync_time | buffers_checkpoint | buffers_clean | maxwritten_clean | buffers_backend | buffers_backend_fsync | buffers_alloc
------------------+-----------------+-----------------------+----------------------+--------------------+---------------+------------------+----------------+-----------------------+--------------
             1289 |             412 |               8123456 |               923456 |           73456789 |       1234567 |             9876 |        2345678 |                 12345 |     987654321

Qué significa: Un checkpoints_req alto sugiere que fuerzas checkpoints por volumen de WAL, no por la programación. Tiempos altos de escritura/sincronización de checkpoint correlacionan con paradas de E/S.

Decisión: Si los checkpoints solicitados son frecuentes, considera aumentar max_wal_size y asegurar que checkpoint_completion_target sea sensato. Pero no “tunes” hasta confirmar la capacidad de E/S y el comportamiento del dispositivo WAL.

Tarea 9: Ver estadísticas de WAL (cuando estén disponibles)

cr0x@server:~$ sudo -u postgres psql -X -c "SELECT wal_records, wal_fpi, wal_bytes, wal_buffers_full, wal_write, wal_sync, stats_reset FROM pg_stat_wal;"
 wal_records | wal_fpi | wal_bytes | wal_buffers_full | wal_write | wal_sync |        stats_reset
------------+---------+-----------+------------------+-----------+----------+-------------------------------
   88234567 |  123456 | 98 GB     |             4212 |     91234 |    45678 | 2025-12-27 00:00:00+00

Qué significa: wal_buffers_full frecuente sugiere presión en el buffer de WAL. Altos recuentos de WAL write/sync no son inherentemente malos; correlaciónalos con la latencia del dispositivo y los tiempos de commit.

Decisión: Si la presión de WAL coincide con paradas, considera almacenamiento rápido y dedicado para WAL, valida las expectativas de synchronous_commit e inspecciona la latencia de commit en la app.

Tarea 10: Comprobar la proporción de aciertos de caché (con etiqueta de advertencia)

cr0x@server:~$ sudo -u postgres psql -X -c "SELECT datname, blks_hit, blks_read, round(100.0*blks_hit/GREATEST(blks_hit+blks_read,1),2) AS hit_pct FROM pg_stat_database ORDER BY hit_pct ASC;"
 datname  | blks_hit  | blks_read | hit_pct
----------+-----------+-----------+---------
 appdb    | 987654321 | 45678901  | 95.59

Qué significa: La proporción de aciertos no es un KPI para adorar; es una pista. 95% puede ser adecuado o terrible dependiendo de la carga. Una caída repentina durante incidentes importa más que el número absoluto.

Decisión: Si la proporción de aciertos baja durante la lentitud y la latencia de E/S sube, estás vertiendo a disco. Investiga memoria, tamaño del working set y si los patrones de consulta cambiaron.

Tarea 11: Encontrar los mayores consumidores de tiempo con pg_stat_statements

cr0x@server:~$ sudo -u postgres psql -X -c "SELECT queryid, calls, round(total_exec_time::numeric,1) AS total_ms, round(mean_exec_time::numeric,2) AS mean_ms, rows, left(query,120) AS query FROM pg_stat_statements ORDER BY total_exec_time DESC LIMIT 10;"
 queryid  | calls  | total_ms | mean_ms | rows  |                          query
----------+--------+----------+---------+-------+--------------------------------------------------------
 12345678 | 998765 | 8123456.0|   8.13  | 998765| SELECT * FROM users WHERE email = $1
 88776655 |  12345 | 6234567.0| 505.12  |  12345| SELECT ... FROM orders JOIN ... WHERE created_at > $1

Qué significa: total_exec_time identifica el mayor coste agregado. Consultas con media alta son asesinas de latencia; consultas con tiempo total alto son asesinas de capacidad.

Decisión: Para consultas de mean alto, ejecuta EXPLAIN (ANALYZE, BUFFERS) en un entorno seguro. Para total alto, considera caching, índices o cambios en la consulta. Si el tiempo de consulta solo sube durante incidentes, correlaciónalo primero con esperas y métricas del host.

Tarea 12: Confirmar swapping activo o reclaim con vmstat

cr0x@server:~$ vmstat 1 5
procs -----------memory---------- ---swap-- -----io---- -system-- ------cpu-----
 r  b   swpd   free   buff  cache   si   so    bi    bo   in   cs us sy id wa
 5  2 262144 312000  80000 9000000  12   38   120   890  900 2400 18  6 55 21
 7  1 262200 298000  76000 8800000   0   52    80  1200 1100 2700 20  7 48 25

Qué significa: si/so distinto de cero significa swapping in/out. Eso no es “tal vez.” Eso es “tu base de datos está pagando alquiler al dispositivo de swap.” Observa también el b (bloqueado) y los patrones de wa (espera de E/S).

Decisión: Si el swapping está activo, deja de afinar SQL y arregla la presión de memoria. Reduce concurrencia, añade RAM, ajusta la colocación de cargas o detén procesos fuera de control.

Tarea 13: Comprobar el sistema de archivos y opciones de montaje para PGDATA

cr0x@server:~$ findmnt -no SOURCE,FSTYPE,OPTIONS /var/lib/postgresql
/dev/mapper/vg0-pgdata ext4 rw,relatime,errors=remount-ro

Qué significa: Quieres saber en qué sistema de archivos estás y si las opciones de montaje son extrañas. Para PostgreSQL, las opciones “ingeniosas” de montaje normalmente terminan en informes de incidentes.

Decisión: Si ves opciones exóticas (como deshabilitar barriers) sin una razón sólida y hardware validado, vuelve a valores sensatos y arregla el problema real de rendimiento en otro lugar.

Tarea 14: Validar que PostgreSQL no está siendo limitado por cgroups

cr0x@server:~$ systemctl show postgresql --property=CPUQuota,MemoryMax,IOReadBandwidthMax,IOWriteBandwidthMax
CPUQuota=
MemoryMax=infinity
IOReadBandwidthMax=
IOWriteBandwidthMax=

Qué significa: Si hay cuotas establecidas (especialmente límites de ancho de banda de E/S), puedes tener una “lentitud aleatoria” autoinfligida cuando la carga sube y los límites se activan.

Decisión: Elimina límites inapropiados para un servicio de base de datos, o redimensiónalos deliberadamente y fija expectativas. Las bases de datos no funcionan bien con dietas sorpresa.

Tarea 15: Revisar logs de PostgreSQL por picos de checkpoint, autovacuum y duraciones

cr0x@server:~$ sudo journalctl -u postgresql@15-main -S -2h | egrep -i "checkpoint|autovacuum|duration|could not|timeout" | tail -n 15
Dec 29 09:02:10 db01 postgres[2210]: LOG:  checkpoint complete: wrote 32145 buffers (1.9%); 0 WAL file(s) added, 2 removed, 1 recycled; write=78.123 s, sync=1.992 s, total=80.256 s
Dec 29 09:10:44 db01 postgres[2210]: LOG:  automatic vacuum of table "app.public.orders": index scans: 1 pages: 0 removed, 123456 remain, 12000 scanned (9.72% of total) tuples: 0 removed, 500000 remain
Dec 29 09:11:02 db01 postgres[2210]: LOG:  duration: 2412.889 ms  execute <unnamed>: UPDATE orders SET ...

Qué significa: Un checkpoint que dura 80 segundos no es sutil. El autovacuum cerca de ventanas de incidentes puede ser causal o solo correlacionado; aún necesitas métricas de E/S.

Decisión: Si los tiempos de checkpoint son enormes, prioriza el rendimiento del almacenamiento y el ajuste de checkpoints. Si los picos de duración se alinean con esperas por bloqueo, arregla el locking. Si el autovacuum es pesado, ajusta por tabla y comprueba el bloat.

Tarea 16: Confirmar churn de conexiones (demasiadas conexiones nuevas)

cr0x@server:~$ sudo -u postgres psql -X -c "SELECT datname, numbackends, xact_commit, xact_rollback, blks_read, blks_hit FROM pg_stat_database;"
 datname | numbackends | xact_commit | xact_rollback | blks_read | blks_hit
--------+-------------+-------------+---------------+----------+----------
 appdb  |         240 |   987654321 |        123456 | 45678901 | 987654321

Qué significa: numbackends es conexiones actuales. Picos aquí, especialmente con alta CPU y cambios de contexto, a menudo significan un pool roto o un patrón de tráfico inesperado.

Decisión: Si las conexiones son altas, limítalas y aplica pooling. Considera pgbouncer, pero no lo instales como respuesta de pánico; configúralo con cuidado y valida la semántica de transacciones.

Broma #2: Un ajuste de depuración “temporal” en producción tiene la misma vida media que una mancha de café en una camisa blanca.

Tres mini-historias corporativas (qué falló, qué funcionó)

Mini-historia 1: El incidente causado por una asunción errónea

Una compañía SaaS de tamaño medio migró su host de base de datos principal de SSDs SATA antiguos a flamantes NVMe. La ventana de cambio fue limpia. Los benchmarks se veían geniales. Todos esperaban menos incidentes.

Dos semanas después, la cola de soporte empezó a describir “lentitud aleatoria” durante tráfico pico: pagos que caducaban, jobs background reintentando y ocasionales 504 en la API. Las métricas de base de datos no gritaban. La CPU no estaba al máximo. La red parecía bien. La explicación por defecto del equipo se volvió “Postgres es raro bajo carga”, que es el equivalente ingenieril de encogerse de hombros en una hoja de cálculo.

La asunción equivocada: “NVMe es rápido, así que el almacenamiento no puede ser el cuello de botella.” Habían mirado throughput, no la varianza de latencia. Bajo presión de escritura sostenida, el dispositivo ocasionalmente hacía picos de latencia de escritura a cientos de milisegundos por comportamiento interno. A PostgreSQL no le importa que tu throughput medio sea genial; le importa que un fsync haya tardado 400 ms y haya bloqueado un montón de commits detrás.

Lo demostraron capturando iostat -xz 1 durante un incidente y correlacionándolo con logs de transacciones lentas. Los picos coincidían perfectamente. Una vez que movieron WAL a un dispositivo dedicado y ajustaron el dimensionamiento de checkpoint/WAL para reducir ráfagas, la “lentitud aleatoria” se volvió predecible y aburrida.

Conclusión: deja de tratar el almacenamiento como binario (rápido/lento). Para bases de datos, lo que importa es la varianza.

Mini-historia 2: La optimización que salió mal

Una plataforma del ámbito financiero tenía paradas periódicas cada 15 minutos. Alguien notó checkpoints en los logs y hizo lo que la internet suele sugerir: aumentó checkpoint_timeout y max_wal_size drásticamente para “reducir la frecuencia de checkpoints”. Funcionó—en parte.

Los checkpoints ocurrieron menos veces, así que el gráfico se vio más suave por un tiempo. Pero cuando los checkpoints se ejecutaban, eran masivos. El subsistema de E/S no estaba construido para esa clase de ráfagas. Durante esos checkpoints, la latencia se disparaba y la aplicación acumulaba reintentos, causando aún más presión de escritura. El incidente empeoró: menos frecuente y más difícil de reproducir en staging. Encantador.

Cuando finalmente midieron lo correcto—latencia del dispositivo y checkpoint_write_time—el patrón fue obvio. La “optimización” había redistribuido el dolor en menos, pero más grandes, explosiones. Revirtieron a un checkpoint_timeout menor, aumentaron checkpoint_completion_target para suavizar la E/S y dimensionaron bien el WAL para que los checkpoints no fueran forzados por el churn de WAL.

También descubrieron un problema de segundo orden: un job analítico semanal hacía grandes updates sin batching, creando picos de WAL que forzaban checkpoints independientemente del timeout. Arreglar ese job redujo tanto E/S como volumen de WAL más que cualquier perilla.

Conclusión: “menos checkpoints” no es automáticamente “mejor”. El objetivo es E/S estable, no desastres raros.

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

Un equipo interno empresarial gestionaba PostgreSQL para varias unidades de negocio. Nada glamuroso: integraciones de nómina, flujos de trabajo de RRHH, compras. Su mejor característica era que los incidentes eran raros y cortos.

Lo que hacían no era magia. Mantenían un runbook escrito con una secuencia rápida de diagnóstico: comprobar esperas, latencia de E/S, bloqueos, autovacuum, presión de memoria y luego decidir. También tenían líneas base: latencia “normal” de iostat, cuentas típicas de conexiones y tiempos esperados de checkpoint. Cada trimestre hacían un simulacro de 30 minutos donde alguien fingía que la base de datos estaba lenta y todos practicaban reunir evidencia sin reiniciar servicios.

Cuando una actualización de un agente de vendor empezó a martillar el disco con escrituras de logs, el equipo no discutió de quién era la culpa. Vieron subir la latencia de E/S, confirmaron que PostgreSQL esperaba por E/S, identificaron el proceso ofensivo con estadísticas de E/S por proceso y limitaron el agente. El negocio no lo notó, salvo porque su página de estado se mantuvo aburrida.

Conclusión: la disciplina aburrida vence al tuning heroico. Líneas base más un playbook practicado convierten la “lentitud aleatoria” en “un ticket con evidencia”.

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

Esta sección es intencionalmente específica. Si reconoces el síntoma, no lo debatas en una reunión—ve a probar la causa raíz.

1) Síntoma: “Todo está lento, pero la CPU está baja”

  • Causa raíz: Esperas de E/S (picos de latencia de almacenamiento) o contención por bloqueos.
  • Solución: Revisa pg_stat_activity waits y iostat -xz. Si es E/S: reduce E/S competidora, mueve WAL, verifica la salud del almacenamiento. Si son bloqueos: encuentra y arregla al bloqueador; acorta transacciones.

2) Síntoma: Las escrituras son lentas; las lecturas parecen normales

  • Causa raíz: Latencia de flush de WAL, checkpoints, o replicación síncrona esperando (si está configurada).
  • Solución: Inspecciona pg_stat_wal, estadísticas de checkpoints y latencia de escritura del dispositivo. Valida que WAL esté en almacenamiento rápido y estable. No desactives la durabilidad como “arreglo de rendimiento” a menos que aceptes pérdida de datos.

3) Síntoma: Lentitud periódica en intervalos regulares

  • Causa raíz: Checkpoints, ciclos de autovacuum, cron jobs, scrubs, backups, rotación de logs o trabajos por lotes.
  • Solución: Correlaciona ventanas de tiempo con logs (journalctl) y métricas. Suaviza la E/S de checkpoints; ajusta autovacuum por tabla; reprograma trabajos en lote; aísla la E/S de backups.

4) Síntoma: Picos en el recuento de conexiones y latencia durante ráfagas de tráfico

  • Causa raíz: Falla en el pooling de conexiones o un despliegue de cliente nuevo que abre demasiadas sesiones.
  • Solución: Aplica pooling y limita max_connections a lo que tu RAM puede soportar. Usa un pooler si es necesario, pero pruébalo; configura la semántica de transacciones y prepared statements.

5) Síntoma: “Timeouts aleatorios”, pero solo en ciertos endpoints

  • Causa raíz: Un plan de consulta que cambia entre index scan y seq scan, o un plan sensible a parámetros.
  • Solución: Usa pg_stat_statements para encontrar al culpable y ejecuta EXPLAIN (ANALYZE, BUFFERS) con parámetros representativos. Considera ajustar plan_cache_mode con cautela o reescribir consultas para más estabilidad.

6) Síntoma: Vacuum ejecutándose constantemente, pero el rendimiento sigue degradándose

  • Causa raíz: Autovacuum no da abasto, o el bloat ya es grande; transacciones largas impiden la limpieza.
  • Solución: Identifica transacciones largas, reduce su duración y ajusta autovacuum por tabla. Para bloat severo, planifica mantenimiento (por ejemplo, VACUUM (FULL) o estrategias de reconstrucción online) con un plan real.

7) Síntoma: Después de actualizar Debian, PostgreSQL se siente más lento bajo la misma carga

  • Causa raíz: Cambios en kernel/scheduler de E/S, valores por defecto diferentes, ajustes de cgroup, o comportamiento distinto del sistema de archivos; a veces simplemente se revela un nuevo cuello de botella.
  • Solución: Compara líneas base: latencia de E/S, steal de CPU, reclaim de memoria y eventos de espera de PostgreSQL. Verifica que no se introdujeron nuevos límites de servicio y que huge pages, THP o cambios de governor no alteraron el comportamiento.

Listas de verificación / plan paso a paso

Paso a paso: durante una ralentización activa (10 minutos, sin heroísmos)

  1. Captura esperas de PostgreSQL: ejecuta la Tarea 1 y guarda la salida.
  2. Captura las consultas activas peores: ejecuta la Tarea 2.
  3. Captura latencia de E/S: ejecuta la Tarea 4.
  4. Captura saturación de CPU: ejecuta la Tarea 5.
  5. Captura presión de memoria: ejecuta la Tarea 6 y la Tarea 12.
  6. Si aparecen bloqueos: ejecuta la Tarea 7 y decide sobre terminar al bloqueador o esperar.
  7. Si se sospecha WAL/checkpoint: ejecuta la Tarea 8 y la Tarea 9; correlaciónalas con escrituras de E/S y logs (Tarea 15).
  8. Si las conexiones están altas: ejecuta la Tarea 16; verifica salud del pool y cambios de despliegue de la app.
  9. Anota límites temporales: “lento de 10:12:40 a 10:16:10”. La correlación necesita timestamps.

Paso a paso: después del incidente (la parte que los equipos omiten y luego repiten incidentes)

  1. Clasifica el incidente: IO-bound, lock-bound, CPU-bound, memory-bound, WAL-bound, connection-bound o network/auth-bound.
  2. Elige una métrica principal para alertar: por ejemplo, await de disco por encima del umbral, recuento de esperas por bloqueo, tiempo de checkpoint, actividad de swap.
  3. Añade un panel al dashboard que lo habría hecho obvio. No doce. Uno.
  4. Haz un cambio de código/configuración con plan de rollback. No “ajustes todo”.
  5. Actualiza el runbook. Si la solución requirió conocimiento tribal, tienes un bug de fiabilidad.

Preguntas frecuentes

1) ¿Por qué PostgreSQL se siente lento incluso cuando el uso de CPU es bajo?

Porque a menudo está esperando, no calculando. Las esperas por bloqueos y por E/S no consumen CPU. Mira wait_event_type en pg_stat_activity y la latencia de disco con iostat.

2) ¿Qué números de “await” son malos en iostat?

Para OLTP, milisegundos sostenidos de un dígito suelen estar bien; decenas de milisegundos sostenidos son problemáticos; picos en cientos de milisegundos serán visibles para el usuario. El umbral exacto depende de los SLO, pero “await subiendo con %util cerca del 100%” es una firma clásica de saturación.

3) ¿Debo aumentar shared_buffers para arreglar la lentitud?

No como reflejo. Demasiado shared_buffers puede dejar al OS sin page cache y empujar al sistema al reclaim o swap. Dimensiona intencionalmente y verifica la presión de memoria con vmstat y free.

4) ¿Autovacuum debe causar caídas de rendimiento?

Pudo hacerlo, especialmente cuando tiene que ponerse al día. El objetivo es ajustarlo para que corra continuamente y discretamente en lugar de ocasional y violentamente. Ajustes por tabla suelen ser la herramienta correcta.

5) ¿Los checkpoints pueden causar “cada N minutos todo se ralentiza”?

Sí. Los checkpoints pueden crear ráfagas de escrituras. Si el almacenamiento no puede absorber la ráfaga, la latencia se dispara. Mira el tiempo de checkpoint en logs y pg_stat_bgwriter, luego correlaciónalo con la latencia del dispositivo.

6) ¿Poner synchronous_commit=off es una buena solución para la lentitud aleatoria?

Es un trade-off: reduces la latencia de commit aceptando posible pérdida de datos en un crash. Para algunas cargas es aceptable; para muchas no. Trátalo como una decisión explícita de producto, no como un ajuste de rendimiento.

7) ¿Cómo sé si los bloqueos son el problema?

Si muchas sesiones están esperando por Lock y puedes identificar un bloqueador que mantiene bloqueos mucho tiempo (Tarea 7), los bloqueos son el problema. Arregla transacciones largas y patrones de hot-row contention.

8) ¿Por qué las tormentas de conexiones hacen tanto daño?

Cada conexión cuesta memoria, CPU y a menudo dispara trabajo de auth/logging. En condiciones de tormenta, la base de datos gasta recursos gestionando conexiones en lugar de ejecutar consultas. Arregla en el cliente con pooling y límites sensatos.

9) ¿Cambia Debian 13 algo que pueda afectar la latencia de PostgreSQL?

Las actualizaciones de SO pueden cambiar el comportamiento del kernel, el scheduler de E/S y los valores por defecto de servicios y cgroups. Trátalo como una nueva línea base: valida latencia de E/S, steal de CPU y reclaim de memoria bajo carga representativa en lugar de asumir “igual pero más nuevo”.

10) ¿Cuál es la extensión de PostgreSQL más útil para triage de rendimiento?

pg_stat_statements. No te dirá la latencia de almacenamiento directamente, pero rápidamente identificará si unas pocas consultas dominan el tiempo y si la “lentitud aleatoria” es en realidad un patrón de consultas conocido.

Conclusión: siguientes pasos que puedes hacer hoy

PostgreSQL no es aleatoriamente lento. Tu sistema lo es. La base de datos solo resulta ser donde los usuarios lo sienten primero.

  1. Implementa la guía rápida. Haz de “esperas, latencia de E/S, bloqueos” las tres primeras comprobaciones. Practícalo una vez.
  2. Establece líneas base de tu host. Ejecuta iostat, vmstat y cuenta de conexiones durante carga normal y guarda los números.
  3. Convierte la mayor incógnita en una métrica. Si no sabes si los incidentes son IO-bound o lock-bound, no tienes respuesta de incidentes; tienes superstición.
  4. Arregla un cuello de botella a la vez. La forma más rápida de alargar un outage es cambiar cinco perillas y luego discutir cuál ayudó.

Si no haces nada más: la próxima vez que alguien diga “Postgres está aleatoriamente lento”, responde pidiendo una franja horaria y ejecuta la Tarea 1 y la Tarea 4. Parecerás psíquico. No lo eres. Simplemente estás midiendo las cosas correctas.

← Anterior
ZFS SAS vs SATA timeouts: por qué SAS parece ‘más estable’ bajo estrés
Siguiente →
Por qué Intel adoptó AMD64 (y por qué cambió todo)

Deja un comentario