02 Analizar y diseñar
Inspecciona la carga de Elasticsearch, traduce su modelo de datos y sus consultas, y registra las decisiones de arquitectura de la migración.
Ejecuta este módulo desde el directorio de artefactos del taller:
cd "$(git rev-parse --show-toplevel)/workshops/elasticsearch_migration_lab/part2"Tiempo estimado: 60–90 minutos
Descripción general
Antes de utilizar herramientas de migración, analizarás el entorno de Elasticsearch en ejecución, mapearás sus conceptos a ClickHouse (el destino), traducirás consultas representativas a SQL de ClickHouse y redactarás un registro de decisiones de arquitectura (ADR) que documente la estrategia de migración. En esta parte no se modifica la infraestructura: es un ejercicio de reflexión que guía el plan de ejecución de la Parte 3.
Requisitos previos
- El entorno de la Parte 1 está en ejecución (consulta la comprobación de la Parte 1)
- Pasan todas las comprobaciones:
bash ../part1/validation/check.sh jqestá instalado (brew install jqen macOS,apt-get install jqen Debian/Ubuntu)
Lo que producirás
Tres entregables, todos en exercises/:
| # | Entregable | Archivo |
|---|---|---|
| 2A | Hoja de trabajo del modelo de datos que analiza el entorno ES, propone un diseño de ClickHouse y registra una referencia de latencia para compararla en la Parte 3 | exercises/worksheet.md |
| 2B | SQL de ClickHouse para 5 consultas representativas de Elasticsearch | exercises/query-translation.md |
| 2C | Registro de decisiones de arquitectura con 7 decisiones estratégicas | exercises/adr-template.md |
Las respuestas modelo están en solutions/. Completa cada ejercicio antes de consultarlas.
Referencia: mapeo de conceptos fundamentales
Utiliza esta tabla en los Ejercicios 2A y 2C.
| Concepto de Elasticsearch | Equivalente en ClickHouse | Diferencia clave |
|---|---|---|
| Índice | Tabla | ES crea índices nuevos por periodo (rollover de ILM); CH usa una sola tabla con particiones |
| Flujo de datos | Una tabla MergeTree | ES abstrae índices rotativos + ILM + rollover automático; CH los sustituye por una tabla particionada, sin rotación ni gestión de flujos |
| Documento | Fila | Los documentos de ES tienen esquema flexible; las filas de CH están ligadas al esquema (con tipos JSON/Map para aportar flexibilidad) |
| Campo | Columna | En ES se añaden campos dinámicamente; en CH las columnas se definen explícitamente (salvo con el tipo JSON) |
| Mapeo / plantilla de índice componible | Esquema de tabla (DDL) | ES compone plantillas de componentes; CH utiliza un solo CREATE TABLE |
| Shard | Shard (lógico) | Los shards de ES son estructuras físicas de Lucene ligadas al heap de JVM; los de CH son lógicos y escalan verticalmente |
| Réplica | Réplica | ES usa replicación primaria-réplica síncrona; CH es asíncrono de forma predeterminada |
| Índice invertido | Clave primaria + índices de salto | ES indexa todos los campos; CH usa claves primarias ordenadas e índices de salto opcionales bloom_filter / text (tokenbf_v1 / ngrambf_v1 obsoletos >= 26.2) |
| ILM (hot → warm → cold → delete) | TTL + particiones (solo eliminación) | ClickHouse Cloud guarda todos los datos en almacenamiento de objetos con caché local automática; las capas hot/warm/cold son irrelevantes. Solo hace falta TTL … DELETE para caducar datos. |
Pipeline de ingesta (geoip, user_agent, grok, script) | Vistas materializadas + columnas materializadas + diccionarios | ES transforma antes de indexar con procesadores; CH usa vistas materializadas como disparadores durante la inserción, columnas materializadas para expresiones por fila y diccionarios para búsquedas de enriquecimiento |
| Transformaciones de Elasticsearch (rollups) | Vistas materializadas incrementales + AggregatingMergeTree | ES vuelve a agregar periódicamente; CH utiliza estados parciales de agregación incremental que se combinan automáticamente |
| Reglas de alertas de Kibana | Grafana Alerting / HyperDX Alerts / MV precalculadas | ClickHouse no incorpora alertas: utiliza Grafana con la fuente de datos de ClickHouse, alertas de HyperDX o precalcula condiciones mediante vistas materializadas |
| Kibana | HyperDX | Ambas son interfaces de observabilidad; HyperDX es nativo de OTel |
| Elastic Agent / Filebeat | OpenTelemetry Collector | ES usa agentes propietarios; ClickStack usa OTel Collector, independiente del proveedor |
| ECS (Elastic Common Schema) | Convenciones semánticas de OTel | Los nombres de atributos difieren; ECS se está integrando en la especificación OTel |
Referencia: mapeo de ECS a convenciones semánticas de OTel
En la Parte 3 configurarás OTel Collector para emitir campos nativos de OTel. Utiliza esta tabla para traducir cada campo ECS documentado en el Ejercicio 2A. Los campos que permanecen en los Maps LogAttributes / SpanAttributes / ResourceAttributes del collector se indican como tales; promuévelos a columnas de nivel superior solo cuando los patrones de consulta lo justifiquen (consulta la Decisión 3 del ADR).
| Campo ECS (Elasticsearch) | Equivalente OTel (ClickHouse) | Observaciones |
|---|---|---|
@timestamp | Timestamp (nivel superior DateTime64(9)) | |
message | Body (nivel superior String) | |
log.level / level | SeverityText + SeverityNumber | OTel añade un nivel numérico 1–24 |
event.severity | SeverityText | |
service / service.name | ServiceName (nivel superior LowCardinality(String)) | |
service.version | ServiceVersion / ResourceAttributes['service.version'] | |
host.hostname / hostname | host.name (ResourceAttributes['host.name']) | ECS admite el alias host.name; OTel solo usa host.name |
host.ip | host.ip (atributo de recurso) | |
trace.id | TraceId (nivel superior String) | |
span.id / transaction.id | SpanId | |
parent.id | ParentSpanId | |
http.request.method | http.request.method | Igual en ambos: OTel adoptó el nombre de ECS |
http.response.status_code | http.response.status_code | Igual |
client.ip / source.ip / remote_addr | client.address | Almacenado como string |
user_agent.original / user_agent | user_agent.original | |
user_agent.name / user_agent_parsed.name | user_agent.name | Resultado del analizador UA |
error.message | exception.message (evento de span) | |
error.stack_trace | exception.stacktrace | |
transaction.name | span.name (para transacciones) | |
transaction.duration.us | Duration (Int64 nanosegundos) | Cambio importante de unidad: µs → ns |
labels.* | ResourceAttributes / SpanAttributes / LogAttributes | Tipo Map |
Ejercicio 2A: mapeo del modelo de datos
Entregable: Completa exercises/worksheet.md.
Examina tu entorno de Elasticsearch y rellena una sección de la hoja por cada flujo (logs-web_access-lab, logs-application-lab, logs-infrastructure-lab). Después propón un diseño de tabla de ClickHouse para cada uno.
Comandos de inspección
Ejecútalos desde cualquier host que pueda acceder a Elasticsearch en http://localhost:9200 (el mismo host de la Parte 1).
Enumerar flujos de datos y sus índices subyacentes:
curl -s "http://localhost:9200/_data_stream/logs-*" | jq '.data_streams[] | {name, indices: [.indices[].index_name], generation}'Obtener el mapeo de un flujo:
curl -s "http://localhost:9200/logs-web_access-lab/_mapping" | jq .Contar campos hoja únicos del mapeo — excluye metacampos de ES (_id, _index, _source, _doc_count, etc.) que inflarían el recuento en ~14:
curl -s "http://localhost:9200/logs-web_access-lab/_field_caps?fields=*" \
| jq '[.fields | to_entries[] | select(.key | startswith("_") | not)] | length'Obtener estadísticas del índice (documentos y tamaño):
curl -s "http://localhost:9200/logs-web_access-lab/_stats" \
| jq '.indices | to_entries[] | {index: .key, docs: .value.primaries.docs.count, size_bytes: .value.primaries.store.size_in_bytes}'Obtener la asignación de shards:
curl -s "http://localhost:9200/_cat/shards/logs-web_access-*?v"Inspeccionar la política ILM y la fase actual:
curl -s "http://localhost:9200/_ilm/policy/lab-observability-policy" | jq .
curl -s "http://localhost:9200/logs-web_access-lab/_ilm/explain" \
| jq '.indices | to_entries[] | {index: .key, phase: .value.phase, age: .value.age}'Inspeccionar los pipelines de ingesta:
for p in default-enrichment web-access-enrichment app-log-enrichment infra-log-parsing; do
echo "=== $p ==="
curl -s "http://localhost:9200/_ingest/pipeline/$p" | jq ".\"$p\".processors"
doneComprobar estadísticas de pipelines (documentos y fallos):
curl -s "http://localhost:9200/_nodes/stats/ingest?filter_path=nodes.*.ingest.pipelines" \
| jq '.nodes | to_entries[0].value.ingest.pipelines'Verificar que funciona el enriquecimiento:
curl -s "http://localhost:9200/logs-web_access-lab/_search?size=1" \
| jq '.hits.hits[0]._source | {remote_addr, geo, user_agent_parsed, "event.severity": ."event".severity}'Contar valores únicos en campos de alta cardinalidad:
curl -s -H 'Content-Type: application/json' "http://localhost:9200/logs-web_access-lab/_search?size=0" -d '{
"aggs": {
"services": {"cardinality": {"field": "service"}},
"paths": {"cardinality": {"field": "request_path.keyword"}},
"ips": {"cardinality": {"field": "remote_addr"}}
}
}' | jq .aggregationsInspección: Carga de trabajo 2 (trazas y logs APM)
OTel Demo escribe en traces-apm-* y en unos 18 flujos logs-apm.app.*. La Sección 4 de la hoja cubre esta carga.
Enumerar flujos APM:
curl -s "http://localhost:9200/_data_stream/traces-apm*,logs-apm*" \
| jq '.data_streams[] | {name, generation}'Muestrear las claves de nivel superior de una traza:
curl -s "http://localhost:9200/traces-apm-*/_search?size=1" \
| jq '.hits.hits[0]._source | keys'Cardinalidades de campos de traza y división por tipo de evento:
curl -s -H 'Content-Type: application/json' "http://localhost:9200/traces-apm-*/_search?size=0" -d '{
"aggs": {
"services": {"cardinality": {"field": "service.name"}},
"transactions":{"cardinality": {"field": "transaction.name"}},
"languages": {"terms": {"field": "service.language.name"}},
"event_types": {"terms": {"field": "processor.event"}}
}
}' | jq .aggregationsPercentiles de duración de transacciones (para p50/p95 de la Sección 4a):
curl -s -H 'Content-Type: application/json' "http://localhost:9200/traces-apm-*/_search?size=0" -d '{
"query": {"term": {"processor.event": "transaction"}},
"aggs": {
"dur_pct": {"percentiles": {"field": "transaction.duration.us", "percents": [50, 95, 99]}}
}
}' | jq .aggregationsDocumentos de flujos de logs por servicio de OTel Demo:
curl -s "http://localhost:9200/_cat/indices/logs-apm.app.*?h=index,docs.count&format=json" \
| jq 'sort_by(-(."docs.count" | tonumber)) | .[] | {index, docs: .["docs.count"]}'Registrar una referencia de latencia
Antes de diseñar el esquema de destino, registra cuánto tardan 3 consultas canónicas en la pila ES actual. En la Parte 3 repetirás los equivalentes de ClickHouse. Usa el campo .took (milisegundos que ES dedicó a servir la consulta, sin red ni serialización).
# Q1 — Top 10 request paths (status 200)
for i in 1 2 3; do
curl -s -H 'Content-Type: application/json' \
"http://localhost:9200/logs-web_access-lab/_search?size=0" -d '{
"query":{"bool":{"filter":[{"term":{"status":"200"}}]}},
"aggs":{"top_paths":{"terms":{"field":"request_path.keyword","size":10}}}
}' | jq '.took'
done
# Q2 — 5xx count per 1m in last 1h
for i in 1 2 3; do
curl -s -H 'Content-Type: application/json' \
"http://localhost:9200/logs-web_access-lab/_search?size=0" -d '{
"query":{"bool":{"filter":[
{"range":{"status":{"gte":"500"}}},
{"range":{"@timestamp":{"gte":"now-1h"}}}]}},
"aggs":{"errors":{"date_histogram":{"field":"@timestamp","fixed_interval":"1m"}}}
}' | jq '.took'
done
# Q3 — Trace lookup by any trace.id (grab a real one first)
TID=$(curl -s "http://localhost:9200/traces-apm-*/_search?size=1" | jq -r '.hits.hits[0]._source.trace.id')
for i in 1 2 3; do
curl -s -H 'Content-Type: application/json' \
"http://localhost:9200/traces-apm-*/_search?size=10" -d "{\"query\":{\"term\":{\"trace.id\":\"$TID\"}}}" \
| jq '.took'
doneRegistra la mediana de las 3 ejecuciones de cada consulta en la Sección 5 de exercises/worksheet.md.
Consejo: Al proponer la clave ORDER BY de ClickHouse, piensa qué combinación reúne mejor las filas consultadas juntas. Los dashboards de la Parte 1 son la guía principal de patrones de acceso.
Ejercicio 2B: traducción de patrones de consulta
Entregable: Completa exercises/query-translation.md.
El archivo contiene 5 consultas representativas de Elasticsearch extraídas de los dashboards y la interfaz APM de la Parte 1. Escribe el SQL de ClickHouse equivalente contra un esquema de destino llamado otel_logs (o otel_traces para búsquedas de trazas) con estas columnas:
| Columna | Tipo | Se mapea desde el campo ES |
|---|---|---|
Timestamp | DateTime64(9) | @timestamp |
ServiceName | LowCardinality(String) | service |
Body | String | message / línea de log bruta |
SeverityText | LowCardinality(String) | event.severity |
LogAttributes | Map(LowCardinality(String), String) | todos los demás campos (request_path, status, remote_addr, …) |
TraceId | String | trace.id (solo tabla de trazas) |
¿Por qué Map en lugar de columnas individuales? El esquema OTel utiliza un
Mappara almacenar atributos con flexibilidad y sin depender del proveedor. En la Parte 3 extraerás los campos consultados con frecuencia (comostatus) a columnas materializadas para mejorar el rendimiento.
Ejercicio 2C: registro de decisiones de arquitectura
Entregable: Completa exercises/adr-template.md.
Escribe un ADR breve que cubra siete decisiones estratégicas. No hay una única respuesta «correcta»: la justificación debe ser coherente con la configuración ES actual (Ejercicio 2A) y las restricciones de ClickHouse Cloud (almacenamiento columnar basado en objetos y sin alertas integradas).
Las siete decisiones:
- Enfoque de migración — ¿ejecución paralela, transición total o fases por tipo de log?
- Estrategia de agentes — ¿OTel Collector directo, puente Filebeat → Vector → OTel o Filebeat → Kafka → Vector → OTel?
- Estrategia de esquema — ¿esquema OTel predeterminado, personalizado con columnas materializadas o tabla de origen Null + MV? Especifícalo para cada tipo de log y las trazas APM, propón una clave
ORDER BYpara cada uno y decide cómo tratar los campos nuevos tras el lanzamiento (solo Map, promoción automática, columna JSON o híbrido). - Traducción del pipeline de ingesta — ¿dónde se implementa cada procesador de ES? GeoIP (diccionario o procesador del collector), análisis de user-agent y grok, derivación de severidad y marca de tiempo de ingesta.
- Ciclo de vida — fases ILM innecesarias (y por qué), periodo de retención y cláusula TTL.
- Migración de alertas — herramienta y recreación de 2 reglas de Kibana (
High Error Rate: >5% de 5xx en 5m;Service Heartbeat: ningún log de un servicio durante >3m). - Datos históricos — ¿backfill de los ~62 millones de documentos de ES, inicio desde cero conservando ES solo para lectura o solución híbrida? Si haces backfill, ¿con qué herramienta y cómo eliminas duplicados?
Comprobación
Antes de continuar a la Parte 3, verifica:
-
exercises/worksheet.mdtiene todos los campos cumplimentados para los 3 flujos de datos - Cada sección propone un diseño de tabla de ClickHouse (clave de partición, ORDER BY, columnas materializadas y TTL)
- Has identificado qué fases de ILM son innecesarias en ClickHouse Cloud y por qué
-
exercises/query-translation.mdcontiene SQL de ClickHouse funcional para las 5 consultas -
exercises/adr-template.mdcontiene una justificación para cada una de las 7 decisiones (no solo una elección) - Has comparado tu hoja y el ADR con solutions/ y puedes explicar las desviaciones deliberadas
Siguiente: Parte 3: Ejecución de la migración →
01 Construir el entorno de origen
Inicia Elasticsearch, Kibana, Filebeat, Elastic APM Server y OpenTelemetry Demo como referencia para la migración.
03 Ejecutar la migración
Aprovisiona ClickHouse Cloud, escribe en ambos destinos mediante OpenTelemetry, valida la paridad, explora ClickStack y realiza la transición.