PostgreSQL vs ClickHouse: dónde almacenar los logs firehose sin dolor

¿Te fue útil?

Tus logs no son “datos”. Son una responsabilidad en movimiento: una manguera de incendios que aparece 24/7, exige retención y sólo se vuelve valiosa cuando la producción ya está en llamas. No puedes pausar las escrituras para “optimizar más tarde”. Debes elegir un motor de almacenamiento que falle de una forma con la que puedas convivir.

PostgreSQL y ClickHouse funcionan para logs—hasta que dejan de hacerlo. La clave es saber qué dolor estás aceptando: crecimiento por almacenamiento de filas y deuda de vacuum, o deuda de merge y explosiones de partes. Si eliges mal, tu persona de on-call tendrá un nuevo pasatiempo: explicar a los ejecutivos por qué “sólo logs” tumbó la base de datos primaria.

El marco de decisión: qué estás eligiendo realmente

Cuando la gente dice “almacenar logs”, a menudo se refiere a tres cosas distintas:

  • Buffer de ingestión: aceptar escrituras a alta velocidad, incluso durante picos. La durabilidad importa; la latencia, en su mayoría, no.
  • Búsqueda operativa: encontrar “ese request id” o un mensaje de error específico en la última hora. La baja latencia importa. El indexado importa.
  • Analítica: group-bys sobre miles de millones de filas, dashboards, detección de anomalías, retención larga y agregaciones pesadas.

PostgreSQL es una base de datos de propósito general con excelente corrección, indexación rica y semántica transaccional. Es cómoda cuando necesitas restricciones estrictas y búsquedas puntuales que deben ser correctas. Es menos cómoda cuando le pides retener y churnar petabytes de datos mayoritariamente append-only mientras también sirve consultas OLTP.

ClickHouse es una base de datos analítica columnar optimizada para escaneos y agregaciones rápidas. Se alimenta de logs sin esfuerzo—hasta que diseñas tus particiones como si fuera Postgres, o ignoras los merges, o lo tratas como un sistema transaccional estricto. No es un Postgres “peor”; es un animal diferente.

Así que aquí va una regla práctica y con opinión:

  • Si los logs son parte de tu producto transaccional (auditoría, trails de cumplimiento con garantías estrictas, o los unes constantemente con tablas OLTP), Postgres puede ser una opción sensata—con particionado por tiempo y retención disciplinada.
  • Si los logs son principalmente para búsqueda + dashboards + agregaciones, y el volumen es “firehose”, ClickHouse es la opción por defecto—con diseño adecuado de MergeTree y guardarraíles operativos.
  • Si intentas usar Postgres como almacén de logs porque “ya lo tenemos”, no estás ahorrando dinero; estás pidiendo prestado dolor a tasa variable.

Una idea parafraseada de Werner Vogels (CTO de Amazon): construyes fiabilidad diseñando para la falla, no esperando que no ocurra. Los logs son donde esta filosofía se prueba—porque tu sistema falla más seguido de lo que admite tu presupuesto.

Hechos interesantes y contexto histórico

  1. El linaje de PostgreSQL se remonta al proyecto POSTGRES en UC Berkeley en los años 80, con un sesgo de diseño hacia la corrección y la extensibilidad.
  2. MVCC en Postgres significa que actualizaciones/eliminaciones crean nuevas versiones de fila; para tablas de logs con churn (eliminaciones por retención), esto se convierte en almacenamiento y trabajo de vacuum que debes pagar.
  3. JSONB llegó en Postgres 9.4 y hizo que “logs semiestructurados en una BD relacional” parecieran fáciles—a veces demasiado, porque tienta a los equipos a saltarse la disciplina de esquema.
  4. ClickHouse se desarrolló en Yandex para impulsar analítica en tiempo real a gran escala, que es exactamente la forma de “logs como analítica”.
  5. El almacenamiento columnar no es nuevo; los almacenes por columna se han estudiado durante décadas. La ganancia moderna es combinar compresión, ejecución vectorizada y discos baratos.
  6. Los motores MergeTree en ClickHouse dependen de merges en segundo plano; ignorar la salud de los merges es como ignorar autovacuum en Postgres—eventualmente acumula deuda.
  7. TTL en ClickHouse puede aplicar retención a nivel de almacenamiento. Postgres puede hacer retención, pero normalmente es práctica a nivel de aplicación o jobs (drop de particiones, delete, vacuum).
  8. TimescaleDB (una extensión de Postgres) existe en gran parte porque los patrones de series temporales estresan a Postgres vanilla; es una señal de que “logs como series temporales” es especial.

Patrones de carga que hacen o rompen cada sistema

1) Escrituras en ráfaga con claves previsibles

La ingestión no trata de throughput medio. Trata de sobrevivir al percentil 99: tormentas de deploy, reintentos, fallos en cascada y ese cliente que activa logging de depuración en producción “por una hora”. Si tienes picos, necesitas:

  • Un camino de escritura que no se desmorone bajo contención.
  • Estrategia de backpressure (cola, buffer, inserts en batch).
  • Un layout de almacenamiento que mantenga las escrituras secuenciales-ish, o al menos amortizadas.

