Autovacuum de PostgreSQL en Ubuntu 24.04: cómo ajustarlo de forma segura ante la “lentitud misteriosa”

¿Te fue útil?

Autovacuum debería ser el conserje silencioso. En Ubuntu 24.04, a veces aparece como una banda de música: picos de latencia, sesiones “idle in transaction” acumuladas, discos muy ocupados pero nada “usando CPU”, y tu equipo de aplicaciones jura que no cambiaron nada.

La trampa es asumir que autovacuum está “bien” o que es “el problema”. En la práctica es un amplificador de síntomas. Convierte bloat existente, patrones de consulta malos y techos de I/O en dolor muy visible. La buena noticia: puedes volverlo predecible sin convertir tu base de datos en un experimento científico.

Cómo se manifiesta la “lentitud misteriosa” en producción

La lentitud de autovacuum rara vez se presenta como “vacuum tarda mucho”. Se presenta como que todo lo demás tarda mucho.

  • Consultas de lectura que antes eran rápidas empiezan a generar más I/O aleatorio. Ves cómo sube shared read, baja la ratio de cache hits, y tu p99 se desploma.
  • Tablas con muchas escrituras muestran heap bloat y index bloat. el conjunto de trabajo ya no cabe en RAM. Tu SSD se comporta como si fuera un disco rotatorio.
  • Los workers de autovacuum parecen “activos” pero el throughput es bajo. Los wait events muestran I/O o locks, y la vista de progreso de vacuum parece atascada.
  • Ocasionalmente aparece el comportamiento del estilo “cannot vacuum … because it contains tuples still needed”: no es un error, sino una señal de transacciones de larga duración que están anclando la limpieza.
  • Y el clásico: la presión de wraparound se acerca sigilosamente. De repente vacuum deja de ser opcional y pasa a ser existencial.

Una verdad seca: autovacuum no está lento porque sea perezoso. Está lento porque es educado—salvo que lo forces a no serlo.

Guía rápida de diagnóstico (haz esto primero)

Esta es la secuencia de “necesito señales en 10 minutos”. Hazla en orden. Cada paso estrecha la categoría del cuello de botella antes de tocar cualquier control.

1) ¿El problema es I/O, locks o wraparound?

  • Si el I/O del sistema está saturado y los wait events de Postgres muestran esperas por I/O → estás mayoritariamente limitado por almacenamiento.
  • Si autovacuum está bloqueado por locks o no puede avanzar debido a transacciones largas → estás mayoritariamente limitado por transacciones/bloqueos.
  • Si datfrozenxid presenta alta edad → estás en modo de prevención de wraparound. El rendimiento pasa a ser secundario frente a la supervivencia.

2) Identifica las tablas más calientes, no solo “autovacuum”

Autovacuum actúa tabla por tabla. Una tabla de 200 GB con un patrón de actualizaciones malo puede crear la ilusión de que “toda la base de datos está lenta”. Encuentra la tabla.

3) Verifica si autovacuum está subaprovisionado o intencionalmente limitado

Workers, cost delay y cost limit determinan cuánto empuja vacuum. En muchos sistemas de producción, los valores por defecto son seguros pero demasiado suaves para tasas de escritura modernas.

4) Valida los sospechosos de “anclaje”

Transacciones de larga duración, replication slots lógicos y sesiones abandonadas pueden impedir la limpieza. Si no solucionas eso, afinar autovacuum es como comprar una fregona más rápida para un suelo que aún está inundándose.

Broma #1: autovacuum es el único empleado que limpia los desordenes de los demás y aun así lo culpan por el olor.

Cómo autovacuum realmente emplea su tiempo

Vacuum no es un único trabajo. Es un conjunto de tareas que compiten con tu carga:

  • Heap scan: leer páginas de la tabla para encontrar tuplas muertas y marcar espacio reutilizable.
  • Index cleanup: eliminar entradas muertas de índices (a menos que se omita por elecciones heurísticas de index vacuuming).
  • Freezing: marcar transaction IDs antiguas como frozen para prevenir wraparound.
  • Visibility map updates: habilitar index-only scans registrando páginas all-visible/all-frozen.
  • ANALYZE: actualizar estadísticas del planner (autovacuum a menudo también corre analyze).

La “lentitud misteriosa” suele pasar cuando el trabajo cambia de limpieza incremental a limpieza de puesta al día. Una vez que el bloat crece, vacuum lee y escribe más páginas, toca más índices y genera más I/O aleatorio. Eso aumenta la latencia para todo lo demás, lo que incrementa la duración de las transacciones, lo que genera más tuplas muertas… y el ciclo se retroalimenta.

Qué hace que vacuum sea lento incluso cuando “está corriendo”

  • Demora basada en coste: vacuum se duerme intencionalmente. Es un comportamiento normal.
  • Saturación de I/O: vacuum está realizando lecturas/escrituras reales pero el almacenamiento está en su límite.
  • Agitación de la caché de buffers: vacuum desplaza páginas calientes, haciendo que las lecturas de la aplicación fallen en caché con más frecuencia.
  • Conflictos de locks: VACUUM regular no toma locks pesados, pero necesita un modo de lock que puede ser bloqueado por cierto DDL y casos límite. VACUUM FULL es otra bestia y arruinará tu tarde.
  • Transacciones de larga duración: vacuum no puede eliminar tuplas que siguen siendo visibles para snapshots antiguos.

Particularidades de Ubuntu 24.04 que importan

