# ClickHouse desde cero (V): producción, replicación y clusters

*Quinta y última entrega de la serie **[ClickHouse desde cero a pro](/search?tag=clickhouse-desde-cero)**. Tiempo de lectura estimado: 14 minutos.*

Llegamos al final. En las entregas anteriores has pasado de arrancar un ClickHouse en Docker ([I](/post/clickhouse-desde-cero-i-instalacion-y-primeros-pasos)) a diseñar tablas con `MergeTree` ([II](/post/clickhouse-desde-cero-ii-tipos-de-datos-y-mergetree)), escribir consultas analíticas serias ([III](/post/clickhouse-desde-cero-iii-consultas-analiticas-en-profundidad)) y pre-agregar con materialized views ([IV](/post/clickhouse-desde-cero-iv-materialized-views-projections-y-ttl)).

Ahora toca ponerlo en producción. Este post cubre cuatro cosas que todo equipo acaba necesitando: **replicación**, **sharding**, **backups** y **monitorización**. No vas a salir de aquí sabiendo operar un cluster de 50 nodos, pero sí con la cabeza puesta para diseñar algo que no se caiga a la primera.

## Cuándo replicar y cuándo sharding

Empieza por entender qué problema resuelve cada cosa. Las decisiones tempranas son las que más duelen si te equivocas.

- **Replicación**: copias de los mismos datos en varios nodos. Resuelve *alta disponibilidad*, *durabilidad* y *escalado de lecturas*. No resuelve el problema de que los datos no te caben en un nodo.
- **Sharding**: particiona los datos entre varios nodos. Resuelve *escalado horizontal*: cada nodo guarda una fracción. No da HA por sí solo; hay que combinar con replicación.

Para la mayoría de proyectos medianos, **2 réplicas sin sharding** es la respuesta correcta. Un ClickHouse con 2 nodos de 64 vCPU y 256 GB RAM aguanta decenas de miles de millones de filas sin despeinarse. No empieces por un cluster de 12 nodos porque *"por si acaso"*.

Si llegas a necesitar sharding, lo sabrás: tu disco no da más, los merges no acaban, las consultas paralelas saturan CPU en un solo nodo.

## ClickHouse Keeper

Históricamente, la replicación en ClickHouse dependía de **ZooKeeper**. Hoy existe **ClickHouse Keeper**, un reemplazo compatible escrito en C++ y distribuido con el propio ClickHouse. Mismo protocolo, menos memoria, menos quebraderos.

Lo necesitas obligatoriamente si vas a usar `ReplicatedMergeTree`. Puedes desplegarlo:

- **Como proceso separado** (recomendado en producción): 3 instancias de Keeper en máquinas distintas para quórum.
- **Dentro de clickhouse-server** (modo embebido): útil para pruebas y clusters pequeños.

Ejemplo de configuración embebida en `/etc/clickhouse-server/config.d/keeper.xml`:

```xml
<clickhouse>
  <keeper_server>
    <tcp_port>9181</tcp_port>
    <server_id>1</server_id>
    <log_storage_path>/var/lib/clickhouse/coordination/log</log_storage_path>
    <snapshot_storage_path>/var/lib/clickhouse/coordination/snapshots</snapshot_storage_path>
    <raft_configuration>
      <server><id>1</id><hostname>ch-01</hostname><port>9234</port></server>
      <server><id>2</id><hostname>ch-02</hostname><port>9234</port></server>
      <server><id>3</id><hostname>ch-03</hostname><port>9234</port></server>
    </raft_configuration>
  </keeper_server>

  <zookeeper>
    <node><host>ch-01</host><port>9181</port></node>
    <node><host>ch-02</host><port>9181</port></node>
    <node><host>ch-03</host><port>9181</port></node>
  </zookeeper>
</clickhouse>
```

Tres instancias es el mínimo razonable. Con dos no hay quórum; con una no hay HA.

Comprueba el estado desde cualquier nodo:

```bash
echo mntr | nc localhost 9181
echo stat | nc localhost 9181
```

## ReplicatedMergeTree