ClickHouse prefiere inserts en batch y puede absorber grandes volúmenes eficientemente. Postgres también puede ingerir rápido, pero te toparás antes con volumen de WAL, mantenimiento de índices y sobrecarga de vacuum.

2) Depuración “aguja en un pajar”

Los operadores piden: “Muéstrame los logs para request_id = X” y “Dame las últimas 200 líneas alrededor del error”. Postgres con índices B-tree en request_id y timestamp puede ser extremadamente rápido para esto, especialmente si el working set está en memoria.

ClickHouse también puede hacerlo, pero prefiere escanear columnas y filtrar. Puedes hacerlo bueno con el orden de clave primaria adecuado y índices de salto (data skipping), pero debes diseñarlo para ello.

3) Agregaciones y dashboards

Si tu realidad diaria es GROUP BY service, status, endpoint, 5-minute bucket sobre semanas de datos, Postgres está luchando contra su naturaleza. Puedes añadir vistas materializadas, rollups y particiones. Funciona—hasta que fallas una ventana de mantenimiento y terminas con un backlog de vacuum/analyze y un disco que parece haber engullido una copia duplicada de sí mismo.

ClickHouse está diseñado para esto. Comprime bien, escanea rápido y paraleliza. La pregunta operativa se vuelve: ¿puedes mantener los merges saludables y las partes bajo control?

4) Retención: delete vs drop vs TTL

La política de retención no es “guardamos 30 días”. Es: “¿Cómo eliminamos datos de 29 días sin destruir el sistema?”

  • En Postgres, dropear particiones es limpio y rápido. Eliminar fila a fila es caro y crea deuda de vacuum.
  • En ClickHouse, TTL puede eliminar datos antiguos automáticamente, pero sigue siendo trabajo físico realizado por procesos en segundo plano. Suele ser más barato que los deletes en Postgres, pero aún puede saturar discos si está mal configurado.

PostgreSQL para logs: en qué es bueno y por qué castiga

Cuando Postgres es la elección correcta

  • Necesitas transacciones estrictas: los logs son parte de un proceso de negocio (eventos de auditoría, trails tipo ledger).
  • Necesitas joins relacionales entre logs y entidades OLTP, con semántica consistente.
  • Tu volumen es moderado: “mucho” pero no “firehose”, o la ventana de retención es corta y las caídas de particiones son rutinarias.
  • Necesitas indexación flexible: B-tree, GIN en JSONB, búsqueda trigram, índices parciales.

Por qué Postgres te castiga

Postgres almacena filas, no columnas. Para analítica de logs a menudo lees pocos campos en un rango temporal enorme. Eso se vuelve intensivo en I/O. Y con MVCC, “eliminar logs antiguos” significa tuples muertas y trabajo de vacuum. El particionado ayuda, pero no es opcional.

También está el WAL. El WAL es tu amigo—hasta que tu tabla de logs se convierte en el mayor contribuyente al volumen de WAL, la latencia de replicación y el churn de backups.

Cómo hacer Postgres menos doloroso para logs

  • Particiona por tiempo (diario u horario según volumen) y dropea particiones para retención.
  • Minimiza índices en tablas de ingestión caliente. Cada índice es un impuesto por amplificación de escritura.
  • Usa JSONB con cuidado: guarda las claves consultadas frecuentemente como columnas tipadas; mantiene el resto en JSONB.
  • Separa responsabilidades: no co-localices ingestión pesada de logs con OLTP crítico en el mismo primary si puedes evitarlo.

Broma corta #1: Tratar a Postgres como un almacén de logs es como usar un coche deportivo para transportar grava—posible, pero la suspensión pondrá una queja.

ClickHouse para logs: en qué es bueno y por qué castiga

Cuando ClickHouse es la elección correcta

  • Ingestión tipo firehose con inserts por lotes (o un broker delante) y muchos datos de solo adjunción.
  • Consultas orientadas a analítica: dashboards, agregaciones, percentiles de latencia, top-N, estimaciones de cardinalidad.
  • Retención larga con compresión y jerarquía de almacenamiento.
  • Alta concurrencia de muchos lectores haciendo scans y group-bys.

Por qué ClickHouse te castiga

ClickHouse te castiga por pensar que “clave de partición” significa “lo que filtro más”. En ClickHouse, particionar sirve para gestionar partes y ciclo de vida; el ordering (primary key) sirve para pruning de consultas y localidad. Te equivocas y crearás un apocalipsis de partes pequeñas donde los merges nunca alcanzan.

También te castiga por fingir que es un sistema transaccional estricto. Los inserts son atómicos por lote, pero no obtienes las mismas garantías que en Postgres. Si necesitas “exactamente una vez”, lo construyes por encima de la base de datos con idempotencia y estrategias de deduplicación.

Cómo hacer ClickHouse menos doloroso para logs

  • Inserciones por lotes (bloques más grandes, menos partes). Si tu pipeline inserta filas individuales, pagas un impuesto en partes y merges.
  • Elige ORDER BY para tus filtros más comunes (usualmente timestamp + servicio + algo como host o trace id).
  • Mantén particiones gruesas (a menudo por día) a menos que el volumen obligue a particionar más fino. Demasiadas particiones aumentan metadatos y sobrecarga de merges.
  • Vigila merges y partes como vigilas la CPU. La deuda de merge es deuda operativa.