Ubuntu 24.04 no es intrínsecamente “peor” para Postgres, pero es una base Linux moderna con valores por defecto actuales—algunos útiles, otros sorprendentes.

  • Kernel y pila de I/O: normalmente estás en un kernel 6.x. Los valores por defecto del planificador de I/O y el comportamiento NVMe suelen ser buenos, pero cgroups mal configurados o vecinos ruidosos aún pueden dejar a Postgres sin recursos.
  • Systemd: los servicios pueden ejecutarse con controles de recursos que no configuraste intencionalmente. CPUQuota/IOWeight pueden producir la confusión de “vacuum es lento pero nada está al máximo”.
  • Transparent Huge Pages (THP): a menudo sigue habilitado por defecto en sistemas de propósito general. Puede causar picos de latencia. No es una opción de autovacuum, pero puede hacer que autovacuum parezca culpable.
  • Sistemas de archivos y opciones de montaje: ext4 vs XFS vs ZFS tienen patrones distintos bajo la carga mixta de lectura/escritura de vacuum. Autovacuum no es especial—simplemente toca muchas páginas.

Datos interesantes y contexto (porque la historia se repite)

Estos son pequeños datos concretos que te ayudan a razonar sobre autovacuum sin superstición.

  1. Autovacuum se estandarizó en PostgreSQL 8.1 después de ser un complemento. Antes de eso, muchos sistemas simplemente se degradaban a menos que los humanos ejecutaran vacuum rutinariamente.
  2. MVCC es la razón por la que existe vacuum: Postgres guarda versiones antiguas de filas para concurrencia. La limpieza se difiere por diseño.
  3. Vacuum no “achica” tablas en el caso general. Hace que el espacio sea reutilizable dentro del archivo; el tamaño del archivo suele permanecer igual a menos que uses métodos más invasivos.
  4. Wraparound no es teórico: los transaction IDs son de 32 bits y eventualmente darán la vuelta. Si no haces vacuum/freeze, Postgres se protegerá forzando vacuums agresivos y eventualmente rechazando escrituras.
  5. Hot updates pueden reducir la rotación de índices, pero solo si las columnas actualizadas no están indexadas. Actualiza la columna equivocada y bloat de índices crecerá rápidamente.
  6. Las visibility maps habilitan index-only scans. Vacuum que actualiza las visibility maps puede acelerar lecturas después, aunque cueste I/O ahora.
  7. La demora basada en coste existe para proteger la latencia, no para molestarte. Se diseñó cuando los discos eran más lentos y los entornos compartidos eran comunes.
  8. Los factores de escala de autovacuum estaban pensados para “tablas normales”. Las tablas muy grandes con alta rotación a menudo necesitan anulaciones por tabla; los defaults escalan demasiado linealmente para algunas cargas.
  9. La exactitud de ANALYZE afecta indirectamente el impacto de vacuum: estadísticas malas llevan a planes malos, que generan transacciones más largas, más tuplas muertas y más trabajo de vacuum.

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

Estos son los chequeos de campo que realmente ejecuto. Cada tarea incluye: comando, ejemplo de salida, qué significa y qué decisión tomar.

Task 1: Confirmar versión de Postgres y disposición del cluster

cr0x@server:~$ psql --version
psql (PostgreSQL) 16.3 (Ubuntu 16.3-1.pgdg24.04+1)

Significado: Estás en PG 16.x client tools. En Ubuntu, puedes tener múltiples clusters/versions.

Decisión: Asegúrate de que estés afinando la instancia correcta y editando la ruta de config adecuada para ese cluster.

cr0x@server:~$ pg_lsclusters
Ver Cluster Port Status Owner    Data directory              Log file
16  main    5432 online postgres /var/lib/postgresql/16/main /var/log/postgresql/postgresql-16-main.log

Significado: Cluster único, PG16 main, rutas estándar de Debian/Ubuntu.

Decisión: Sabes dónde buscar logs y qué puerto apuntar.

Task 2: ¿Está autovacuum habilitado y cuáles son los knobs globales?

cr0x@server:~$ sudo -u postgres psql -X -c "SHOW autovacuum; SHOW autovacuum_max_workers; SHOW autovacuum_naptime; SHOW autovacuum_vacuum_cost_limit; SHOW autovacuum_vacuum_cost_delay;"
 autovacuum
------------
 on
(1 row)

 autovacuum_max_workers
-----------------------
 3
(1 row)

 autovacuum_naptime
-------------------
 1min
(1 row)

 autovacuum_vacuum_cost_limit
-----------------------------
 -1
(1 row)

 autovacuum_vacuum_cost_delay
-----------------------------
 2ms
(1 row)

Significado: Autovacuum está activado, solo 3 workers, 1 minuto de pausa entre comprobaciones. Cost limit -1 significa “usar vacuum_cost_limit”.

Decisión: Si tienes muchas tablas activas y bloat, 3 workers suele ser insuficiente. Pero no aumentes a ciegas—primero averigua si estás bloqueado o limitado por I/O.

Task 3: Encontrar workers activos de autovacuum y en qué esperan

cr0x@server:~$ sudo -u postgres psql -X -c "\
SELECT pid, datname, relid::regclass AS table, phase, wait_event_type, wait_event, now()-xact_start AS xact_age \
FROM pg_stat_progress_vacuum v \
JOIN pg_stat_activity a USING (pid) \
ORDER BY xact_age DESC;"
 pid  | datname |        table        |   phase   | wait_event_type | wait_event | xact_age
------+--------+---------------------+-----------+-----------------+------------+----------
 8421 | appdb  | public.events       | scanning  | IO              | DataFileRead | 00:12:41
 8534 | appdb  | public.sessions     | vacuuming indexes | CPU      |            | 00:05:10