Una vez Keeper está en marcha, cambias `MergeTree` por `ReplicatedMergeTree` y automáticamente tienes replicación multi-master: cualquier réplica acepta escrituras y todas acaban sincronizadas.

```sql
CREATE TABLE blog.pageviews ON CLUSTER my_cluster (
  ts           DateTime,
  user_id      UInt64,
  path         String,
  country      LowCardinality(String),
  duration_ms  UInt32
)
ENGINE = ReplicatedMergeTree(
  '/clickhouse/tables/{shard}/blog/pageviews',
  '{replica}'
)
PARTITION BY toYYYYMM(ts)
ORDER BY (ts, user_id);
```

Dos detalles:

- El primer parámetro es la **ruta en Keeper**. Debe ser única por tabla y **compartida entre todas las réplicas del mismo shard**.
- El segundo es el **nombre de la réplica**, único dentro del shard.
- `{shard}` y `{replica}` son macros resueltas por cada nodo, declaradas en `config.xml` / `macros.xml`.

Archivo `macros.xml` en cada nodo:

```xml
<clickhouse>
  <macros>
    <shard>01</shard>
    <replica>ch-01</replica>
    <cluster>my_cluster</cluster>
  </macros>
</clickhouse>
```

`ON CLUSTER my_cluster` es una comodidad para que la consulta se ejecute en todos los nodos del cluster definido en `remote_servers`. Sin ella, tienes que crear la tabla a mano en cada nodo.

### Inserts y consistencia

Por defecto, un `INSERT` en una réplica devuelve `OK` en cuanto el dato está escrito localmente. La réplica lo propaga de forma asíncrona al resto.

Si tu caso exige que el `INSERT` no retorne hasta que N réplicas tengan el dato:

```sql
SET insert_quorum = 2;
INSERT INTO blog.pageviews ...;
```

Con 3 réplicas, `insert_quorum = 2` da "escritura durable en mayoría" al estilo Raft. Es más lento, pero ninguna escritura confirmada puede perderse al caerse una réplica.

## Sharding con Distributed

Cuando llega el momento del sharding, se trabaja con **dos tablas**:

1. Una **local** (normalmente `ReplicatedMergeTree`) que guarda el trozo correspondiente.
2. Una **Distributed** que es un *puntero* a las locales y presenta una vista única.

```sql
-- En cada nodo
CREATE TABLE blog.pageviews_local ON CLUSTER my_cluster (
  ts DateTime,
  user_id UInt64,
  ...
)
ENGINE = ReplicatedMergeTree(...)
ORDER BY (ts, user_id);

-- También en cada nodo
CREATE TABLE blog.pageviews ON CLUSTER my_cluster AS blog.pageviews_local
ENGINE = Distributed(
  my_cluster,          -- nombre del cluster
  blog,                -- base de datos
  pageviews_local,     -- tabla local
  cityHash64(user_id)  -- clave de sharding
);
```

Cuando consultas `SELECT ... FROM blog.pageviews`, ClickHouse:

1. Contacta con una réplica de cada shard.
2. Les manda la consulta en paralelo.
3. Mergea los resultados.

La clave de sharding determina cómo se distribuyen los datos. Elígela bien: algo con alta cardinalidad (como `user_id`) evita hotspots. No uses `country` porque "España" acabaría inflada en un shard y vacía en el resto.

### remote_servers

Es la definición del cluster, en `config.xml` o en un fichero `.xml` bajo `config.d/`:

```xml
<remote_servers>
  <my_cluster>
    <shard>
      <replica><host>ch-01</host><port>9000</port></replica>
      <replica><host>ch-02</host><port>9000</port></replica>
    </shard>
    <shard>
      <replica><host>ch-03</host><port>9000</port></replica>
      <replica><host>ch-04</host><port>9000</port></replica>
    </shard>
  </my_cluster>
</remote_servers>
```

Dos shards, dos réplicas cada uno. Total: 4 nodos. Cuando insertas en la Distributed, ClickHouse enruta a un shard concreto según la clave; cuando lees, pregunta a los dos.