Broma corta #2: Un clúster ClickHouse con partes fuera de control es como un garaje lleno de “montones organizados”—técnicamente ordenado, emocionalmente devastador.

Diseño de esquema y tablas que sobreviven a la realidad

Los datos de logs no son totalmente sin esquema

Los logs totalmente sin esquema son una fantasía. Siempre tienes algunos campos que consultas constantemente: timestamp, servicio, entorno, severidad, host, request_id/trace_id, estado y quizá user_id. Esos campos deben ser columnas tipadas tanto en Postgres como en ClickHouse. Todo lo demás puede ir en una carga JSON o en una estructura tipo Map.

Postgres: estrategia de particionado

Usa particionado declarativo por tiempo. Las particiones diarias son un buen comienzo. Las particiones horarias pueden funcionar para volúmenes muy altos, pero pagarás en número de tablas y sobrecarga operativa. El movimiento ganador es hacer que la retención sea una operación DROP TABLE, no un DELETE.

ClickHouse: estrategia MergeTree

Para logs, un patrón común es:

  • PARTITION BY toDate(ts) (o toYYYYMM(ts) para retenciones muy largas)
  • ORDER BY (service, environment, toDateTime(ts), trace_id) o similar
  • TTL ts + INTERVAL 30 DAY

El ORDER BY correcto depende de tus filtros. Si siempre filtras por service y rango temporal, pon service al principio. Si tienes muchas búsquedas puntuales por request_id, inclúyelo—pero no finjas que se comportará como un índice único.

Compresión y tipos de datos

En ClickHouse, elegir tipos apropiados (LowCardinality para nombres de servicio, Enum para nivel de log, IPv4/IPv6 para IPs, DateTime64 para timestamps) aporta compresión real y velocidad. En Postgres, una normalización cuidadosa puede ayudar, pero demasiada normalización puede perjudicar la ingestión. Los logs son un lugar donde “ligeramente desnormalizado pero tipado” suele ganar.

Tareas prácticas: comandos, salidas, decisiones

Estos son chequeos reales que puedes ejecutar en producción. Cada uno incluye: comando, qué significa la salida y qué decisión tomar.

Task 1: Confirmar bloat de tabla e índices en Postgres

cr0x@server:~$ sudo -u postgres psql -d app -c "SELECT relname, n_live_tup, n_dead_tup, ROUND(100.0*n_dead_tup/GREATEST(n_live_tup,1),2) AS dead_pct FROM pg_stat_user_tables ORDER BY dead_pct DESC LIMIT 5;"
  relname       | n_live_tup | n_dead_tup | dead_pct
----------------+------------+------------+----------
logs_2025_12_28 |   98000000 |   34000000 |   34.69
logs_2025_12_27 |   99000000 |   21000000 |   21.21
...

Significado: Tuplas muertas son almacenamiento y trabajo de vacuum. 35% de muertas en una partición de logs normalmente indica que hay deletes/updates ocurriendo o que autovacuum no puede mantenerse al día.

Decisión: Deja de borrar filas para retención; cambia a drop de particiones. Si ya particionas, investiga por qué mueren tuplas (updates, o vacuum retrasado) y ajusta autovacuum para esas particiones.

Task 2: Comprobar si autovacuum realmente corre en particiones calientes

cr0x@server:~$ sudo -u postgres psql -d app -c "SELECT relname, last_autovacuum, last_autoanalyze FROM pg_stat_user_tables WHERE relname LIKE 'logs_%' ORDER BY last_autovacuum NULLS FIRST LIMIT 10;"
   relname      |     last_autovacuum     |     last_autoanalyze
---------------+--------------------------+--------------------------
logs_2025_12_28 |                          |
logs_2025_12_27 | 2025-12-30 08:14:02+00   | 2025-12-30 08:15:31+00
...

Significado: NULL en last_autovacuum en una partición activa y grande es una señal roja: vacuum puede estar bloqueado, deshabilitado o hambriento.

Decisión: Si es una tabla de ingestión intensa, considera bajar los umbrales de autovacuum para esa tabla, o replantea retención e índices. Si vacuum está bloqueado, encuentra los bloqueadores.

Task 3: Encontrar sesiones bloqueantes en Postgres

cr0x@server:~$ sudo -u postgres psql -d app -c "SELECT a.pid, a.state, a.query, pg_blocking_pids(a.pid) AS blockers FROM pg_stat_activity a WHERE a.datname='app' AND a.wait_event_type IS NOT NULL;"
 pid  | state  |                 query                  | blockers
------+--------+----------------------------------------+----------
18422 | active | VACUUM (ANALYZE) logs_2025_12_28;      | {17201}
...

Significado: Vacuum está esperando un lock retenido por otro PID. Si ese PID es una transacción de larga duración, puede impedir la limpieza.

Decisión: Arregla el comportamiento de la aplicación (no mantener transacciones largas e inactivas), o mata a los ofensores. Luego revisa el progreso del vacuum.