(2 rows)

Significado: Un worker está esperando I/O en lecturas; otro está gastando CPU en index vacuuming.

Decisión: Si ves muchas esperas de IO, ajustar cost limits puede aumentar la contención. Considera primero el throughput de almacenamiento y la caché.

Task 4: Comprobar si vacuum está bloqueado por locks

cr0x@server:~$ sudo -u postgres psql -X -c "\
SELECT a.pid, a.wait_event_type, a.wait_event, a.query, l.relation::regclass AS rel, l.mode, l.granted \
FROM pg_stat_activity a \
JOIN pg_locks l ON l.pid=a.pid \
WHERE a.query ILIKE '%autovacuum%' AND NOT l.granted;"
 pid | wait_event_type |  wait_event   |                 query                 |      rel       |      mode       | granted
-----+-----------------+---------------+---------------------------------------+----------------+-----------------+---------
(0 rows)

Significado: No hay evidencia de que los workers de autovacuum estén esperando por locks no concedidos en este momento.

Decisión: No persigas fantasmas de locks. Pasa a investigar anclajes de transacciones e I/O.

Task 5: Buscar transacciones de larga duración que anclen vacuum

cr0x@server:~$ sudo -u postgres psql -X -c "\
SELECT pid, usename, application_name, state, now()-xact_start AS xact_age, wait_event_type, wait_event \
FROM pg_stat_activity \
WHERE xact_start IS NOT NULL \
ORDER BY xact_age DESC \
LIMIT 10;"
 pid  | usename | application_name |        state        | xact_age  | wait_event_type | wait_event
------+--------+------------------+---------------------+-----------+-----------------+-----------
 7712 | app    | api              | idle in transaction | 03:17:09  | Client          | ClientRead
 9120 | app    | batch            | active              | 00:42:11  |                 |
(2 rows)

Significado: Una sesión “idle in transaction” ha mantenido un snapshot durante horas. Vacuum no puede eliminar tuplas muertas que siguen siendo visibles para esa sesión.

Decisión: Corrige el bug de la aplicación (falta commit/rollback). A corto plazo, termina esa sesión si es seguro; de lo contrario, ajustar autovacuum no ayudará.

Task 6: Verificar riesgo de wraparound (esto importa aunque el rendimiento parezca “bien”)

cr0x@server:~$ sudo -u postgres psql -X -c "\
SELECT datname, age(datfrozenxid) AS xid_age, age(datminmxid) AS mxid_age \
FROM pg_database \
ORDER BY xid_age DESC;"
  datname  | xid_age  | mxid_age
-----------+----------+----------
 appdb     | 145000000 | 1800000
 postgres  |   2300000 |  120000
 template1 |   2300000 |  120000
 template0 |   2300000 |  120000
(4 rows)

Significado: appdb está muy por delante en edad de XID; ahí es donde vacuum/freeze debe mantenerse al día.

Decisión: Si xid_age se acerca a tu margen de seguridad autovacuum_freeze_max_age, prioriza el progreso de freeze sobre el throttling “agradable”.

Task 7: Identificar tablas con más tuplas muertas y urgencia de vacuum

cr0x@server:~$ sudo -u postgres psql -X -c "\
SELECT relid::regclass AS table, n_live_tup, n_dead_tup, last_autovacuum, last_vacuum, vacuum_count, autovacuum_count \
FROM pg_stat_user_tables \
ORDER BY n_dead_tup DESC \
LIMIT 15;"
        table        | n_live_tup | n_dead_tup |      last_autovacuum       | last_vacuum | vacuum_count | autovacuum_count
---------------------+------------+------------+----------------------------+-------------+--------------+------------------
 public.events       | 120000000  | 48000000   | 2025-12-30 08:10:01+00     |             |            0 |               91
 public.sessions     |  90000000  | 22000000   | 2025-12-30 08:20:14+00     |             |            0 |              143
(2 rows)

Significado: Cantidades masivas de tuplas muertas; autovacuum se está ejecutando con frecuencia pero no gana terreno. Eso es típico de un fracaso por ponerse al día.

Decisión: Considera umbrales por tabla más agresivos y configuraciones de vacuum, además de arreglos en consultas/aplicación.

Task 8: Comprobar si la tabla “se vacuumea mucho” pero sigue con bloat (pistas aproximadas de bloat)

cr0x@server:~$ sudo -u postgres psql -X -c "\
SELECT schemaname, relname, n_live_tup, n_dead_tup, \
pg_size_pretty(pg_total_relation_size(relid)) AS total_size, \
pg_size_pretty(pg_relation_size(relid)) AS heap_size, \
pg_size_pretty(pg_indexes_size(relid)) AS index_size \
FROM pg_stat_user_tables \
ORDER BY pg_total_relation_size(relid) DESC \
LIMIT 10;"
 schemaname |  relname  | n_live_tup | n_dead_tup | total_size | heap_size | index_size
------------+-----------+------------+------------+------------+-----------+-----------
 public     | events    | 120000000  | 48000000   | 180 GB     | 120 GB    | 60 GB
 public     | sessions  |  90000000  | 22000000   | 110 GB     | 70 GB     | 40 GB
(2 rows)

Significado: Heap grande e índices grandes; la proporción de tuplas muertas es alta. Vacuum probablemente está generando I/O intenso.

Decisión: Si la tabla tiene muchas actualizaciones, evalúa la viabilidad de HOT updates y el diseño de índices. Ajustar solo autovacuum no será suficiente.

Task 9: Ver si autovacuum está siendo limitado (si el cost delay está en efecto)