## Configuración esencial de producción

Una muestra de cambios típicos respecto a los defaults:

```xml
<clickhouse>
  <!-- Logs rotados y con nivel razonable -->
  <logger>
    <level>information</level>
    <size>1000M</size>
    <count>10</count>
  </logger>

  <!-- Compresión por defecto, zstd para datos fríos -->
  <compression>
    <case>
      <min_part_size>100000000</min_part_size>
      <method>zstd</method>
      <level>3</level>
    </case>
  </compression>

  <!-- Límites de memoria por consulta -->
  <profiles>
    <default>
      <max_memory_usage>30000000000</max_memory_usage>       <!-- 30 GB por consulta -->
      <max_memory_usage_for_user>60000000000</max_memory_usage_for_user>
      <max_bytes_before_external_group_by>20000000000</max_bytes_before_external_group_by>
      <max_execution_time>300</max_execution_time>
    </default>
  </profiles>

  <!-- Retención de query_log -->
  <query_log>
    <database>system</database>
    <table>query_log</table>
    <partition_by>toYYYYMM(event_date)</partition_by>
    <ttl>event_date + INTERVAL 30 DAY DELETE</ttl>
    <flush_interval_milliseconds>7500</flush_interval_milliseconds>
  </query_log>
</clickhouse>
```

Recomendaciones específicas de sistema operativo:

- **Filesystem**: ext4 o XFS. Evita ZFS y btrfs si puedes, hay *caveats*.
- **Huge pages transparentes**: desactiva THP. Degradan rendimiento en cargas grandes.
- **Ulimits**: `nofile` a 262144, `memlock` a `unlimited`.
- **Swap**: desactivado o `swappiness = 1`.

## Monitorización

ClickHouse ya incluye métricas detalladas. Solo hay que exponerlas.

### Endpoint Prometheus nativo

En `config.xml`:

```xml
<prometheus>
  <endpoint>/metrics</endpoint>
  <port>9363</port>
  <metrics>true</metrics>
  <events>true</events>
  <asynchronous_metrics>true</asynchronous_metrics>
</prometheus>
```

Prometheus scrapea `http://nodo:9363/metrics`. Si ya usas [Prometheus y Grafana para servicios pequeños](/post/prometheus-y-grafana-para-servicios-pequenos), encaja igual.

### Tablas de `system` más útiles

```sql
-- ¿Están las réplicas al día?
SELECT database, table, is_leader, absolute_delay, queue_size
FROM system.replicas;

-- Consultas lentas de la última hora
SELECT query, query_duration_ms, read_rows, memory_usage
FROM system.query_log
WHERE event_time >= now() - INTERVAL 1 HOUR
  AND type = 'QueryFinish'
ORDER BY query_duration_ms DESC
LIMIT 20;

-- Errores recientes
SELECT name, value, last_error_time, last_error_message
FROM system.errors
WHERE last_error_time >= now() - INTERVAL 1 HOUR;

-- Parts por tabla (alerta si explotan)
SELECT database, table, count() AS parts
FROM system.parts
WHERE active
GROUP BY database, table
ORDER BY parts DESC
LIMIT 20;

-- Mutations en curso (pueden ser largas y pesadas)
SELECT * FROM system.mutations WHERE NOT is_done;
```

Alertas mínimas que deberías tener:

- `absolute_delay` de `system.replicas` > 60 segundos.
- Número de *parts* activas > ~300 por tabla. Señal de inserts mal dimensionados.
- Errores nuevos en `system.errors`.
- Espacio en disco < 20%.
- Uso de memoria > 85% sostenido.
- Latencia p99 de consultas por encima de tu SLO.

## Backups

ClickHouse tiene un comando `BACKUP` incorporado desde la versión 22.8:

```sql
BACKUP TABLE blog.pageviews TO S3('https://bucket.s3.amazonaws.com/backups/pageviews', 'key', 'secret');

BACKUP DATABASE blog TO Disk('backups', 'blog-2026-04-20.zip');

-- Restaurar
RESTORE TABLE blog.pageviews FROM S3(...);
```