Task 4: Medir presión de WAL por ingestión de logs

cr0x@server:~$ sudo -u postgres psql -d app -c "SELECT pg_size_pretty(pg_wal_lsn_diff(pg_current_wal_lsn(),'0/0')) AS wal_bytes_since_boot;"
 wal_bytes_since_boot
----------------------
 812 GB

Significado: Esta es una señal burda, pero si el WAL crece explosivamente durante picos de logs, impactará replicación y backups.

Decisión: Considera mover logs fuera del cluster OLTP primario, reducir índices, usar inserts agrupados, o cambiar a ClickHouse para logs analíticos.

Task 5: Comprobar lag de replicación (Postgres)

cr0x@server:~$ sudo -u postgres psql -d app -c "SELECT application_name, state, pg_size_pretty(pg_wal_lsn_diff(sent_lsn, replay_lsn)) AS lag FROM pg_stat_replication;"
 application_name |   state   |  lag
------------------+-----------+--------
 standby-a        | streaming | 14 GB

Significado: 14 GB de lag durante un pico de logs normalmente significa que la réplica no puede mantenerse al día con la generación de WAL o con el throughput de disco/red.

Decisión: Si los logs son el principal contribuyente, desacópialos. Si no, investiga saturación de I/O en la réplica y ajustes del receptor WAL.

Task 6: Confirmar conteo de particiones y sesgo de tamaño en Postgres

cr0x@server:~$ sudo -u postgres psql -d app -c "SELECT inhparent::regclass AS parent, count(*) AS partitions FROM pg_inherits WHERE inhparent::regclass::text='logs';"
 parent | partitions
--------+------------
 logs   | 62

Significado: Demasiadas particiones pueden perjudicar planning y mantenimiento; muy pocas pueden hacer la retención costosa.

Decisión: Si las particiones exceden lo que tu cadencia operativa puede manejar, pasa de horario a diario. Si las particiones son enormes y ocurren deletes, ve a particiones más pequeñas y dropea.

Task 7: Comprobar salud de ingestión en ClickHouse vía system.parts

cr0x@server:~$ clickhouse-client --query "SELECT table, count() AS parts, sum(rows) AS rows FROM system.parts WHERE database='logs' AND active GROUP BY table ORDER BY parts DESC LIMIT 5;"
log_events	12480	9812234451
log_errors	2210	88233412

Significado: 12k partes activas para una tabla suele ser excesivo. Partes pequeñas implican batches de insert demasiado pequeños o particionado demasiado granular.

Decisión: Aumenta el tamaño de los batches de insert, ajusta el particionado y considera settings que reduzcan churn de partes. También verifica el backlog de merges.

Task 8: Comprobar backlog de merges en ClickHouse

cr0x@server:~$ clickhouse-client --query "SELECT database, table, sum(rows) AS rows_in_merges, sum(bytes_on_disk) AS bytes_in_merges FROM system.parts WHERE database='logs' AND active=0 GROUP BY database, table ORDER BY bytes_in_merges DESC LIMIT 5;"
logs	log_events	1823311220	412339922944

Significado: Partes no activas representan partes en merge o partes antiguas. Bytes enormes “en merges” sugiere presión de merge y potenciales ralentizaciones de consulta.

Decisión: Si los merges no dan abasto, reduce la fragmentación de inserts, aumenta recursos para merges en segundo plano, o rehace el diseño de la tabla (order key, partition key).

Task 9: Comprobar hotspots de consulta en ClickHouse

cr0x@server:~$ clickhouse-client --query "SELECT query_duration_ms, read_rows, read_bytes, result_rows, substring(query,1,120) AS q FROM system.query_log WHERE type='QueryFinish' ORDER BY read_bytes DESC LIMIT 5;"
8421	9122333441	188233992110	120	SELECT service, count() FROM log_events WHERE ts >= now()-INTERVAL 7 DAY GROUP BY service

Significado: Una sola consulta de dashboard leyendo 188 GB no es “un poco lenta”, es una revisión de diseño. O el filtro es demasiado amplio o el ORDER BY está mal para el patrón de acceso.

Decisión: Añade pre-aggregación, ajusta los filtros, revisa el ORDER BY o crea vistas materializadas para dashboards.

Task 10: Validar distribución de particiones en ClickHouse

cr0x@server:~$ clickhouse-client --query "SELECT partition, count() AS parts, sum(rows) AS rows FROM system.parts WHERE database='logs' AND table='log_events' AND active GROUP BY partition ORDER BY parts DESC LIMIT 5;"
20251230	980	812233441
20251229	910	799334221

Significado: Cientos de partes por partición diaria es un olor a problema. Normalmente significa batches de insert demasiado pequeños o demasiados escritores paralelos.

Decisión: Inserta en batch y/o canaliza escrituras a través de menos streams concurrentes por shard.

Task 11: Comprobar saturación de I/O en un host Linux

cr0x@server:~$ iostat -xm 1 3
Linux 6.5.0 (server) 	12/30/2025 	_x86_64_	(32 CPU)

Device            r/s     w/s   rMB/s   wMB/s  await  %util
nvme0n1         320.0   900.0   180.0   420.0   28.5  99.2