cr0x@server:~$ sudo -u postgres psql -X -c "SHOW vacuum_cost_limit; SHOW vacuum_cost_delay;"
 vacuum_cost_limit
-------------------
 200
(1 row)

 vacuum_cost_delay
-------------------
 0
(1 row)

Significado: El vacuum cost delay global es 0, pero autovacuum tiene su propio delay (a menudo 2ms). El comportamiento efectivo depende de las configuraciones de autovacuum que viste antes.

Decisión: Si estás limitado por I/O, disminuir delays puede perjudicar la latencia. Si estás atrasado en la limpieza, puede que necesites aumentar el cost limit y/o reducir el delay con cuidado.

Task 10: Comprobación rápida del I/O a nivel sistema (¿el disco es el limitador?)

cr0x@server:~$ iostat -x 1 5
Linux 6.8.0-41-generic (server)  12/30/2025  _x86_64_ (16 CPU)

avg-cpu:  %user   %nice %system %iowait  %steal   %idle
          12.15    0.00    4.33   18.90    0.00   64.62

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  52480.0     0.0   0.00    9.20    64.0     610.0  48800.0   12.10    80.0    18.4   99.0

Significado: NVMe está en ~99% de utilización con alto await. Estás saturado por almacenamiento. “Vacuum lento” es básicamente física.

Decisión: Aún puedes ajustar autovacuum, pero la solución más amplia es reducir la amplificación de escrituras, aumentar RAM/ratio de cache, mejorar el throughput de almacenamiento o distribuir el I/O.

Task 11: Comprobar rápidamente opciones de montaje del sistema de archivos

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

Significado: ext4 con relatime. Nada obviamente exótico.

Decisión: Si ves filesystems en red o opciones sync extrañas, detente y reevalúa. A vacuum no le gustan los fsync lentos.

Task 12: Inspeccionar logs de autovacuum para obtener tiempos reales

cr0x@server:~$ sudo -u postgres psql -X -c "SHOW log_autovacuum_min_duration;"
 log_autovacuum_min_duration
----------------------------
 -1
(1 row)

Significado: Autovacuum no está registrando duraciones, así que estás depurando a ciegas.

Decisión: Configura log_autovacuum_min_duration = '30s' (o incluso 0 temporalmente durante un incidente) para capturar qué está tomando tiempo realmente.

cr0x@server:~$ sudo tail -n 5 /var/log/postgresql/postgresql-16-main.log
2025-12-30 08:20:14.902 UTC [8534] LOG:  automatic vacuum of table "appdb.public.sessions": index scans: 1
2025-12-30 08:20:14.902 UTC [8534] DETAIL:  pages: 0 removed, 842113 remain, 500000 skipped due to pins, 0 skipped frozen
2025-12-30 08:20:14.902 UTC [8534] DETAIL:  tuples: 0 removed, 0 remain, 0 are dead but not yet removable
2025-12-30 08:20:14.902 UTC [8534] DETAIL:  buffer usage: 21000 hits, 180000 misses, 40000 dirtied
2025-12-30 08:20:14.902 UTC [8534] DETAIL:  avg read rate: 45.0 MB/s, avg write rate: 10.0 MB/s, I/O timings: read 220000.000 ms, write 90000.000 ms

Significado: “Skipped due to pins” es la pistola humeante: algo está manteniendo snapshots/tuplas. También fijaos en los tiempos de I/O—las lecturas son lentas y dominantes.

Decisión: Arregla primero los anclajes (transacciones largas, replication slots), luego considera throughput de I/O y ajuste de cost.

Task 13: Verificar replication slots (anclaje común de vacuum)

cr0x@server:~$ sudo -u postgres psql -X -c "\
SELECT slot_name, slot_type, active, restart_lsn, confirmed_flush_lsn \
FROM pg_replication_slots;"
   slot_name   | slot_type | active | restart_lsn | confirmed_flush_lsn
---------------+-----------+--------+-------------+---------------------
 analytics_cdc | logical   | f      | 2A/90000000 | 2A/100000000
(1 row)

Significado: Un slot lógico inactivo puede retener WAL. Eso no es lo mismo que congelar tuplas, pero puede crear presión de disco y tiempo de recuperación prolongado tras reinicios. Además suele correlacionarse con transacciones largas en consumidores CDC.

Decisión: Si el slot está abandonado, elimínalo. Si se necesita, arregla al consumidor para que avance.

Task 14: Buscar tormentas de archivos temporales y problemas de planes de consulta (el clásico “culpan a vacuum”)

cr0x@server:~$ sudo -u postgres psql -X -c "\
SELECT datname, temp_files, pg_size_pretty(temp_bytes) AS temp \
FROM pg_stat_database \
ORDER BY temp_bytes DESC;"
 datname | temp_files |  temp
---------+------------+--------
 appdb   |      18234 | 96 GB
(1 row)

Significado: Gran uso de temporales. Tus discos están ocupados, pero no necesariamente por vacuum.

Decisión: Si el uso de temporales coincide con los periodos lentos, ajusta work_mem por consulta, arregla sorts/joins y reduce la churn de temporales. Si no, el ajuste de vacuum no moverá la aguja.

Estrategia de ajuste segura: qué cambiar y en qué orden

El ajuste de autovacuum es un juego de compensaciones: throughput vs latencia, mantenimiento en segundo plano vs consultas en primer plano. El enfoque seguro es aumentar capacidad con guardas y apuntar a las peores tablas, en lugar de subir todo globalmente.