Es incremental si el destino ya tiene un backup previo compatible. Para clusters con múltiples shards, necesitas orquestarlo nodo a nodo o usar `BACKUP ON CLUSTER`.

Alternativas clásicas:

- **`clickhouse-backup`** ([Altinity](https://github.com/Altinity/clickhouse-backup)): herramienta dedicada, muy madura, integración S3/GCS/Azure nativa.
- **Snapshots de volumen**: si tienes LVM/ZFS o discos en la nube, un snapshot atómico mientras haces `SYSTEM STOP MERGES` funciona.

Regla de oro: **prueba tus restores**. Un backup sin restore probado es placebo. Si no has restaurado nunca, no tienes backup.

## Retos típicos en producción

Una lista de cosas con las que casi todo el mundo acaba tropezando:

1. **"Too many parts"**. Significa que haces inserts demasiado pequeños o demasiado frecuentes. Agrupa en batches más grandes o usa `Buffer` engine delante.
2. **MVs que explotan en un insert**. Los MVs se ejecutan en el contexto del insert; si el MV falla, el insert falla. Prueba los MVs con datos representativos antes de activarlos en producción.
3. **Queries de usuarios consumiendo toda la memoria**. Usa `max_memory_usage` por perfil de usuario y obliga a que consultas ad hoc vayan por un perfil más restrictivo.
4. **Réplicas que se desincronizan**. Casi siempre es Keeper que no tiene quórum o que tiene latencia alta entre nodos. Mide Keeper.
5. **DDL `ON CLUSTER` que se queda bloqueado**. Usa `distributed_ddl_task_timeout` y monitoriza `system.distributed_ddl_queue`.
6. **Disco lleno tras un merge grande**. Un merge puede necesitar temporalmente 2× el espacio del *part* resultante. Mantén siempre margen.

## Recursos imprescindibles

- Documentación oficial: [clickhouse.com/docs](https://clickhouse.com/docs).
- Blog de ClickHouse, Inc.: artículos técnicos muy buenos sobre internals.
- Repositorio de GitHub [ClickHouse/ClickHouse](https://github.com/ClickHouse/ClickHouse): leer los *issues* enseña más que muchos tutoriales.
- El blog de Altinity sigue siendo una referencia para *how-tos* avanzados.

## Cierre de la serie

En cinco posts hemos recorrido ClickHouse desde instalarlo hasta operarlo en cluster. No hace falta dominarlo todo a la primera: cada proyecto te forzará a profundizar en uno u otro aspecto.

Si hay algo que me llevo de usar ClickHouse en producción durante años es esto: **el 80% del rendimiento viene de decisiones tomadas al crear la tabla**. Tipos adecuados, `ORDER BY` bien pensada, *particionado* sensato y un par de materialized views cubren la inmensa mayoría de los casos. El resto es operación, que es donde este último post intenta poner un poco de base.

Serie completa, por si estás aterrizando:

- [I: instalación y primeros pasos](/post/clickhouse-desde-cero-i-instalacion-y-primeros-pasos)
- [II: tipos de datos y MergeTree](/post/clickhouse-desde-cero-ii-tipos-de-datos-y-mergetree)
- [III: consultas analíticas en profundidad](/post/clickhouse-desde-cero-iii-consultas-analiticas-en-profundidad)
- [IV: materialized views, projections y TTL](/post/clickhouse-desde-cero-iv-materialized-views-projections-y-ttl)
- V: producción, replicación y clusters *(estás aquí)*

Y como lectura complementaria, el post introductorio [ClickHouse para desarrolladores que vienen de PostgreSQL](/post/clickhouse-para-desarrolladores-que-vienen-de-postgresql) sigue siendo útil como resumen en una única página para compartir con el equipo cuando tengas que justificar por qué vais a meter un motor nuevo.

Si has llegado hasta aquí, gracias por el rato. Si tienes dudas concretas sobre diseñar una tabla, migrar desde otro sistema o depurar una consulta lenta, mis DMs siguen abiertos.