Significado: 99% de util y await alto indican que estás limitado por I/O. Tanto Postgres como ClickHouse se verán “lentos” por diferentes razones, pero la raíz es el disco.

Decisión: Reduce la amplificación de escritura (índices/partes), añade discos, separa el WAL, cambia a almacenamiento más rápido o ajusta la concurrencia de merges/vacuum para evitar tormentas de I/O autoinfligidas.

Task 12: Comprobar presión de memoria y comportamiento de caché

cr0x@server:~$ free -h
               total        used        free      shared  buff/cache   available
Mem:           256Gi       210Gi       3.1Gi       2.0Gi        43Gi        39Gi
Swap:           16Gi        12Gi       4.0Gi

Significado: Uso intensivo de swap en un host de BD es un acantilado de rendimiento. Para Postgres puede destrozar la efectividad de shared buffers; para ClickHouse puede hundir merges y ejecución de consultas.

Decisión: Reduce huella de memoria (baja concurrencia, ajusta caches), añade RAM o aisla cargas de trabajo. Si estás swap durante merges/vacuum, limita el trabajo en segundo plano.

Task 13: Inspeccionar consultas lentas en Postgres y si los índices ayudan

cr0x@server:~$ sudo -u postgres psql -d app -c "EXPLAIN (ANALYZE, BUFFERS) SELECT count(*) FROM logs_2025_12_28 WHERE ts >= now()-interval '1 hour' AND service='api';"
Aggregate  (cost=...)
  Buffers: shared hit=120 read=54000
  ->  Seq Scan on logs_2025_12_28 ...

Significado: Un seq scan leyendo 54k buffers para una ventana de una hora sugiere o bien la tabla es demasiado grande, el filtro no es selectivo, o faltan índices/no se usan por estadísticas pobres.

Decisión: Considera un índice BRIN en timestamp para particiones append-only grandes, o revisa pruning de particiones y estadísticas. Si la analítica domina, contempla mover esa carga a ClickHouse.

Task 14: Comprobar definición de tabla ClickHouse para ORDER BY/partition/TTL

cr0x@server:~$ clickhouse-client --query "SHOW CREATE TABLE logs.log_events"
CREATE TABLE logs.log_events
(
    `ts` DateTime64(3),
    `service` LowCardinality(String),
    `env` LowCardinality(String),
    `level` LowCardinality(String),
    `trace_id` String,
    `message` String
)
ENGINE = MergeTree
PARTITION BY toDate(ts)
ORDER BY (service, env, ts)
TTL ts + INTERVAL 30 DAY
SETTINGS index_granularity = 8192

Significado: Esto es generalmente sensato para filtros “service/env/rango temporal”. Si tus consultas principales filtran por trace_id solamente, este ORDER BY no ayudará mucho.

Decisión: Alinea el ORDER BY con tus predicados reales. Si necesitas búsquedas por trace-id, considera índices de data skipping secundarios o una tienda de lookup dedicada.

Guía rápida de diagnóstico

Cuando el almacenamiento de logs está “lento”, no tienes tiempo para ideologías. Necesitas un método en tres pasadas para encontrar el cuello de botella sin adivinar.

Primero: ¿Es el host (CPU, memoria, disco, red)?

  • Saturación de disco: ejecuta iostat -xm 1. Si %util está cerca de 100% y await sube, estás limitado por I/O.
  • Presión de memoria: ejecuta free -h y revisa swap. Uso de swap en hosts de BD es una confesión de rendimiento.
  • CPU: ejecuta mpstat -P ALL 1 (no mostrado arriba). Si la CPU está saturada y iowait bajo, estás limitado por cómputo (a menudo compresión, parseo, regex o group-bys).
  • Red: en clústeres, verifica si replicación o consultas distribuidas saturan las NICs.

Decisión: Si el host es el límite, ajusta la BD después; primero evita autodaños (reduce concurrencia de merges/vacuum, baja concurrencia de consultas, batch inserts), luego añade capacidad o separa roles.

Segundo: ¿Es el camino de escritura o de lectura?

  • Síntomas de escritura: lag de ingestión, profundidad de cola creciente, conteo de partes en aumento (ClickHouse), WAL y lag de replicación subiendo (Postgres).
  • Síntomas de lectura: dashboards con timeout, alto read_bytes en logs de consultas (ClickHouse), scans secuenciales y lecturas de buffers (Postgres).

Decisión: Si las escrituras fallan, prioriza batching, saneamiento de particiones y salud del mantenimiento en segundo plano. Si las lecturas fallan, prioriza esquema/order keys, pre-aggregación y restricciones de consulta.

Tercero: Identifica deuda de mantenimiento (vacuum vs merges)

  • Postgres: tuplas muertas, transacciones largas bloqueando vacuum, autovacuum no corriendo o no siendo lo suficientemente agresivo para particiones de logs.
  • ClickHouse: demasiadas partes, merges que no alcanzan, disco ocupado con I/O de merges, limpieza TTL acumulándose.

Decisión: La deuda de mantenimiento no se arregla sola. O cambias patrones de ingestión (batching) o cambias patrones de ciclo de vida (drop de particiones / TTL), o sigues pagando para siempre.

