02 Analisar e projetar
Inspecione a carga de trabalho do Elasticsearch, traduza seu modelo de dados e suas consultas e registre as decisões da arquitetura de migração.
Execute este módulo a partir do diretório de artefatos do workshop:
cd "$(git rev-parse --show-toplevel)/workshops/elasticsearch_migration_lab/part2"Tempo estimado: 60–90 minutos
Visão geral
Antes de usar qualquer ferramenta de migração, você analisará o ambiente Elasticsearch em execução, mapeará seus conceitos para o ClickHouse (o destino), traduzirá consultas representativas para SQL do ClickHouse e escreverá um Registro de Decisões de Arquitetura (ADR) documentando sua estratégia de migração. Nenhuma alteração de infraestrutura ocorre nesta parte — este é um exercício de raciocínio que orienta o plano de execução da Parte 3.
Pré-requisitos
- O ambiente da Parte 1 está em execução (consulte o Checkpoint da Parte 1)
- Todas as verificações de validação estão passando:
bash ../part1/validation/check.sh - O
jqestá instalado (brew install jqno macOS,apt-get install jqno Debian/Ubuntu)
O que você produzirá
Três entregáveis, todos em exercises/:
| # | Entregável | Arquivo |
|---|---|---|
| 2A | Planilha do modelo de dados que analisa seu ambiente ES, propõe um projeto no ClickHouse e registra uma linha de base da latência das consultas para comparação na Parte 3 | exercises/worksheet.md |
| 2B | SQL do ClickHouse para 5 consultas representativas do Elasticsearch | exercises/query-translation.md |
| 2C | Registro de Decisões de Arquitetura que aborda 7 decisões estratégicas | exercises/adr-template.md |
As respostas-modelo estão em soluções/. Conclua cada exercício antes de consultá-las.
Referência: mapeamento dos conceitos principais
Use esta tabela ao realizar os Exercícios 2A e 2C.
| Conceito do Elasticsearch | Equivalente no ClickHouse | Principal diferença |
|---|---|---|
| Índice | Tabela | O ES cria novos índices por período (rollover de ILM); o CH usa uma única tabela com partições |
| Fluxo de dados | Uma única tabela MergeTree | O ES abstrai índices rotativos + ILM + rollover automático; o CH substitui isso por uma única tabela particionada — sem rotação nem gerenciamento de fluxos |
| Documento | Linha | Os documentos do ES têm esquema flexível; as linhas do CH seguem um esquema (com tipos JSON/Map para flexibilidade) |
| Campo | Coluna | Campos do ES podem ser adicionados dinamicamente; colunas do CH são definidas explicitamente (exceto com o tipo JSON) |
| Mapeamento/modelo de índice combinável | Esquema da tabela (DDL) | O ES combina modelos de componentes; o CH usa um único CREATE TABLE |
| Shard | Shard (lógico) | Shards do ES são estruturas físicas do Lucene vinculadas ao heap da JVM; shards do CH são lógicos e escaláveis verticalmente |
| Réplica | Réplica | O ES usa replicação síncrona entre primário e réplica; o CH usa replicação assíncrona por padrão |
| Índice invertido | Chave primária + índices de skipping | O ES indexa todos os campos por padrão; o CH usa chaves primárias ordenadas e índices de skipping opcionais bloom_filter/text (tokenbf_v1/ngrambf_v1 obsoletos a partir da 26.2) |
| ILM (hot → warm → cold → delete) | TTL + partições (somente exclusão) | O ClickHouse Cloud armazena todos os dados em armazenamento de objetos com cache local automático — as camadas hot/warm/cold são irrelevantes. Para expiração, basta TTL … DELETE. |
Pipeline de ingestão (geoip, user_agent, grok, script) | Views materializadas + colunas materializadas + dicionários | O ES transforma os dados antes da indexação com processadores; o CH usa views materializadas como gatilhos no momento da inserção, colunas materializadas para expressões por linha e dicionários para consultas de enriquecimento |
| Transformações do Elasticsearch (rollups) | Views materializadas incrementais + AggregatingMergeTree | As transformações do ES reagregam periodicamente; o CH usa estados parciais de agregação incremental que se mesclam automaticamente |
| Regras de alerta do Kibana | Alertas do Grafana/HyperDX/MVs pré-computadas | O ClickHouse não possui alertas integrados — use o Grafana com a fonte de dados do ClickHouse, alertas do HyperDX ou pré-calcule condições de alerta por meio de views materializadas |
| Kibana | HyperDX | Ambos são interfaces de observabilidade; o HyperDX é nativo do OTel |
| Elastic Agent/Filebeat | OpenTelemetry Collector | O ES usa agentes proprietários; o ClickStack usa o OTel Collector independente de fornecedor |
| ECS (Elastic Common Schema) | Convenções semânticas do OTel | Nomenclatura de atributos diferente; o ECS está sendo incorporado à especificação do OTel |
Referência: mapeamento do ECS para as convenções semânticas do OTel
Na Parte 3, você configurará o OTel Collector para emitir campos nativos do OTel. Use esta tabela para traduzir cada campo ECS documentado no Exercício 2A. Os campos mantidos nos Maps LogAttributes/SpanAttributes/ResourceAttributes do collector do OTel são indicados dessa forma — promova-os a colunas de nível superior somente quando os padrões de consulta justificarem isso (consulte a Decisão 3 do ADR).
| Campo ECS (Elasticsearch) | Equivalente OTel (ClickHouse) | Observações |
|---|---|---|
@timestamp | Timestamp (top-level DateTime64(9)) | |
message | Body (top-level String) | |
log.level / level | SeverityText + SeverityNumber | O OTel adiciona o nível numérico 1–24 |
event.severity | SeverityText | |
service / service.name | ServiceName (top-level LowCardinality(String)) | |
service.version | ServiceVersion / ResourceAttributes['service.version'] | |
host.hostname / hostname | host.name (ResourceAttributes['host.name']) | O ECS usa host.name como alias; o OTel usa apenas host.name |
host.ip | host.ip (resource attribute) | |
trace.id | TraceId (top-level String) | |
span.id / transaction.id | SpanId | |
parent.id | ParentSpanId | |
http.request.method | http.request.method | Igual nos dois — o OTel adotou a nomenclatura do ECS |
http.response.status_code | http.response.status_code | Igual |
client.ip / source.ip / remote_addr | client.address | Armazenado como string |
user_agent.original / user_agent | user_agent.original | |
user_agent.name / user_agent_parsed.name | user_agent.name | Saída do analisador de UA |
error.message | exception.message (evento de span) | |
error.stack_trace | exception.stacktrace | |
transaction.name | span.name (para transações) | |
transaction.duration.us | Duration (Int64 nanossegundos) | Mudança importante de unidade: µs → ns |
labels.* | ResourceAttributes / SpanAttributes / LogAttributes | Tipo Map |
Exercício 2A: mapeamento do modelo de dados
Entregável: conclua exercises/worksheet.md.
Examine o ambiente Elasticsearch em execução e preencha uma seção da planilha para cada fluxo de dados (logs-web_access-lab, logs-application-lab, logs-infrastructure-lab). Em seguida, proponha um projeto de tabela do ClickHouse para cada um.
Comandos de inspeção
Execute-os em qualquer host que consiga acessar o Elasticsearch em http://localhost:9200 (o mesmo host em que você executou a Parte 1).
Liste os fluxos de dados e seus índices subjacentes:
curl -s "http://localhost:9200/_data_stream/logs-*" | jq '.data_streams[] | {name, indices: [.indices[].index_name], generation}'Obtenha o mapeamento de um fluxo de dados:
curl -s "http://localhost:9200/logs-web_access-lab/_mapping" | jq .Conte os campos folha exclusivos do mapeamento — isso exclui os metacampos do ES (_id, _index, _source, _doc_count etc.), que aumentariam a contagem em cerca de 14:
curl -s "http://localhost:9200/logs-web_access-lab/_field_caps?fields=*" \
| jq '[.fields | to_entries[] | select(.key | startswith("_") | not)] | length'Obtenha as estatísticas do índice (contagem e tamanho dos documentos):
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}'Obtenha a alocação de shards:
curl -s "http://localhost:9200/_cat/shards/logs-web_access-*?v"Inspecione a política de ILM e a fase atual:
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}'Inspecione os pipelines de ingestão:
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"
doneVerifique as estatísticas dos pipelines (documentos processados e falhas):
curl -s "http://localhost:9200/_nodes/stats/ingest?filter_path=nodes.*.ingest.pipelines" \
| jq '.nodes | to_entries[0].value.ingest.pipelines'Verifique se o enriquecimento está funcionando:
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}'Conte os valores exclusivos em campos de alta cardinalidade:
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 .aggregationsInspeção: Carga de trabalho 2 (traces e logs de APM)
O OTel Demo grava em traces-apm-* e em cerca de 18 fluxos de dados logs-apm.app.*. A Seção 4 da planilha aborda essa carga de trabalho.
Liste os fluxos de dados de APM:
curl -s "http://localhost:9200/_data_stream/traces-apm*,logs-apm*" \
| jq '.data_streams[] | {name, generation}'Obtenha uma amostra das chaves de nível superior de um documento de trace:
curl -s "http://localhost:9200/traces-apm-*/_search?size=1" \
| jq '.hits.hits[0]._source | keys'Cardinalidades dos campos de trace e divisão 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 .aggregationsPercentis da duração das transações (use para p50/p95 da Seção 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 dos fluxos de logs por serviço do 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 uma linha de base da latência das consultas
Antes de projetar o esquema de destino, registre quanto tempo 3 consultas canônicas levam na stack atual do ES. Na Parte 3, você executará novamente as consultas equivalentes no ClickHouse e fará a comparação. Use o campo .took (milissegundos gastos pelo ES para atender à consulta, sem incluir rede/serialização).
# 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'
doneRegistre a mediana das 3 execuções de cada consulta na Seção 5 de exercises/worksheet.md.
Dica: Ao propor a chave ORDER BY do ClickHouse, pense em qual combinação de campos melhor agrupa as linhas consultadas juntas. Os dashboards criados na Parte 1 são seu principal guia dos padrões de acesso.
Exercício 2B: tradução de padrões de consulta
Entregável: conclua exercises/query-translation.md.
O arquivo do exercício contém 5 consultas representativas do Elasticsearch extraídas dos dashboards e da interface de APM da Parte 1. Para cada uma, escreva o SQL equivalente do ClickHouse para um esquema de destino chamado otel_logs (ou otel_traces para consultas de traces), com as seguintes colunas:
| Coluna | Tipo | Mapeada do campo do ES |
|---|---|---|
Timestamp | DateTime64(9) | @timestamp |
ServiceName | LowCardinality(String) | service |
Body | String | message/linha de log bruta |
SeverityText | LowCardinality(String) | event.severity |
LogAttributes | Map(LowCardinality(String), String) | todos os demais campos de log (request_path, status, remote_addr, …) |
TraceId | String | trace.id (somente tabela de traces) |
Por que usar Map em vez de colunas individuais? O esquema do OTel usa um
Mappara armazenar atributos de forma flexível e independente de fornecedor. Na Parte 3, você extrairá campos consultados com frequência (comostatus) para colunas materializadas a fim de melhorar o desempenho.
Exercício 2C: Registro de Decisões de Arquitetura
Entregável: conclua exercises/adr-template.md.
Escreva um ADR curto que aborde sete decisões estratégicas. Não há uma única resposta "correta" — o objetivo é que sua justificativa seja coerente tanto com a configuração atual do ES (do Exercício 2A) quanto com as restrições do ClickHouse Cloud (armazenamento colunar, baseado em objetos e sem alertas integrados).
As sete decisões:
- Abordagem de migração — execução paralela, transição direta ou em fases por tipo de log?
- Estratégia de agentes — OTel Collector direto, ponte Filebeat → Vector → OTel ou Filebeat → Kafka → Vector → OTel?
- Estratégia de esquema — esquema padrão do OTel, personalizado com colunas materializadas ou tabela de origem Null + MVs? Especifique-a para cada um dos 3 tipos de log e para os traces de APM, proponha uma chave
ORDER BYpara cada um e decida como tratar novos campos que surgirem após o lançamento (somente Map, promoção automática, coluna JSON ou solução híbrida). - Tradução do pipeline de ingestão — onde fica cada processador do ES? GeoIP (dicionário ou processador do collector), análise de user-agent, análise grok, derivação de severidade e timestamp de ingestão.
- Ciclo de vida dos dados — fases de ILM que se tornam desnecessárias (explique por quê), período de retenção e cláusula TTL.
- Migração de alertas — escolha da ferramenta e como recriar as 2 regras de alerta do Kibana (
High Error Rate: >5% de 5xx em 5 min;Service Heartbeat: nenhum log de um serviço por >3 min). - Estratégia para dados históricos — carregar retroativamente todos os cerca de 62 milhões de documentos do ES no ClickHouse, começar do zero e manter o ES como somente leitura ou adotar uma solução híbrida? Em caso de backfill, qual ferramenta usar e como remover duplicatas?
Checkpoint
Antes de prosseguir para a Parte 3, verifique:
- Todos os campos de
exercises/worksheet.mdestão preenchidos para os 3 fluxos de dados - Cada seção de fluxo de dados inclui uma proposta de projeto de tabela do ClickHouse (chave de partição, ORDER BY, colunas materializadas e TTL)
- Você identificou quais fases de ILM se tornam desnecessárias no ClickHouse Cloud e por quê
-
exercises/query-translation.mdcontém SQL funcional do ClickHouse para as 5 consultas -
exercises/adr-template.mdcontém uma justificativa para cada uma das 7 decisões (não apenas uma escolha) - Você comparou suas respostas da planilha e do ADR com as soluções/ e consegue explicar qualquer desvio intencional
Próximo: Parte 3: execução da migração →