Una idea parafraseada de Werner Vogels (CTO de Amazon): Todo falla eventualmente; diseña y opera como si la falla fuera normal, no excepcional.

Principio 1: Trata “no poder mantenerse al día” como un problema estructural

Si las tuplas muertas crecen más rápido de lo que vacuum puede eliminar, tienes uno de estos problemas:

  • Demasiado pocos workers / demasiado throttling
  • Techo de I/O (almacenamiento, caché, o vecinos ruidosos)
  • Anclaje (transacciones largas, idle in transaction, replication/slots)
  • Patrón de escritura que genera bloat más rápido de lo que cualquier vacuum razonable puede manejar (ciertos patrones UPDATE-heavy, falta de HOT opportunities, índices hinchados)

Ajustar autovacuum solo resuelve el primer punto. Los otros requieren cambios operativos y de esquema/aplicación.

Principio 2: Prefiere configuraciones por tabla para tablas grandes o patológicas

Las configuraciones globales son instrumentos gruesos. Las tablas grandes con alta rotación son donde los valores por defecto fallan. Los parámetros por tabla te permiten ser agresivo donde importa y suave en el resto.

Ejemplo: si public.events es enorme y se actualiza constantemente, puedes reducir el scale factor para que autovacuum se dispare antes, y potencialmente aumentar cost limit solo para esa tabla.

cr0x@server:~$ sudo -u postgres psql -X -c "\
ALTER TABLE public.events SET (autovacuum_vacuum_scale_factor = 0.01, autovacuum_vacuum_threshold = 50000);"
ALTER TABLE

Significado: Vacuum se dispara tras ~1% de cambios en la tabla + un umbral base. Eso es antes que los defaults y ayuda a prevenir bloat descontrolado.

Decisión: Usa esto cuando ves “autovacuum corre a menudo pero siempre tarde”. Quieres que corra antes y haga menos trabajo cada vez.

Principio 3: Aumenta workers con cuidado; es fácil hacerte un DOS

autovacuum_max_workers es tentador. Más workers pueden mejorar throughput, pero multiplican I/O y la agitación de buffers. En almacenamiento rápido con RAM adecuada puede ser una victoria. En almacenamiento saturado, puede convertir un sistema lento en uno colapsado.

Orientación base con la que me siento cómodo para muchos sistemas de producción:

  • Empieza moviendo de 3 a 5 workers en sistemas moderados si tienes muchas tablas grandes.
  • Sube más solo con evidencia de que el almacenamiento tiene margen y que los wait events no están dominados por I/O.
  • Límitalo si compartes discos con otros servicios o tienes SLOs de latencia estrictos.
cr0x@server:~$ sudo -u postgres psql -X -c "ALTER SYSTEM SET autovacuum_max_workers = 5;"
ALTER SYSTEM
cr0x@server:~$ sudo -u postgres psql -X -c "SELECT pg_reload_conf();"
 pg_reload_conf
----------------
 t
(1 row)

Significado: Aumentado el conteo de workers; configuración recargada.

Decisión: Observa I/O (iostat), latencia y vistas de progreso durante 30–60 minutos. Si p99 empeora bruscamente, revierte o aumenta los delays de cost.

Principio 4: Usa el tuning de coste para moldear el impacto, no para “acelerar vacuum” a ciegas

El modelo de coste de vacuum intenta limitar cuánto I/O hace vacuum forzando sleeps. Los dos knobs que más importan:

  • autovacuum_vacuum_cost_limit: cuánto trabajo antes de dormir
  • autovacuum_vacuum_cost_delay: cuánto tiempo dormir

Si vacuum se queda atrás y tienes margen de I/O, sube cost limit y/o reduce delay. Si vacuum causa latencia en consultas, haz lo contrario.

Un “primer movimiento” conservador en sistemas modernos con SSD suele ser:

  • Aumentar moderadamente autovacuum_vacuum_cost_limit (por ejemplo, 200 → 1000)
  • Mantener el delay pequeño pero no nulo (por ejemplo, 2ms está bien; 0ms puede ser agresivo)
cr0x@server:~$ sudo -u postgres psql -X -c "ALTER SYSTEM SET autovacuum_vacuum_cost_limit = 1000;"
ALTER SYSTEM
cr0x@server:~$ sudo -u postgres psql -X -c "ALTER SYSTEM SET autovacuum_vacuum_cost_delay = '2ms';"
ALTER SYSTEM
cr0x@server:~$ sudo -u postgres psql -X -c "SELECT pg_reload_conf();"
 pg_reload_conf
----------------
 t
(1 row)

Significado: Autovacuum puede hacer más trabajo por unidad de tiempo, pero aún cede periódicamente.

Decisión: Si ya estás saturado por I/O, no hagas esto globalmente. Prefiere ajuste por tabla o arregla el límite de I/O subyacente.

Principio 5: Ajusta el comportamiento de freeze solo cuando entiendas el perfil de edad de tus XID

Hay dos modos de fallo comunes:

  • Demasiado tímido: el trabajo de freeze no se mantiene al día, te acercas al wraparound y vacuum se vuelve urgente y disruptivo.
  • Demasiado agresivo: fuerzas freeze pesado con demasiada frecuencia en tablas grandes, aumentando la amplificación de escritura.

Toma decisiones basadas en la medición de age(relfrozenxid) para tus tablas más grandes y la edad a nivel de base de datos.

cr0x@server:~$ sudo -u postgres psql -X -c "\
SELECT c.oid::regclass AS table, age(c.relfrozenxid) AS xid_age \
FROM pg_class c \
JOIN pg_namespace n ON n.oid=c.relnamespace \
WHERE n.nspname='public' AND c.relkind='r' \
ORDER BY xid_age DESC \
LIMIT 10;"
       table       | xid_age