Tres micro-historias corporativas (todas anonimizadas, dolorosamente plausibles)

Incidente: la suposición equivocada (“los logs son append-only, así que Postgres no se inflará”)

Una empresa SaaS mediana decidió almacenar logs de aplicación en Postgres “temporalmente”. La suposición era simple: los logs son append-only, así que la sobrecarga MVCC no importará. Usaron una tabla grande con índice por timestamp y payload JSONB. La ingestión estuvo bien durante semanas. Los dashboards también—hasta que entró la retención.

La retención se implementó como un DELETE FROM logs WHERE ts < now() - interval '30 days' nocturno. Corría durante horas. Luego días. Autovacuum empezó a perseguir un objetivo en movimiento: tuplas muertas acumulándose más rápido de lo que podía limpiar. El uso de disco subió. El lag de replicación subió. El primario empezó a pasar más tiempo escribiendo WAL y menos atendiendo consultas de producto.

Un lunes por la mañana, un deploy aumentó la verbosidad de logs para un componente ruidoso. El volumen de WAL se disparó. La réplica quedó tan retrasada que los márgenes de seguridad para failover desaparecieron. Luego el disco alcanzó un umbral y la respuesta al incidente se convirtió en una búsqueda de piezas: borrar datos antiguos, vacuum, esperar que termine, repetir.

La solución no fue heroica. Particionaron la tabla de logs por día y cambiaron la retención a drop de particiones. Ajustaron autovacuum por partición. El sistema “temporal” de logs dejó de amenazar la BD OLTP. La lección mayor: “append-only” es mentira en el momento en que implementas retención como delete.

Optimización que salió mal: “Partitionemos ClickHouse por hora para deletes más rápidos”

Otra organización movió logs a ClickHouse para analítica. El rendimiento temprano fue excelente. Alguien notó que la limpieza TTL era a veces espasmódica y decidió “ayudar” particionando por hora en vez de por día, asumiendo que particiones más pequeñas borrarían más rápido.

La ingestión venía de muchos servicios, cada uno insertando batches pequeños. Las particiones horarias multiplicaron el número de particiones activas. Cada partición acumuló partes. Las partes se multiplicaron en decenas de miles por tabla. Los merges empezaron a quedarse atrás. La latencia de consultas subió porque el sistema tenía que abrir y considerar muchísimas partes, y los merges en segundo plano saturaban el disco.

El equipo respondió aumentando threads de merge en background. Eso hizo los discos más ocupados, lo que empeoró las consultas, que hizo que los dashboards fallaran, lo que provocó una oleada de lógica de reintento en clientes. La ingestión se volvió espasmódica también, y todo el sistema desarrolló un ritmo de estroboscopio: tormenta de merges, tormenta de consultas, tormenta de reintentos.

Volvieron a particionar por día, aumentaron los tamaños de batch de insert y redujeron el número de streams concurrentes de insert por shard. TTL volvió a ser aburrido. La moraleja: particiones más pequeñas no son automáticamente más baratas—las partes y los merges son la unidad real del dolor.

Práctica aburrida pero correcta que salvó el día: “separar almacenamiento de logs, imponer presupuestos y ensayar la retención”

Una plataforma financiera tenía requisitos estrictos de auditoría, pero también enormes logs operativos. Se negaron a mezclarlos. Los eventos de auditoría vivían en Postgres con restricciones estrictas y esquema cuidadoso. Los logs operativos fueron a ClickHouse, alimentados vía cola y un batcher.

La práctica aburrida fue un “ejercicio de retención” trimestral. No un ejercicio de mesa. Uno real: verificaron que dropear particiones en Postgres funcionaba, que TTL en ClickHouse eliminaba los rangos correctos y que backups y restores no resucitaban datos expirados por accidente. También impusieron un presupuesto de consulta: los dashboards tenían límites en el rango temporal, y cualquier cosa más amplia requería una tabla pre-agrupada.

Un día, un deploy ruidoso elevó el volumen de logs. La cola absorbió el pico. La ingestión a ClickHouse quedó con lag pero estable. Los operadores tenían un SLO claro: “logs buscables en X minutos.” Lo cumplieron escalando temporalmente workers de ingestión—sin tocar la BD OLTP.

El resultado no fue dramático, y ese es el punto. La práctica correcta fue separación de responsabilidades más operaciones de ciclo de vida ensayadas. Cuando la manguera de incendios se volvió más ruidosa, la arquitectura no entró en pánico.

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

1) El disco de Postgres sigue creciendo incluso después de borrar logs antiguos

Síntomas: Uso de disco aumenta; deletes nocturnos; vacuum corre constantemente; consultas se vuelven más lentas con el tiempo.

Causa raíz: Tuplas muertas por MVCC + deletes por retención. El espacio no se recupera rápidamente; vacuum no da abasto; se acumula bloat.

Solución: Particiona por tiempo y dropea particiones. Si debes eliminar filas, hazlo en pequeños lotes y vacuum agresivamente, pero trátalo como parche.

2) Latencia de consultas en ClickHouse durante picos de ingestión

Síntomas: Dashboards con timeout; discos con alta util; conteo de partes sube rápido; backlog de merges crece.

