Añades un panel «simple» en tiempo real. Producto lo adora. Los ejecutivos también. Entonces el checkout se pone raro: la latencia p95 se duplica, la CPU se dispara y MySQL empieza
a toser «demasiadas conexiones» como si estuviera audicionando para un drama médico.
El panel no es el villano. La arquitectura sí. Las bases OLTP y las cargas analíticas tienen diferentes físicas, y ignorarlo es la forma en que terminas
explicándole a finanzas por qué la “tasa de conversión” se convirtió brevemente en “cero”.
El verdadero problema: cargas mixtas y dominios de fallo compartidos
Los paneles en tiempo real fallan de forma sorprendentemente predecible: piden a los sistemas OLTP que se comporten como sistemas OLAP. A veces puedes
forzarlo con índices, réplicas y caché. Pero si el panel importa, crecerá. Adquirirá más filtros, más joins, más “una dimensión más”. Y un día se ejecuta durante un pico de campaña y tu flujo de pagos tiene un asiento en primera fila para tus ambiciones analíticas.
El problema central no es la velocidad de la consulta. Es la interferencia. OLTP y analítica compiten por los mismos recursos escasos:
- CPU (a la analítica le encanta; OLTP la necesita para concurrencia)
- Buffer pool / caché de páginas (la analítica lo churnea; OLTP se beneficia de estabilidad)
- I/O (la analítica hace scans; OLTP necesita lecturas/escrituras aleatorias de baja latencia)
- Bloqueos y latches (no siempre obvios, pero aparecen cuando menos los quieres)
- Slots de conexión (los paneles suelen ser “conversacionales”)
Los paneles también son cargas socialmente privilegiadas. Nadie dice “apaga el checkout por una hora.” Sí se dice “¿por qué el panel no está en tiempo real?” Así es como terminas optimizando lo equivocado.
Una verdad seca: “tiempo real” suele significar “lo suficientemente fresco para tomar decisiones”. Puede ser 2 segundos. Puede ser 2 minutos. Si no lo defines,
implementarás el peor tipo: tiempo real caro.
MySQL vs ClickHouse: en qué es realmente bueno cada motor
MySQL: la mula del checkout
MySQL es una base OLTP con un ecosistema maduro, herramientas operativas sólidas y comportamiento predecible bajo cargas transaccionales con muchas escrituras.
InnoDB está optimizado para:
- Muchas lecturas/escrituras pequeñas
- Búsquedas puntuales por clave primaria o índices secundarios bien elegidos
- Garantías transaccionales y lecturas consistentes
- Alta concurrencia con consultas cortas
MySQL puede ejecutar consultas similares a analítica. Incluso puede hacerlo bien si tu conjunto de datos es modesto y tus consultas son disciplinadas. Pero cuando los paneles comienzan
a hacer grandes scans, group-bys pesados, joins amplios y “últimos 90 días por X y Y y Z”, MySQL paga en I/O, churn del buffer pool y CPU. El precio aparece
en el único lugar que no quieres: la latencia de cola.
ClickHouse: el motor de paneles que devora scans al desayuno
ClickHouse es una base columnar OLAP diseñada para consultas analíticas de alto rendimiento sobre grandes conjuntos de datos. Brilla cuando necesitas:
- Agrupaciones rápidas sobre miles de millones de filas
- Dimensiones de alta cardinalidad (con mesura y buen diseño)
- Compresión y almacenamiento eficiente para datos de eventos append-heavy
- Ejecución de consultas en paralelo
Las fortalezas de ClickHouse vienen con un conjunto distinto de expectativas: modelas para lecturas, aceptas patrones de consistencia eventual y aprendes las
particularidades de merges, partes y trabajo en background. Si intentas usarlo como MySQL (actualizaciones fila por fila, workflows transaccionales, muchas mutaciones pequeñas),
te lo recordará—educadamente al principio—que lo estás usando mal.
Aquí está la línea de decisión que uso en producción: si la forma de la consulta es “scan + filter + group-by + ventana temporal”, eso pertenece a ClickHouse.
Si es “leer/actualizar un pequeño número de filas, hacer cumplir invariantes, cobrar dinero”, mantenlo en MySQL.
Broma #1: Los paneles en tiempo real sobre MySQL son como remolcar un barco con una scooter. Puedes hacerlo por un rato, pero todos aprenden algo desagradable.
Hechos históricos e contexto interesante (lo que explica los trade-offs de hoy)
- Las column stores se hicieron mainstream cuando los datasets analíticos superaron las asunciones de caché de row-store; la compresión y la ejecución vectorizada cambiaron el juego.
- ClickHouse nació en Yandex para alimentar analítica web a gran escala; se diseñó para responder “¿qué pasó?” rápidamente, no “¿esta compra tuvo éxito?”
- El diseño del buffer pool de InnoDB es excelente para conjuntos de trabajo calientes; los scans de panel pueden expulsar las páginas que necesita el checkout.
- La replicación de MySQL se usó históricamente para descargar lecturas; ayuda, pero los patrones de lectura analítica aún pueden saturar réplicas y la latencia de réplica se vuelve problemática.
- El auge del CDC (change data capture) se volvió común cuando los equipos quisieron desacoplar OLTP de OLAP sin construir jobs de exportación personalizados cada semana.
- Las vistas materializadas no son nuevas; lo que cambió es cuán barato es mantenerlas con almacenamiento y cómputo moderno, especialmente en motores columnar.
- Las dimensiones de alta cardinalidad antes se trataban como un impuesto; ClickHouse las hizo factibles, pero no gratuitas—la codificación por diccionario y el orden cuidadoso aún importan.
- “Tiempo real” solía significar batch cada hora para muchos negocios; ahora las expectativas son minutos o segundos porque los bucles de alertas y marketing se han acelerado.
Arquitecturas que funcionan (y por qué)
Patrón A: MySQL para OLTP, ClickHouse para paneles (recomendado para serio realtime)
Esta es la arquitectura adulta. Mantienes la verdad transaccional en MySQL, fluyes cambios, y consultas ClickHouse para los paneles. El beneficio clave
no es la velocidad. Es el control del radio de explosión. Los paneles pueden comportarse mal sin robar oxígeno al checkout.
Componentes centrales:
- MySQL primario para escrituras
- Réplicas MySQL para tráfico de lectura operativo (opcional)
- Pipeline CDC (basado en binlog) hacia ClickHouse
- Tablas ClickHouse optimizadas para consultas (familia MergeTree)
- Preagregaciones donde resulten rentables (vistas materializadas o tablas rollup)
Lo difícil: decidir qué enviar y cómo modelarlo. Si replicás ingenuamente tablas normalizadas de OLTP en ClickHouse y esperás
los mismos patrones de join, terminarás con consultas lentas y un equipo confundido. Desnormaliza con estrategia.
Patrón B: “Usar una réplica de lectura de MySQL para paneles” (funciona hasta que deja de hacerlo)
Una réplica es tentadora porque parece un almuerzo gratis. No lo es. El lag de replicación bajo lecturas pesadas es real, y si los paneles golpean la réplica con
grandes scans, básicamente estás construyendo un horno de I/O. Eso puede estar bien si:
- El dataset es relativamente pequeño
- Los paneles son limitados y estables
- Los requisitos de frescura son laxos
- Puedes tolerar picos ocasionales de lag
Este patrón falla cuando el uso del panel crece (y lo hará) o cuando tus eventos de negocio más importantes se correlacionan con picos de tráfico (lo hacen).
Patrón C: Dual-write (la app escribe en MySQL y ClickHouse)
El dual-write puede lograr baja latencia. También introduce riesgo de corrección: escrituras parciales, diferencias de orden, reintentos y el fenómeno de “funciona en staging”.
Si lo haces, necesitas idempotencia, backfill y una historia de reconciliación. De lo contrario el panel se vuelve un proyecto creativo.
Patrón D: ETL por lotes a ClickHouse (aburrido, barato, a veces perfecto)
Si tu negocio puede vivir con 5–15 minutos de frescura, el batch gana en simplicidad. Puedes ejecutar extracciones periódicas desde MySQL (idealmente desde una réplica),
cargar en ClickHouse y mantener el pipeline entendible. La gente subestima cuánto vale “entendible” a las 3 a.m.
Modelado de datos: la parte que todos omiten y luego pagan
Decisiones de esquema en MySQL que sabotean paneles
En OLTP, la normalización ayuda con la corrección y el rendimiento de escritura. En analítica, los esquemas normalizados trasladan el costo a joins y búsquedas repetidas.
Una consulta de panel que une orders, order_items, users, payments, shipments y promotions es una gran forma de quemar CPU e I/O.
El truco no es “desnormalizarlo todo.” El truco es crear un modelo de evento o de hechos que coincida con las preguntas que haces:
- Hechos: order_created, payment_captured, shipment_delivered, refund_issued
- Dimensiones: merchant, país, tipo de dispositivo, campaña, método de pago
- Tiempo: siempre de primera clase; los paneles son series temporales con opiniones
Diseño de tablas en ClickHouse: ORDER BY es tu índice y es un compromiso
En ClickHouse, la cláusula ORDER BY en tablas MergeTree es la palanca principal para la velocidad de consulta. Define cómo se organiza físicamente la data.
Elígela según tus filtros más comunes. Para paneles en tiempo real, eso suele ser:
- Ventana temporal primero (p. ej., event_date, event_time)
- Un pequeño conjunto de dimensiones comunes (p. ej., merchant_id, región, event_type)
Si eliges ORDER BY user_id porque se siente “como clave primaria”, te arrepentirás cuando cada panel filtre por tiempo y escanee todo de todos modos.
Pre-aggregación: cuándo ayuda, cuándo engaña
Las pre-aggregaciones (vistas materializadas, rollups) son poderosas. También crean un riesgo sutil: la gente confía en los números del panel como verdad absoluta mientras
la lógica de pre-aggregación se desvía silenciosamente de la semántica fuente. Esto es especialmente común con reembolsos, capturas parciales y conceptos de “estado final”.
Mi regla: pre-agrega solo cuando puedas definir la métrica con precisión, incluyendo eventos tardíos y correcciones. Si no, mantén hechos crudos y acepta mayor costo de consulta (en ClickHouse, a menudo puedes).
Pipelines de ingestión: CDC, batch, streaming y la mentira del “tiempo real”
CDC desde MySQL: a qué te comprometes realmente
CDC es la opción habitual para paneles “casi en tiempo real” porque ofrece baja latencia sin martillar MySQL con consultas frecuentes. Pero CDC no es magia;
es un sistema distribuido. Necesitas responder:
- Orden: ¿los eventos llegan en orden de commit? ¿por tabla? ¿por partición?
- Cambios de esquema: ¿cómo se manejan DDL y cambios de tipo?
- Borrados/actualizaciones: ¿los modelas como nuevos eventos o como mutaciones?
- Backfills: ¿cómo reingestas datos históricos de forma segura?
- Idempotencia: ¿puedes reproducir sin contar dos veces?
Para ClickHouse, muchos equipos prefieren un stream de eventos append-only incluso cuando la fuente es row-updated. En lugar de “actualizar fila,”
escribes “estado cambió” como eventos. No siempre es posible, pero con frecuencia es más limpio.
Presupuestos de frescura: defínelos o sufre
“Tiempo real” debería ser un presupuesto con SLOs: p95 de lag de ingestión < 30 segundos, p99 < 2 minutos, por ejemplo. Sin eso, los equipos persiguen el cuello de botella equivocado. Además: eventualmente necesitarás un banner de “los datos están retrasados” en la UI del panel. No es opcional; es honestidad.
Cita (idea parafraseada): Werner Vogels ha promovido la idea de que debes elegir modelos de consistencia intencionalmente; la confiabilidad viene de trade-offs explícitos.
Playbook de diagnóstico rápido (encuentra el cuello de botella en minutos)
Cuando los paneles son lentos o el checkout está sufriendo, no empieces con opiniones. Empieza con dónde se está gastando el tiempo y
qué sistema se está saturando. Este orden funciona bien bajo presión.
1) ¿El dolor está en MySQL, ClickHouse o en la capa de aplicación?
- Revisa la latencia p95 de consultas en MySQL y CPU/iowait
- Revisa la latencia de consultas en ClickHouse y carga de merges/trabajo en background
- Revisa la saturación del servicio de paneles (hilos, límites de pool, GC si aplica)
2) Si el checkout está lento: confirma interferencia de MySQL primero
- Busca SELECTs de larga duración de usuarios del panel
- Busca churn del buffer pool y lecturas desde disco
- Busca picos de conexiones y contención de hilos
3) Si los paneles están lentos: determina si es frescura de datos, forma de la consulta o layout de almacenamiento
- ¿Crece el lag de ingestión? Entonces es pipeline o la capacidad de insert/merge de ClickHouse.
- Si la ingestión está bien pero la consulta es lenta: es ORDER BY / particionado / scans demasiado amplios.
- Si las consultas son rápidas pero la UI es lenta: es la aplicación y la estrategia de caching.
4) Decide la mitigación inmediata
- Limitar las consultas del panel (rate limit, snapshots cacheados)
- Mover los paneles fuera de MySQL ahora (incluso a una réplica) si el checkout está ardiendo
- Reducir la ventana temporal y las dimensiones de las consultas temporalmente
- Escalar ClickHouse o afinar merges si es el cuello de botella
Tareas prácticas: comandos, salidas y qué decisión tomar
A continuación están las tareas prácticas que realmente ejecuto durante incidentes y ciclos de afinamiento. Cada una incluye un comando, salida de ejemplo, qué significa y la decisión
que impulsa. Puedes ejecutar la mayoría de estas desde un bastión o directamente en los hosts DB con el acceso apropiado.
Tarea 1: Detectar consultas del panel que están dañando MySQL ahora mismo
cr0x@server:~$ mysql -e "SHOW FULL PROCESSLIST\G" | sed -n '1,120p'
*************************** 1. row ***************************
Id: 83421
User: dashboard_ro
Host: 10.22.8.14:51122
db: checkout
Command: Query
Time: 37
State: Sending data
Info: SELECT date(created_at) d, count(*) FROM orders WHERE created_at > now() - interval 7 day GROUP BY d
*************************** 2. row ***************************
Id: 83455
User: checkout_app
Host: 10.22.3.7:60318
db: checkout
Command: Query
Time: 1
State: updating
Info: UPDATE inventory SET reserved = reserved + 1 WHERE sku = 'A17-44'
Significado: tienes un scan/group-by de larga ejecución en el primario, compitiendo con actualizaciones transaccionales.
Decisión: terminar o limitar la consulta del panel; mover los paneles a ClickHouse o al menos a una réplica; añadir protecciones de consulta inmediatamente.
Tarea 2: Matar al agresor específico (quirúrgico, no heroico)
cr0x@server:~$ mysql -e "KILL 83421;"
Query OK, 0 rows affected (0.00 sec)
Significado: la consulta ofensora queda terminada.
Decisión: seguir con una política de acceso: el usuario del panel no debe consultar el primario, punto.
Tarea 3: Revisar carga actual de MySQL y las sentencias más lentas vía Performance Schema
cr0x@server:~$ mysql -e "SELECT DIGEST_TEXT, COUNT_STAR, ROUND(SUM_TIMER_WAIT/1e12,2) AS total_s, ROUND(AVG_TIMER_WAIT/1e9,2) AS avg_ms FROM performance_schema.events_statements_summary_by_digest ORDER BY SUM_TIMER_WAIT DESC LIMIT 3\G"
*************************** 1. row ***************************
DIGEST_TEXT: SELECT date(created_at) d, count(*) FROM orders WHERE created_at > ? GROUP BY d
COUNT_STAR: 1821
total_s: 614.35
avg_ms: 337.41
*************************** 2. row ***************************
DIGEST_TEXT: SELECT * FROM cart_items WHERE cart_id = ?
COUNT_STAR: 92144
total_s: 211.02
avg_ms: 2.29
*************************** 3. row ***************************
DIGEST_TEXT: UPDATE inventory SET reserved = reserved + ? WHERE sku = ?
COUNT_STAR: 40122
total_s: 88.44
avg_ms: 2.20
Significado: el digest del panel domina el tiempo total de DB.
Decisión: priorizar mover esta métrica a ClickHouse o reescribirla para usar un índice/tabla resumen, y aplicar límites de consulta.
Tarea 4: Confirmar si hay churn del buffer pool
cr0x@server:~$ mysql -e "SHOW GLOBAL STATUS LIKE 'Innodb_buffer_pool_reads'; SHOW GLOBAL STATUS LIKE 'Innodb_buffer_pool_read_requests';"
+-------------------------+----------+
| Variable_name | Value |
+-------------------------+----------+
| Innodb_buffer_pool_reads| 19433211 |
+-------------------------+----------+
+----------------------------------+------------+
| Variable_name | Value |
+----------------------------------+------------+
| Innodb_buffer_pool_read_requests | 8821132441 |
+----------------------------------+------------+
Significado: las lecturas desde disco no son triviales; si el primer número sube rápido durante el uso del panel, los scans están expulsando páginas calientes.
Decisión: aislar la analítica del primario; considerar aumentar el buffer pool solo después de aislar (un horno más grande sigue siendo un horno).
Tarea 5: Revisar lag de replicación antes de culpar al “servidor del panel”
cr0x@server:~$ mysql -h mysql-replica-1 -e "SHOW REPLICA STATUS\G" | egrep "Seconds_Behind_Source|Replica_IO_Running|Replica_SQL_Running"
Replica_IO_Running: Yes
Replica_SQL_Running: Yes
Seconds_Behind_Source: 187
Significado: la réplica va ~3 minutos detrás; los paneles que lean de ella estarán desactualizados y pueden provocar tickets de “¿por qué no coinciden los números?”.
Decisión: aceptar SLAs de frescura explícitos, añadir más réplicas o mover analítica a ClickHouse con ingestión controlada.
Tarea 6: Verificar que el formato de binlog de MySQL sea compatible con CDC
cr0x@server:~$ mysql -e "SHOW VARIABLES LIKE 'binlog_format'; SHOW VARIABLES LIKE 'gtid_mode';"
+---------------+-------+
| Variable_name | Value |
+---------------+-------+
| binlog_format | ROW |
+---------------+-------+
+---------------+-------+
| Variable_name | Value |
+---------------+-------+
| gtid_mode | ON |
+---------------+-------+
Significado: binlogs ROW y GTIDs simplifican la corrección de CDC y la reanudación.
Decisión: si no es ROW, planear una migración; CDC sobre STATEMENT es una trampa a menos que disfrutes los casos límite.
Tarea 7: Inspeccionar consultas en ejecución en ClickHouse (encontrar el hog del panel)
cr0x@server:~$ clickhouse-client --query "SELECT query_id, user, elapsed, read_rows, formatReadableSize(read_bytes) rb, query FROM system.processes ORDER BY elapsed DESC LIMIT 2"
d8e2c6c1-8d1b-4c3f-bc45-91b34a6c12de dashboard 12.941 812334112 34.21GiB SELECT merchant_id, count() FROM events WHERE event_time > now() - INTERVAL 30 DAY GROUP BY merchant_id
a1a01f2a-9d72-4d85-9b43-423a1c91a8f1 internal 1.120 182991 17.02MiB INSERT INTO events FORMAT JSONEachRow
Significado: una consulta del panel está leyendo 34 GiB para responder a una pregunta que probablemente necesita una ventana temporal más estrecha o un ordenamiento mejor.
Decisión: arreglar ORDER BY/particionado, añadir rollups y limitar los valores por defecto de lookback del panel. También considerar cuotas de consulta.
Tarea 8: Comprobar la presión de merges en ClickHouse (trabajo en background robándote recursos)
cr0x@server:~$ clickhouse-client --query "SELECT database, table, sum(merge_type = 'Regular') AS regular_merges, sum(merge_type = 'TTL') AS ttl_merges, round(sum(elapsed),1) AS total_s FROM system.merges GROUP BY database, table ORDER BY total_s DESC LIMIT 5"
analytics events 4 0 912.4
analytics sessions 1 2 211.9
Significado: los merges están activos y corriendo por mucho tiempo en la tabla hot de events.
Decisión: ajustar tamaños de batch de insert, particionado y settings de merge; añadir capacidad si los merges están constantemente acumulados.
Tarea 9: Verificar explosión de parts en ClickHouse (clásico “demasiados inserts pequeños”)
cr0x@server:~$ clickhouse-client --query "SELECT table, count() parts, formatReadableSize(sum(bytes_on_disk)) disk FROM system.parts WHERE active AND database='analytics' GROUP BY table ORDER BY parts DESC LIMIT 3"
events 12844 1.92TiB
sessions 2211 204.11GiB
rollup_minute 144 9.87GiB
Significado: 12k parts activas sugiere que los inserts son demasiado granulados, causando deuda de merge y overhead en consultas.
Decisión: agrupar inserts, usar buffering o ajustar tu herramienta de ingestión; reducir partes antes de escalar hardware a ciegas.
Tarea 10: Medir lag de ingestión como métrica de primera clase (no adivines)
cr0x@server:~$ clickhouse-client --query "SELECT max(event_time) AS max_event, now() AS now_ts, dateDiff('second', max_event, now_ts) AS lag_s FROM analytics.events"
2025-12-30 11:58:29 2025-12-30 12:00:03 94
Significado: ClickHouse está ~94 segundos detrás del “ahora” para esta tabla.
Decisión: si tu SLO es 30 segundos, enfócate en pipeline y capacidad de merges; si el SLO es 2 minutos, estás bien y deja de entrar en pánico.
Tarea 11: Validar eficiencia de pruning en ClickHouse con EXPLAIN (¿estás escaneando todo?)
cr0x@server:~$ clickhouse-client --query "EXPLAIN indexes=1 SELECT count() FROM analytics.events WHERE event_date >= today()-1 AND merchant_id=42"
Expression ((Projection + Before ORDER BY))
ReadFromMergeTree (analytics.events)
Indexes:
MinMax
Keys: event_date
Condition: (event_date in [today() - 1, +Inf))
PrimaryKey
Keys: (event_date, merchant_id)
Condition: (event_date in [today() - 1, +Inf)) AND (merchant_id in [42, 42])
Significado: tanto MinMax como la condición de clave primaria aplican; el pruning debería ser decente.
Decisión: si no ves condiciones de índice útiles, revisa ORDER BY y la clave de partición.
Tarea 12: Confirmar que el panel no está haciendo DDoS a la BD con polling corto
cr0x@server:~$ sudo ss -tnp | awk '$4 ~ /:3306$/ {print $5}' | cut -d: -f1 | sort | uniq -c | sort -nr | head
422 10.22.8.14
38 10.22.8.15
21 10.22.3.7
Significado: un host del panel tiene 422 conexiones TCP a MySQL.
Decisión: añadir pooling de conexiones, imponer max connections, cachear resultados y detener el “refresh cada 1s” en la fuente.
Tarea 13: Revisar iowait a nivel de host en la máquina MySQL (chequeo rápido de la realidad)
cr0x@server:~$ iostat -x 1 3
Linux 6.8.0 (mysql-primary) 12/30/2025 _x86_64_ (16 CPU)
avg-cpu: %user %nice %system %iowait %steal %idle
18.12 0.00 6.33 24.51 0.00 51.04
Device r/s rkB/s rrqm/s %rrqm r_await rareq-sz w/s wkB/s w_await aqu-sz %util
nvme0n1 812.0 98304.0 0.0 0.00 8.12 121.1 244.0 16384.0 3.91 6.41 92.3
Significado: iowait alto y ~92% de utilización del disco; MySQL está limitado por I/O bajo la carga actual.
Decisión: detener los scans en el primario, luego revisar almacenamiento/IOPS; tirar discos más rápidos a cargas mixtas es una forma cara de seguir equivocado.
Tarea 14: Revisar uso de disco en ClickHouse y si los merges están bloqueados por espacio
cr0x@server:~$ df -h /var/lib/clickhouse
Filesystem Size Used Avail Use% Mounted on
/dev/nvme1n1p1 3.5T 3.3T 140G 96% /var/lib/clickhouse
Significado: ClickHouse está al 96% de uso; los merges pueden fallar o reducir velocidad, aumentando partes y ralentizando todo.
Decisión: liberar espacio ahora (retención/TTL), añadir disco o mover particiones frías. No “esperes hasta el fin de semana”; el fin de semana es cuando los merges deciden entrar en pánico.
Tres mini-historias corporativas desde la trinchera
Mini-historia 1: El incidente causado por una suposición equivocada
Un minorista de mercado medio quería paneles de ventas “en vivo” durante un empujón de temporada. El equipo asumió, razonablemente en papel, que una réplica de lectura aislaría
la analítica del checkout. Crearon una réplica, apuntaron el panel a ella y siguieron con sus labores.
El primer problema fue la frescura. Durante la primera gran ola de tráfico, el lag de replicación saltó de segundos a minutos. Los ejecutivos vieron el panel “congelarse”
y exigieron una solución. Alguien sugirió, con admirable confianza y sin sentido del peligro, que el panel debería leer del primario “solo por la campaña.”
Cinco minutos después, la latencia del checkout subió. No porque los SELECTs tomen locks de forma dramática, sino porque esos scans churnearon el buffer pool y saturaron el I/O. Las órdenes seguían
confirmando, pero la latencia de cola se puso fea y empezaron los timeouts.
La suposición equivocada fue sutil: “las lecturas son seguras.” Las lecturas no son seguras cuando son grandes, frecuentes y no cacheadas. Las lecturas pueden
absolutamente tumbar un sistema orientado a escrituras por privarlo de los recursos que necesita.
La solución no fue afinar consultas heroicamente. La solución fue arquitectural: retiraron los paneles de MySQL por completo, aceptaron 60–120 segundos de frescura durante
la campaña y desplegaron un pipeline CDC→ClickHouse posteriormente. El incidente terminó con una política: los paneles no consultan el primario, ni siquiera cuando alguien senior grita.
Mini-historia 2: La optimización que salió mal
Una empresa SaaS movió analítica a ClickHouse y se emocionó. Las consultas eran rápidas. A todos les encantó la flexibilidad. Luego alguien optimizó la ingestión para
“reducir latencia” enviando cada evento como su propio insert. Funcionó en pruebas. También creó una explosión de parts en producción.
En unos días, los merges en background corrían constantemente. La CPU parecía “bien” a primera vista, pero el disco estaba caliente y la latencia de consultas se volvió espinosa.
Los paneles eran rápidos, luego lentos al azar. El equipo empezó a añadir nodos. Ayudó por un breve tiempo, luego los merges los alcanzaron como un auditor fiscal.
Intentaron ajustar settings de merge e incluso consideraron cambiar motores de tabla. El problema real estaba aguas arriba: demasiados inserts pequeños. ClickHouse puede
ingerir mucho, pero quiere lotes. Las partes pequeñas cuestan overhead de metadata, overhead de merge y overhead en consultas. Es muerte por mil commits.
La solución fue aburrida: agrupar eventos por unos segundos e insertar en chunks. La latencia aumentó ligeramente, pero el sistema se volvió estable y más barato. La
optimización que salió mal fue perseguir unos segundos de frescura a costa de rendimiento sostenido.
Mini-historia 3: La práctica aburrida pero correcta que salvó el día
Otro equipo ejecutaba MySQL y ClickHouse para paneles. También tenían algo raro: un SLO de frescura escrito, una métrica de lag de ingestión visible en el panel y un runbook de on-call que empezaba con “revisar el lag antes de depurar consultas.”
Una tarde, los paneles empezaron a mostrar gráficas “planas”. Las ventas claramente ocurrían, pero la gráfica parecía muerta. Slack se llenó de pánico. El on-call siguió el runbook y revisó el tiempo máximo de evento en ClickHouse versus ahora. El lag era de 18 minutos. Eso no es un problema de consulta; es un problema de ingestión.
Revisaron el consumidor CDC y lo encontraron atascado en una sola fila mala debido a un cambio de esquema inesperado: una columna pasó de nullable a non-nullable y el mapeo de ingestión estaba rechazando registros. Porque tenían métricas explícitas, no perdieron una hora afinando consultas que ya estaban rápidas.
Aplicaron la corrección de mapeo de esquema, reprocesaron el backlog y los paneles se recuperaron. Nadie elogió el runbook. Nadie escribe poemas sobre “alertas de lag.” Pero la práctica aburrida salvó una tarde de confusión y evitó que alguien “arreglara” el problema apuntando los paneles a MySQL.
Broma #2: Un panel que consulta MySQL en producción es básicamente una prueba de rendimiento, excepto que los resultados los califican clientes enojados.
Errores comunes: síntoma → causa raíz → solución
1) Picos de latencia p95 en checkout cuando se lanza un panel
- Síntoma: CPU e iowait de MySQL suben; consultas lentas muestran SELECTs tipo analítica.
- Causa raíz: paneles consultando el primario o una réplica compartida que también atiende lecturas de la app; churn del buffer pool y saturación de I/O.
- Solución: aislar paneles a ClickHouse; aplicar permisos a nivel de usuario; añadir límites y timeouts en consultas; cachear respuestas de paneles.
2) Los paneles son “en tiempo real” pero los números no coinciden con MySQL
- Síntoma: ClickHouse muestra menos órdenes que MySQL en los últimos minutos; los backfills “arreglan” el pasado.
- Causa raíz: lag de CDC, eventos fuera de orden o semántica de updates/deletes faltante.
- Solución: rastrear lag de ingestión; diseñar claves de evento idempotentes; modelar cambios de estado explícitamente; añadir jobs de reconciliación para métricas críticas.
3) Consultas ClickHouse se ralentizan aleatoriamente aunque el hardware esté bien
- Síntoma: mismo panel a veces 200ms, a veces 8s; los merges muestran actividad.
- Causa raíz: demasiadas parts, backlog de merges o disco casi lleno causando throttling.
- Solución: agrupar inserts; monitorear conteo de parts; aumentar margen de disco; afinar particionado; añadir nodos solo después de reducir fragmentación de ingestión.
4) Dashboards basados en réplicas muestran datos obsoletos durante picos
- Síntoma: “Seconds_Behind_Source” salta; la frescura del panel es inconsistente.
- Causa raíz: la réplica no puede aplicar el relay log lo suficientemente rápido bajo presión de lectura; la replicación no es una vía analítica gratuita.
- Solución: mover paneles a ClickHouse; añadir réplica analítica dedicada; reducir carga de consultas; escalar I/O y CPU de la réplica.
5) El almacenamiento de ClickHouse crece más rápido de lo esperado
- Síntoma: uso de disco sube; políticas de retención no funcionan; merges churnean.
- Causa raíz: TTL/caídas de partición faltantes; almacenar columnas demasiado verbosas; duplicar eventos por ingestión no idempotente.
- Solución: imponer TTL; compactar esquema; añadir claves de deduplicación e ingestión exactamente-una vez (o al menos effectively-once vía idempotencia).
6) “Podemos simplemente añadir un índice” se vuelve ritual semanal
- Síntoma: MySQL tiene docenas de índices; las escrituras se ralentizan; aún no puedes satisfacer consultas de panel.
- Causa raíz: intentar que el indexado OLTP solucione scans OLAP; los índices ayudan en búsquedas puntuales, no en análisis dimensional ilimitado.
- Solución: dejar de indexar hasta la tristeza; mover analítica a ClickHouse y modelar para él.
Listas de verificación / plan paso a paso
Plan paso a paso: construir paneles en tiempo real sin destruir el checkout
-
Definir SLOs de frescura.
- Escribir objetivos p95 y p99 de lag de ingestión.
- Decidir qué ocurre cuando se incumplen (banner, caché de fallback, modo degradado).
-
Clasificar consultas de panel por forma.
- Scan + group-by + ventana temporal → ClickHouse.
- Búsquedas puntuales para drill-down → pueden permanecer en MySQL (o en un servicio separado).
-
Poner guardarraíles en MySQL ahora.
- Prohibir acceso de panel al primario.
- Establecer timeouts y tiempo máximo de ejecución para usuarios analíticos.
- Añadir rate limiting en la capa API.
-
Elegir tu enfoque de ingestión.
- CDC para baja latencia y simplicidad razonable.
- Batch para simplicidad cuando la frescura puede ser minutos.
- Evitar dual-write a menos que tengas madurez operativa fuerte.
-
Modelar una tabla de hechos en ClickHouse.
- Preferir eventos append-only con ids estables.
- Elegir ORDER BY basado en tiempo + dimensiones comunes.
-
Implementar pre-aggregaciones solo para consultas calientes probadas.
- Empezar con hechos crudos; medir el costo de consulta; luego agrupar donde compense.
- Definir eventos tardíos y comportamiento de correcciones.
-
Operacionalizarlo.
- Alertar sobre lag de ingestión, conteo de parts, margen de disco y latencia de consultas.
- Crear runbooks de on-call que empiecen por “¿los datos están retrasados?”
-
Probar modos de fallo.
- Consumidor CDC caído.
- ClickHouse con disco casi lleno.
- Consulta del panel ampliada accidentalmente a 365 días.
Lista de seguridad: proteger el checkout de la ambición analítica
- Los paneles no pueden conectar al primario de MySQL (ACL de red + refuerzo de credenciales).
- Usuarios MySQL para analítica con privilegios estrictos y límites bajos de recursos.
- La API de panel tiene caching y rate limiting.
- La ventana temporal por defecto del panel es pequeña; expandirla requiere acción explícita del usuario.
- ClickHouse tiene una política de margen de disco (objetivo < 80–85% usado).
- El lag de ingestión se muestra y está conectado a alertas.
Preguntas frecuentes
1) ¿Puedo mantener los paneles en MySQL si optimizo las consultas?
Para un dataset pequeño y un conjunto pequeño de paneles estables, sí. El momento en que los paneles se vuelven exploratorios (muchos filtros, ventanas largas),
estás luchando contra los objetivos de diseño del motor. Si el checkout importa, aisla la analítica cuanto antes.
2) ¿Es una réplica de lectura MySQL “suficientemente segura” para paneles?
Es más segura que golpear el primario, pero no “segura” por defecto. Los scans grandes pueden saturar la réplica, aumentar el lag y aún causar dolor operativo.
Una réplica analítica dedicada con controles de consulta estrictos puede funcionar como paso intermedio.
3) ¿Qué tan en tiempo real puede ser ClickHouse?
Segundos a minutos, dependiendo del batching de ingestión, la presión de merges y los patrones de consulta. Si intentas forzar ingestión sub-segundo con inserts minúsculos,
lo pagarás en otra parte (parts, merges, disco).
4) ¿Cuál es el mayor error de modelado en ClickHouse para paneles?
Elegir ORDER BY sin pensar en los filtros comunes, especialmente tiempo. Si tus paneles filtran por tiempo y merchant, tu ORDER BY debe reflejar eso.
5) ¿Necesito vistas materializadas en ClickHouse?
No desde el día uno. Empieza con hechos crudos y mide. Usa vistas materializadas cuando un pequeño número de consultas domine el costo y la semántica de la métrica sea estable.
6) ¿Cómo manejo updates y deletes de MySQL en ClickHouse?
Prefiere el modelado por eventos (append de “cambio de estado”) sobre mutaciones frecuentes. Si debes reflejar estado de fila, usa patrones de versionado y claves de deduplicación,
y acepta que las mutaciones pesadas pueden ser costosas.
7) ¿Por qué difieren los números entre MySQL y ClickHouse durante incidentes?
Usualmente por lag de ingestión, eventos fuera de orden o ingestión duplicada. Trata la “frescura de datos” como una métrica, no como una sensación, y haz que el panel la muestre.
8) ¿Qué métricas debería alertar para esta pila?
MySQL: latencia de consultas (p95/p99), threads activos, hit rate del buffer pool, util/disk iowait, lag de replicación.
ClickHouse: latencia de consultas, conteo de parts, backlog de merges, uso de disco, tasa de insert, lag de ingestión.
9) ¿Debería cachear resultados de panel?
Sí, a menos que tu panel deba ser literalmente segundo a segundo. Cachea en la capa API con TTL cortos y reglas de invalidación. Es más barato que “más base de datos.”
10) ¿Cuál es una definición sensata de “tiempo real” para paneles de negocio?
Lo que tus operadores puedan soportar de forma fiable. Muchas organizaciones prosperan con 30–120 segundos de frescura. Hazlo explícito, mídelo y muéstralo.
Conclusión: qué hacer a primera hora del lunes
Si las consultas de tu panel tocan el primario de MySQL, arregla eso primero. No mañana. Hoy. Añade un camino dedicado: o una réplica con límites estrictos como parche,
o (preferiblemente) ClickHouse como plano analítico.
Luego haz el trabajo que realmente previene incidentes repetidos:
- Definir SLOs de frescura y mostrar lag de ingestión en la UI del panel.
- Modelar hechos para ClickHouse con un ORDER BY que coincida con filtros reales.
- Agrupar la ingestión para evitar explosiones de parts y deuda de merges.
- Operacionalizar: alertar sobre lag, merges, margen de disco y peores consultas.
- Escribir el runbook y hacer que sea lo primero a lo que la gente recurra bajo estrés.
El objetivo no es “paneles que son rápidos.” El objetivo es “paneles que son rápidos sin convertir el checkout en un experimento científico.”
Separa las cargas, mide el lag y deja que cada base de datos haga el trabajo para el que fue diseñada.