-------------------+----------
 public.events      | 142000000
 public.sessions    | 120000000
(2 rows)

Significado: Estas tablas están impulsando la edad de XID. El trabajo de freeze debe ocurrir aquí.

Decisión: Considera autovacuum_freeze_max_age por tabla o una cadencia de vacuum más agresiva para estas tablas—después de eliminar los anclajes.

Principio 6: Recuerda al socio oculto: analyze

Autovacuum a menudo también ejecuta analyze. Si las estadísticas están obsoletas, los planes de consulta se vuelven extraños, lo que cambia patrones de escritura y la duración de transacciones. Eso retroalimenta el trabajo de vacuum.

cr0x@server:~$ sudo -u postgres psql -X -c "\
SELECT relid::regclass AS table, last_analyze, last_autoanalyze, n_mod_since_analyze \
FROM pg_stat_user_tables \
ORDER BY n_mod_since_analyze DESC \
LIMIT 10;"
      table       | last_analyze |      last_autoanalyze      | n_mod_since_analyze
------------------+--------------+----------------------------+--------------------
 public.events     |              | 2025-12-29 22:10:01+00     |            24000000
 public.sessions   |              | 2025-12-30 00:05:44+00     |            11000000
(2 rows)

Significado: Modificaciones masivas desde el último analyze; las estadísticas pueden estar desactualizadas respecto a la churn.

Decisión: Reduce el analyze scale factor por tabla en tablas de alta rotación. Esto puede estabilizar planes y reducir picos de duración de transacciones.

Principio 7: No ajustes autovacuum en el vacío (sí, lo dije)

Aquí es donde la ingeniería de almacenamiento se encuentra con la realidad de Postgres:

  • Vacuum lee muchas páginas. Si tu conjunto de trabajo ya apenas cabe en RAM, vacuum expulsará páginas calientes y amplificará los misses.
  • Vacuum escribe (updates de visibility map, freezing, limpieza de índices). Eso es presión de fsync y presión en WAL.
  • En volúmenes cloud, límites de IOPS/throughput pueden convertir cargas “pesadas en I/O aleatorio” en colapsos dramáticos.

Así que: verifica límites de I/O y dimensionamiento de memoria antes de “arreglar autovacuum”.

Tres mini-historias corporativas (anonimizadas)

Mini-historia 1: Incidente causado por una suposición equivocada

La empresa operaba un servicio de suscripción con un cluster Postgres muy ocupado. Tras migrar a Ubuntu 24.04 y una versión menor más nueva de Postgres, vieron picos nocturnos de latencia. La conclusión on-call fue inmediata: “nuevo SO, nuevo kernel, autovacuum se volvió más lento”.

Aumentaron autovacuum_max_workers y pusieron cost delay a cero globalmente. Los picos se convirtieron en una meseta sostenida de miseria. La CPU parecía estar bien, pero los discos estaban a tope. El panel decía “vacuum corriendo constantemente”, lo que solo reforzó la narrativa de que vacuum era el villano.

Entonces alguien finalmente miró pg_stat_activity y vio una conexión de una herramienta batch legacy en estado idle in transaction durante horas. Antes “funcionaba” porque el dataset era más pequeño. Tras crecer, el mismo bug se volvió un anclaje para vacuum.

Matar esa sesión hizo que vacuum empezara a eliminar tuplas muertas en vez de saltarse páginas ancladas. La noche siguiente el pico desapareció sin ningún ajuste agresivo.

La suposición equivocada no fue técnica—fue sociológica: culpar al cambio reciente solo porque fue reciente. La solución fue aburrida: encuentra el anclaje, arregla el cliente y luego reevalúa la configuración de vacuum con base en el throughput real.

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

Un equipo fintech tenía una tabla tipo ledger con muchas actualizaciones. Vieron bloat y decidieron “ayudar” a autovacuum poniendo umbrales extremadamente agresivos y un cost limit alto—globalmente. Pensaron: si vacuum corre constantemente, el bloat no puede acumularse.

Lo que consiguieron fue un desastre silencioso. Autovacuum comenzó a triturar grandes índices durante las horas pico, arruinando la caché. Endpoints API de solo lectura empezaron a fallar en caché y golpear el almacenamiento. La latencia subió, aumentaron los timeouts y los reintentos de la aplicación generaron aún más escrituras.

El on-call intentó arreglarlo añadiendo más workers de autovacuum. Eso multiplicó el I/O y empeoró la agitación de caché. El sistema no estaba sub-vacuumeado; estaba sub-provisionado para el nuevo nivel de trabajo en segundo plano compitiendo con el tráfico de primer plano.

La solución final fue direccionada por tabla: vacuum agresivo solo en la tabla caliente, y solo durante ventanas de baja carga mediante scheduling manual para los periodos peores de bloat. También ajustaron índices para mejorar HOT updates, lo que redujo la cantidad de index vacuuming requerida.

La lección: puedes sintonizar autovacuum hasta convertirlo en un generador de carga. Postgres te lo permitirá. Es tan educado como eso.

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

Un proveedor SaaS ejecutaba Postgres multi-tenant con requisitos estrictos de uptime. Tenían un ritual: cada semana un script recogía duraciones de vacuum, tablas con más tuplas muertas, tendencias de edad XID y snapshots de utilización de I/O. Nada sofisticado. Solo consistencia.

Una semana, el script marcó que age(datfrozenxid) subía más rápido de lo normal en la base de datos de un tenant. No había incidente aún, ni alertas disparadas. Solo una tendencia que parecía incorrecta.