Causa raíz: Inserts demasiado pequeños creando demasiadas partes; los merges consumen I/O y CPU; las consultas de lectura compiten.

Solución: Inserts por lotes (bloques más grandes), reduce escritores concurrentes por tabla por shard, mantén particiones gruesas (diarias) y monitorea partes/merges como métricas de primera clase.

3) El lag de replicación en Postgres aumenta cuando los logs se disparan

Síntomas: Réplica por streaming se queda atrás; failover inseguro; WAL crece; backups lentos.

Causa raíz: Volumen de WAL dominado por logs; mantenimiento de índices amplifica escrituras; la réplica no puede con el I/O.

Solución: Mueve logs fuera del cluster OLTP, minimiza índices, usa inserts por lotes o emplea ClickHouse para logs masivos y deja sólo eventos críticos en Postgres.

4) ClickHouse muestra advertencias de “demasiadas partes” y no mergea lo suficiente

Síntomas: Partes activas en aumento; sobrecarga de metadatos; consultas lentas; merges en segundo plano constantemente ocupados.

Causa raíz: Particionado demasiado fino (horario/minuto), batches de insert diminutos o demasiados shards con carga desigual.

Solución: Particiones más gruesas (día/mes), batches de insert más grandes y reevalúa la clave de sharding para evitar skew.

5) “No encontramos logs específicos rápidamente en ClickHouse”

Síntomas: Búsquedas puntuales por trace_id/request_id escanean rangos enormes; latencia impredecible.

Causa raíz: ORDER BY optimizado para analítica service/time, no para búsquedas por id; no hay índice de skipping adecuado para ese predicado.

Solución: Añade una estructura de lookup dedicada: una tabla pequeña indexada por trace_id con punteros (ts/service), o un índice de data skipping; o mantén las últimas N horas en Postgres/Redis para búsquedas tipo aguja.

6) El planner de Postgres deja de podar particiones correctamente

Síntomas: Consultas tocan muchas particiones incluso con filtros temporales; tiempo de planificación aumenta; CPU del coordinador se dispara.

Causa raíz: Predicados no sargables (funciones sobre ts), tipos de datos/zona horaria desajustados, o demasiadas particiones.

Solución: Mantén predicados simples (ts >= constante), estandariza el tipo timestamp, reduce el conteo de particiones y mantén estadísticas actualizadas.

Listas de verificación / plan paso a paso

Paso a paso: elegir Postgres vs ClickHouse para logs

  1. Clasifica tus casos de uso de logs: búsqueda operativa vs analítica vs trail de auditoría. Si es trail de auditoría, Postgres sigue en la foto.
  2. Anota tus 10 consultas principales con filtros reales y rangos temporales. Si la mayoría son group-bys sobre días, ClickHouse es favorecido.
  3. Cuantifica la ingestión: tasa sostenida, tasa en picos, tamaño medio de mensaje, writers concurrentes en pico.
  4. Define retención y SLO de buscabilidad: “buscable en 5 minutos”, “retener 30 días hot, 180 días cold”, etc.
  5. Decide el mecanismo de ciclo de vida: drop de particiones en Postgres o TTL en ClickHouse. Si no puedes operacionalizar la retención, acabarás almacenándolo todo por accidente.
  6. Elige un esquema mínimo: columnas tipadas para predicados comunes; payload en JSON o string.
  7. Construye una prueba de carga en staging que incluya retención y consultas de dashboard. Ingerir es fácil; ingerir + retención + consultas es donde los sistemas mueren.

Checklist operativo: almacenamiento de logs en Postgres (si insistes)

  • Particiona por día; automatiza creación y eliminación.
  • Prefiere BRIN en timestamp para particiones append-only masivas; mantiene índices B-tree al mínimo.
  • Vigila tuplas muertas y lag de autovacuum por partición.
  • Mantén los logs fuera del primary de OLTP si es posible.
  • Establece un tope estricto en el rango temporal de consultas ad-hoc; empuja analítica pesada a otro sitio.

Checklist operativo: almacenamiento de logs en ClickHouse (recomendado por defecto para firehose)

  • Inserciones por lotes; aplica tamaño mínimo de batch y frecuencia máxima de insert.
  • Usa particiones diarias salvo que tengas una razón fuerte para no hacerlo.
  • Configura ORDER BY alineado a tus filtros más comunes; no lo trates como índice único.
  • Monitorea: partes activas, bytes en merges, read_bytes de consultas, util disco y errores de insert.
  • Define TTL y valida que borre lo que crees que borra.
  • Define límites: rango máximo de consulta en dashboards; usa rollups/vistas materializadas para ventanas largas.

Plan de migración: Postgres logs → ClickHouse sin drama

  1. Dual-write o replay: empieza a enviar logs a ClickHouse mientras mantienes Postgres tal cual.
  2. Backfill de particiones recientes primero (últimos 7–30 días). No rellenes todo el historial antes de probar el nuevo sistema.
  3. Valida consultas: compara conteos y agregados clave. Espera pequeñas diferencias si tenías duplicados o eventos tardíos; decide cómo manejarlos.
  4. Mueve dashboards a ClickHouse, mantén Postgres para búsquedas puntuales si hace falta.
  5. Reduce retención en Postgres a una ventana más corta; dropea particiones antiguas.
  6. Mantén una historia de rollback: si la ingestión a ClickHouse falla, los logs siguen en una cola y pueden re-reproducirse.