Investigaron y encontraron un replication slot largo de una tubería de analytics que había sido deshabilitada pero no removida. La retención de WAL se infló, los checkpoints se volvieron más pesados y autovacuum competía con más I/O que antes. Vacuum no estaba “roto”; estaba siendo superado por la competencia.

Eliminar el slot abandonado redujo la presión de WAL y vacuum volvió a comportarse de forma predecible. Nadie notó nada externamente. El mejor incidente es el que nunca tienes que explicar.

Sí, fue aburrido. Por eso funcionó.

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

1) Síntoma: autovacuum “corre todo el tiempo” y las tuplas muertas siguen creciendo

Causa raíz: vacuum está anclado por transacciones largas o sesiones “idle in transaction”, o escanea repetidamente pero se salta páginas por pins.

Solución: Encuentra y elimina snapshots largos. Aplica timeouts de statement e idle-in-transaction. Confirma anclajes vía logs de autovacuum (“skipped due to pins”) y pg_stat_activity. Ajustar workers no ayudará hasta que los pins desaparezcan.

2) Síntoma: vacuum parece lento; pg_stat_progress_vacuum muestra “scanning” indefinidamente

Causa raíz: escaneo de heap limitado por I/O en una tabla con bloat, a menudo con misses de caché y almacenamiento saturado.

Solución: Reduce causas de bloat (patrones de actualización, índices), asegúrate del throughput de almacenamiento, considera aumentar cost limit solo si hay margen de almacenamiento y ajusta umbrales por tabla para vacuumear antes.

3) Síntoma: picos de latencia correlacionados con el inicio de autovacuum

Causa raíz: vacuum está expulsando páginas calientes e incrementando I/O aleatorio; demasiados workers o ajustes de coste muy agresivos durante picos.

Solución: Limita el impacto de autovacuum: incrementa cost delay, baja cost limit, reduce el número de workers y prefiere agresividad por tabla solo donde sea necesario. Considera programar vacuum manual en off-peak para tablas patológicas.

4) Síntoma: comportamiento de vacuum de emergencia, advertencias sobre wraparound

Causa raíz: el trabajo de freeze se quedó atrás; estás cerca de autovacuum_freeze_max_age. A menudo causado por autovacuum deshabilitado, anclajes o tablas demasiado grandes con umbrales por defecto.

Solución: Trátalo como prioridad 0. Elimina sesiones que anclan. Temporalmente aumenta recursos de vacuum y ejecuta vacuums manuales dirigidos. Después, ajusta los settings de freeze y umbrales para no volver a acercarte al precipicio.

5) Síntoma: “vacuum causa mucho WAL y lag de replicación”

Causa raíz: el freeze y las updates de visibility map generan WAL; configuraciones agresivas amplifican esto. Además, los checkpoints pueden verse estresados.

Solución: Suaviza el mantenimiento: evita tormentas de vacuum haciendo que vacuum ocurra antes y en porciones más pequeñas; ajusta parámetros de checkpoint; asegúrate de que la réplica tenga I/O suficiente; usa ajustes por tabla en lugar de agresión global.

6) Síntoma: vacuum a veces es bloqueado por locks

Causa raíz: DDL concurrente, o operaciones que toman locks más fuertes (como VACUUM FULL, REINDEX sin CONCURRENTLY) interfiriendo.

Solución: Deja de hacer DDL pesado en horas pico. Usa variantes concurrentes cuando sea posible. Reserva VACUUM FULL para ventanas planificadas de mantenimiento y solo cuando sea necesario tras comprobarlo.

Broma #2: VACUUM FULL es como mudarte de apartamento para encontrar el control remoto de la TV—efectivo, pero tu fin de semana se fue.

Listas de verificación / plan paso a paso

Paso a paso: de incidente a ajuste estable

  1. Chequeo de seguridad de freeze: consulta age(datfrozenxid) y las tablas top por age(relfrozenxid). Si estás cerca de umbrales de wraparound, deja de optimizar y empieza a prevenir downtime.
  2. Chequeo de anclajes: encuentra transacciones largas y “idle in transaction”. Arregla o termina a los culpables y establece timeouts para evitar recurrencias.
  3. Visibilidad de autovacuum: habilita log_autovacuum_min_duration (p. ej., 30s) para que puedas ver “skipped due to pins”, uso de buffers y tiempos de I/O.
  4. Top offenders: lista tablas por tuplas muertas y por tamaño total. Tu ajuste debe enfocarse en las 3–5 tablas superiores, no en todo el cluster.
  5. Capacidad de almacenamiento: verifica con iostat -x si tienes margen de I/O. Si estás a tope, aumentar workers/cost probablemente empeorará la latencia.
  6. Umbrales por tabla: reduce autovacuum_vacuum_scale_factor en tablas grandes y de alta rotación para que vacuum corra antes y en porciones menores.
  7. Workers: aumenta autovacuum_max_workers modestamente si hay muchas tablas y tienes margen.
  8. Tuning de cost: ajusta cost limit/delay para balancear throughput vs latencia. Prefiere ajustes por tabla cuando sea posible.
  9. Tuning de analyze: para tablas con alta churn, reduce el analyze scale factor. Esto evita regresiones de plan que dificultan la vida de vacuum.
  10. Validar: compara antes/después: tendencia de tuplas muertas, duraciones de vacuum, p95/p99 de latencia, ratio de cache hits, espera de I/O.
  11. Guardrails: establece timeouts sensatos (idle in transaction, statement timeout donde sea apropiado) y reglas operativas para DDL.
  12. Revisar esquema: reduce índices innecesarios, evita actualizar columnas indexadas sin necesidad y considera particionar tablas enormes de append/update.