Preguntas frecuentes

1) ¿Puede Postgres manejar “muchos” logs?

Sí, si “muchos” aún encajan en el modelo operativo: particiones por tiempo, retención basada en drop, índices mínimos y no usarlo como almacén analítico. Si necesitas group-bys de varias semanas sobre miles de millones de filas, le pides a Postgres comportamiento de ClickHouse desde un sistema optimizado para garantías diferentes.

2) ¿Puede ClickHouse reemplazar a Elasticsearch para logs?

A menudo, para analítica y filtrado estructurado, sí. Para búsqueda de texto completo sobre cadenas arbitrarias con ranking por relevancia y fuzzy matching, ClickHouse puede hacer algo de esto pero no es su personalidad principal. Muchos equipos usan ClickHouse para analítica tipo métricas y mantienen una capa de búsqueda separada para “grep a escala”.

3) ¿Qué pasa con logs JSON—JSONB en Postgres vs Map/String en ClickHouse?

JSONB en Postgres es excelente para indexación flexible y consultas de contención, pero es caro a escala si indexas demasiado. ClickHouse suele funcionar mejor cuando los campos consultados con frecuencia son columnas tipadas y el resto es un string JSON crudo o una estructura Map según la herramienta de ingestión. El patrón ganador en ambos: promociona claves calientes a columnas.

4) ¿Cómo aplico retención de forma segura?

En Postgres: dropea particiones, no borres filas. En ClickHouse: TTL suele ser la palanca adecuada, pero valídalo y vigila la presión de merges. Además, ensaya la retención como si fuera una característica de producción—porque lo es.

5) ¿Y si necesito ingestión “exactly once”?

Ninguno de los dos sistemas te da exactly once end-to-end sin ayuda. Postgres puede imponer constraints de unicidad, pero eso puede ser caro en logs de alto volumen. ClickHouse es típicamente “at least once” friendly; implementas idempotencia upstream o usas estrategias de deduplicación (almacenando un id de evento y deduplicando en consultas o mediante engines/settings especializados).

6) ¿Debería shardear ClickHouse desde el inicio?

Shardea cuando un solo nodo no puede cubrir tu ingestión o necesidades de consulta con margen. El sharding añade complejidad operativa: reequilibrio, consultas distribuidas y modos de fallo. Empieza con un nodo único o una pequeña configuración replicada si puedes, pero no esperes a shardear cuando ya te hayas quedado sin disco.

7) ¿Es TimescaleDB el punto medio?

Pueda ser, especialmente si estás comprometido con el ecosistema Postgres y necesitas optimizaciones de series temporales. Pero sigue siendo Postgres por debajo: la retención y la analítica pesada siguen siendo un centro de costes. Si tu núcleo es analítica de logs a escala, ClickHouse sigue siendo el ajuste más directo.

8) ¿Cuál es el mayor “gotcha” que la gente pasa por alto con ClickHouse?

Las partes y los merges. Si insertas batches diminutos, creas partes diminutas. Las partes diminutas crean deuda de merge. La deuda de merge se convierte en saturación de disco y latencia de consulta. Diseña y opera alrededor de esto desde el día uno.

9) ¿Cuál es el mayor “gotcha” que la gente pasa por alto con Postgres?

Los deletes no son gratis. MVCC significa que el churn crea tuplas muertas, y vacuum es trabajo real que compite con tu carga. Si la retención es deletes por fila en tablas masivas, ya has escogido un incidente futuro.

Próximos pasos que puedes hacer esta semana

  • Anota tus 10 consultas principales de logs y clasifícalas: búsqueda puntual, búsqueda de ventana corta o analítica de ventana larga. Esa lista decide la base de datos más que las opiniones.
  • Si estás en Postgres: implementa particionado por tiempo y cambia la retención a drop de particiones. Luego mide de nuevo el porcentaje de tuplas muertas.
  • Si estás en ClickHouse: revisa hoy partes activas y backlog de merges. Si las partes se disparan, arregla el batching antes de añadir hardware.
  • Establece límites operativos: los dashboards no deberían por defecto mostrar 30 días si tu cluster está dimensionado para 24 horas. Haz que las consultas “caras” sean explícitas.
  • Separa logs de auditoría de logs operativos: mantén los primeros correctos y relacionales; haz que los segundos sean rápidos y económicos de agregar.

La respuesta correcta raramente es “Postgres o ClickHouse”. Suele ser “Postgres para lo que debe ser correcto y relacional, ClickHouse para la manguera de incendios”. Si intentas que un sistema haga ambas cosas, acabarás con ninguno—sólo muchos gráficos y un calendario lleno de revisiones de incidentes.

← Anterior
Rootkit de Sony: cuando un CD de música actuó como malware
Siguiente →
Refrigeración líquida para GPUs: ¿decisión inteligente o cosplay caro?

Deja un comentario