Lista operativa: el conjunto “no me vuelvas a despertar”

  • Logging de duraciones de autovacuum habilitado con un umbral razonable.
  • Dashboards para: tuplas muertas por tablas top, edad XID, progreso de vacuum, utilización de I/O, temp bytes, lag/LSNs de replication slots.
  • Timeout de idle-in-transaction establecido (donde sea seguro).
  • Runbook para terminar ofensores obvios (con reglas de escalado).
  • Políticas de autovacuum por tabla documentadas para las tablas con mayor churn.
  • Política de DDL: no VACUUM FULL en horario laboral; preferir operaciones concurrentes.

Preguntas frecuentes

1) ¿Debo simplemente deshabilitar autovacuum y ejecutar VACUUM manualmente por la noche?

No. Así es como obtienes bloat, estadísticas malas y eventualmente presión de wraparound. Usa VACUUM manual como una herramienta dirigida, no como tu estrategia principal.

2) ¿Por qué vacuum a veces “se salta por pins”?

Porque alguna transacción todavía necesita visibilidad de versiones antiguas de filas. Causas típicas: queries de larga duración, “idle in transaction” o ciertos patrones de replicación/CDC. Arregla el pin; vacuum no puede anular las reglas de MVCC.

3) ¿Es seguro aumentar autovacuum_max_workers siempre?

Es seguro en el sentido de que Postgres no explotará inmediatamente, pero puede degradar la latencia al saturar el almacenamiento y agitar la caché. Aumenta workers solo tras confirmar margen de I/O y resolver anclajes.

4) ¿Cuál es el primer cambio de ajuste más seguro?

Habilitar logging de duración de autovacuum (log_autovacuum_min_duration) y ajustar scale factors por tabla en las tablas más grandes y de mayor churn para que vacuum corra antes. Esos cambios mejoran la observabilidad y reducen “tormentas de vacuum”.

5) ¿Por qué autovacuum es lento en SSD? Los SSD son rápidos.

Los SSD son rápidos, no infinitos. Vacuum puede generar lecturas y escrituras mixtas aleatorias más WAL. Si tu carga ya consume la mayor parte de IOPS/throughput, vacuum compite por el mismo presupuesto. Además, la agitación de caché puede hacer que el “almacenamiento rápido” sea irrelevante si todo falla en RAM.

6) ¿Ayuda VACUUM (ANALYZE) con la lentitud misteriosa?

A veces. Si el problema son estadísticas obsoletas que llevan a planes terribles, sí. Si el problema es anclaje o saturación de I/O, no arreglará la causa raíz, pero puede reducir la volatilidad de planes después de que arregles el problema subyacente.

7) ¿Por qué mis índices se hinchan incluso cuando el bloat de tabla parece aceptable?

Las actualizaciones a columnas indexadas crean entradas muertas en los índices. Incluso con HOT updates, si las columnas actualizadas están indexadas (o la fila no cabe en la misma página), HOT no se aplica y los índices churn. Soluciona reduciendo índices innecesarios y evitando actualizar columnas indexadas cuando sea posible.

8) ¿Cómo sé si autovacuum es la causa de la latencia o solo está correlacionado?

Mira los wait events y métricas de I/O del sistema durante el pico. Si las consultas de la aplicación esperan I/O y vacuum consume una gran parte de lecturas/escrituras, probablemente es causal. Si temp bytes, sorts o un job batch coinciden con los picos, vacuum puede estar solo presente en la escena.

9) ¿Puedo ajustar autovacuum por tabla?

Sí, y deberías hacerlo para tablas grandes y de alta rotación. Usa parámetros de almacenamiento por tabla como autovacuum_vacuum_scale_factor, autovacuum_analyze_scale_factor y ajustes de cost por tabla si es necesario.

10) ¿Y si estoy en una plataforma gestionada o en contenedor con límites de cgroups?

Entonces debes verificar cuotas de CPU e I/O. Vacuum puede estar siendo limitado por la plataforma aunque la VM parezca “idle”. Revisa systemd y ajustes de cgroup y alinea esos límites con tus necesidades de mantenimiento.

Conclusión: pasos que puedes desplegar esta semana

Si autovacuum es “misteriosamente” lento en Ubuntu 24.04, asume que te está diciendo la verdad sobre una de tres cosas: estás anclado, estás limitado por I/O, o estás sub-provisionado/throttled.

  1. Activa la visibilidad: configura log_autovacuum_min_duration a un valor útil y empieza a leer lo que vacuum reporta sobre pins y tiempos de I/O.
  2. Elimina los anclajes en la fuente: elimina “idle in transaction”, arregla snapshots de larga vida y limpia replication slots abandonados.
  3. Apunta a las peores tablas: reduce vacuum/analyze scale factors en tablas gigantes con mucho churn para que vacuum corra antes y en porciones menores.
  4. Sólo entonces ajusta workers y parámetros de cost, en pequeños incrementos, mientras observas I/O await y p99 de latencia.
  5. Hazlo aburrido: tendencias para tuplas muertas, edad XID y duraciones de autovacuum. Aburrido es estable. Estable es rápido.
← Anterior
Ubuntu 24.04: systemd-resolved rompe el DNS de Docker — solución sin desactivar todo
Siguiente →
Proxmox «dispositivo tap ya existe»: Soluciona conflictos de red al iniciar VMs de forma efectiva

Deja un comentario