02 Analyser et concevoir
Inspectez la charge de travail Elasticsearch, transposez son modèle de données et ses requêtes, puis consignez les décisions d'architecture de la migration.
Exécutez ce module depuis le répertoire des ressources de l'atelier :
cd "$(git rev-parse --show-toplevel)/workshops/elasticsearch_migration_lab/part2"Durée estimée : 60 à 90 minutes
Présentation
Avant de toucher à un quelconque outil de migration, vous allez analyser votre environnement Elasticsearch actif, mettre ses concepts en correspondance avec ClickHouse (la cible), traduire des requêtes représentatives en SQL ClickHouse et rédiger un relevé de décisions d'architecture (ADR) qui documente votre stratégie de migration. Aucune modification d'infrastructure n'intervient dans cette partie : il s'agit d'un exercice de réflexion qui guidera le plan d'exécution de la partie 3.
Prérequis
- L'environnement de la partie 1 est en cours d'exécution (consultez le jalon de la partie 1)
- Tous les contrôles de validation réussissent :
bash ../part1/validation/check.sh jqest installé (brew install jqsous macOS,apt-get install jqsous Debian/Ubuntu)
Livrables
Trois livrables, tous dans exercises/ :
| N° | Livrable | Fichier |
|---|---|---|
| 2A | Fiche d'analyse du modèle de données de votre environnement ES, proposition de conception ClickHouse et mesure de référence de la latence des requêtes à comparer dans la partie 3 | exercises/worksheet.md |
| 2B | SQL ClickHouse correspondant à 5 requêtes Elasticsearch représentatives | exercises/query-translation.md |
| 2C | Relevé de décisions d'architecture portant sur 7 décisions stratégiques | exercises/adr-template.md |
Les corrigés se trouvent dans solutions/. Terminez chaque exercice avant de les consulter.
Référence : correspondance des concepts fondamentaux
Servez-vous de ce tableau pour réaliser les exercices 2A et 2C.
| Concept Elasticsearch | Équivalent ClickHouse | Différence essentielle |
|---|---|---|
| Index | Table | ES crée de nouveaux index à intervalles réguliers (roulement ILM) ; CH utilise une seule table comportant des partitions |
| Flux de données | Une seule table MergeTree | ES masque les index successifs, la politique ILM et le roulement automatique ; CH les remplace par une seule table partitionnée, sans roulement ni gestion de flux |
| Document | Ligne | Le schéma des documents ES est flexible ; celui des lignes CH est fixe (avec les types JSON/Map pour préserver de la souplesse) |
| Champ | Colonne | Des champs peuvent être ajoutés dynamiquement dans ES ; les colonnes CH sont définies explicitement (sauf avec le type JSON) |
| Mapping / modèle d'index composable | Schéma de table (DDL) | ES assemble des modèles de composants ; CH utilise une seule instruction CREATE TABLE |
| Shard | Shard (logique) | Les shards ES sont des structures Lucene physiques liées au tas JVM ; les shards CH sont logiques et permettent une mise à l'échelle verticale |
| Réplica | Réplica | ES utilise une réplication synchrone entre primaire et réplica ; CH utilise par défaut une réplication asynchrone |
| Index inversé | Clé primaire + index de saut | ES indexe chaque champ par défaut ; CH utilise des clés primaires triées et, en option, des index de saut bloom_filter / text (tokenbf_v1 / ngrambf_v1 sont obsolètes à partir de la version 26.2) |
| ILM (chaud → tiède → froid → suppression) | TTL + partitions (suppression uniquement) | ClickHouse Cloud stocke toutes les données sur un stockage objet assorti d'un cache local automatique : les niveaux chaud/tiède/froid ne sont donc pas pertinents. Seul TTL … DELETE est nécessaire pour l'expiration. |
Pipeline d'ingestion (geoip, user_agent, grok, script) | Vues matérialisées + colonnes matérialisées + dictionnaires | ES transforme les données avec des processeurs avant leur indexation ; CH emploie des vues matérialisées comme déclencheurs à l'insertion, des colonnes matérialisées pour les expressions par ligne et des dictionnaires pour les recherches d'enrichissement |
| Transformations Elasticsearch (agrégations) | Vues matérialisées incrémentales + AggregatingMergeTree | Les transformations ES réagrègent les données périodiquement ; CH utilise des états d'agrégation partiels incrémentaux qui fusionnent automatiquement |
| Règles d'alerte Kibana | Alertes Grafana / alertes HyperDX / vues matérialisées précalculées | ClickHouse ne comporte pas de fonction d'alerte intégrée : utilisez Grafana avec la source de données ClickHouse, les alertes HyperDX ou précalculez les conditions d'alerte au moyen de vues matérialisées |
| Kibana | HyperDX | Ce sont deux interfaces d'observabilité ; HyperDX est nativement compatible avec OTel |
| Elastic Agent / Filebeat | OpenTelemetry Collector | ES utilise des agents propriétaires ; ClickStack emploie l'OTel Collector, indépendant des fournisseurs |
| ECS (Elastic Common Schema) | Conventions sémantiques OTel | Les noms d'attributs diffèrent ; ECS est en cours d'intégration dans la spécification OTel |
Référence : correspondance entre ECS et les conventions sémantiques OTel
Dans la partie 3, vous configurerez l'OTel Collector afin d'émettre des champs natifs OTel. Utilisez ce tableau pour transposer chaque champ ECS consigné dans l'exercice 2A. Les champs conservés dans les Maps LogAttributes / SpanAttributes / ResourceAttributes du collecteur OTel sont signalés comme tels : ne les promouvez en colonnes de premier niveau que si les motifs de requête le justifient (voir la décision 3 de l'ADR).
| Champ ECS (Elasticsearch) | Équivalent OTel (ClickHouse) | Remarques |
|---|---|---|
@timestamp | Timestamp (DateTime64(9) de premier niveau) | |
message | Body (String de premier niveau) | |
log.level / level | SeverityText + SeverityNumber | OTel ajoute un niveau numérique de 1 à 24 |
event.severity | SeverityText | |
service / service.name | ServiceName (LowCardinality(String) de premier niveau) | |
service.version | ServiceVersion / ResourceAttributes['service.version'] | |
host.hostname / hostname | host.name (ResourceAttributes['host.name']) | ECS accepte l'alias host.name ; OTel utilise uniquement host.name |
host.ip | host.ip (attribut de ressource) | |
trace.id | TraceId (String de premier niveau) | |
span.id / transaction.id | SpanId | |
parent.id | ParentSpanId | |
http.request.method | http.request.method | Identique dans les deux : OTel a adopté le nom ECS |
http.response.status_code | http.response.status_code | Identique |
client.ip / source.ip / remote_addr | client.address | Stocké sous forme de chaîne |
user_agent.original / user_agent | user_agent.original | |
user_agent.name / user_agent_parsed.name | user_agent.name | Résultat de l'analyseur d'agent utilisateur |
error.message | exception.message (événement de span) | |
error.stack_trace | exception.stacktrace | |
transaction.name | span.name (pour les transactions) | |
transaction.duration.us | Duration (Int64 en nanosecondes) | Changement d'unité important : µs → ns |
labels.* | ResourceAttributes / SpanAttributes / LogAttributes | Type Map |
Exercice 2A : mise en correspondance du modèle de données
Livrable : remplissez exercises/worksheet.md.
Examinez votre environnement Elasticsearch actif et remplissez une section de la fiche pour chaque flux de données (logs-web_access-lab, logs-application-lab, logs-infrastructure-lab). Proposez ensuite une conception de table ClickHouse pour chacun d'eux.
Commandes d'inspection
Exécutez ces commandes depuis n'importe quel hôte capable d'accéder à Elasticsearch à l'adresse http://localhost:9200 (le même hôte que celui utilisé pour la partie 1).
Répertorier les flux de données et leurs index sous-jacents :
curl -s "http://localhost:9200/_data_stream/logs-*" | jq '.data_streams[] | {name, indices: [.indices[].index_name], generation}'Obtenir le mapping d'un flux de données :
curl -s "http://localhost:9200/logs-web_access-lab/_mapping" | jq .Compter les champs feuilles uniques du mapping — exclut les méta-champs ES (_id, _index, _source, _doc_count, etc.), qui augmenteraient sinon le total d'environ 14 :
curl -s "http://localhost:9200/logs-web_access-lab/_field_caps?fields=*" \
| jq '[.fields | to_entries[] | select(.key | startswith("_") | not)] | length'Obtenir les statistiques d'index (nombre de documents et taille) :
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}'Obtenir l'allocation des shards :
curl -s "http://localhost:9200/_cat/shards/logs-web_access-*?v"Inspecter la politique ILM et la phase actuelle :
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}'Inspecter les pipelines d'ingestion :
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"
doneVérifier les statistiques des pipelines (documents traités et échecs) :
curl -s "http://localhost:9200/_nodes/stats/ingest?filter_path=nodes.*.ingest.pipelines" \
| jq '.nodes | to_entries[0].value.ingest.pipelines'Vérifier que l'enrichissement fonctionne :
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}'Compter les valeurs uniques dans les champs à forte cardinalité :
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 .aggregationsInspection : charge de travail 2 (traces et journaux APM)
OTel Demo écrit dans traces-apm-* et dans environ 18 flux de données logs-apm.app.*. La section 4 de la fiche couvre cette charge de travail.
Répertorier les flux de données APM :
curl -s "http://localhost:9200/_data_stream/traces-apm*,logs-apm*" \
| jq '.data_streams[] | {name, generation}'Échantillon des clés de premier niveau d'un document de trace :
curl -s "http://localhost:9200/traces-apm-*/_search?size=1" \
| jq '.hits.hits[0]._source | keys'Cardinalité des champs de trace et répartition par type d'événement :
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 .aggregationsCentiles de durée des transactions (à utiliser pour les valeurs p50/p95 de la section 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 .aggregationsNombre de documents de journaux par service 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"]}'Mesurer une référence de latence des requêtes
Avant de concevoir le schéma cible, mesurez la durée de 3 requêtes courantes dans l'environnement ES actuel. Dans la partie 3, vous exécuterez à nouveau les requêtes ClickHouse équivalentes et comparerez les résultats. Utilisez le champ .took (durée en millisecondes du traitement de la requête par ES, hors réseau et sérialisation).
# 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'
doneNotez la médiane des 3 exécutions de chaque requête dans la section 5 de exercises/worksheet.md.
Conseil : lorsque vous proposez la clé ORDER BY de ClickHouse, demandez-vous quelle combinaison de champs regroupe le mieux les lignes interrogées ensemble. Les tableaux de bord construits dans la partie 1 constituent votre principal guide pour les motifs d'accès.
Exercice 2B : traduction des motifs de requête
Livrable : remplissez exercises/query-translation.md.
Le fichier d'exercice contient 5 requêtes Elasticsearch représentatives, issues de vos tableaux de bord et de l'interface APM de la partie 1. Pour chacune, écrivez le SQL ClickHouse équivalent sur un schéma cible nommé otel_logs (ou otel_traces pour les recherches de traces) comportant les colonnes suivantes :
| Colonne | Type | Correspond au champ ES |
|---|---|---|
Timestamp | DateTime64(9) | @timestamp |
ServiceName | LowCardinality(String) | service |
Body | String | message / ligne de journal brute |
SeverityText | LowCardinality(String) | event.severity |
LogAttributes | Map(LowCardinality(String), String) | tous les autres champs de journal (request_path, status, remote_addr, …) |
TraceId | String | trace.id (table des traces uniquement) |
Pourquoi une Map plutôt que des colonnes individuelles ? Le schéma OTel utilise une
Mappour stocker les attributs de façon flexible et indépendante des fournisseurs. Dans la partie 3, vous extrairez les champs souvent interrogés (commestatus) dans des colonnes matérialisées afin d'améliorer les performances.
Exercice 2C : relevé de décisions d'architecture
Livrable : remplissez exercises/adr-template.md.
Rédigez un bref ADR couvrant sept décisions stratégiques. Il n'existe pas une seule « bonne » réponse : l'objectif est de produire un raisonnement cohérent à la fois avec votre configuration ES actuelle (issue de l'exercice 2A) et avec les contraintes de ClickHouse Cloud (stockage colonnaire, adossé à un stockage objet, sans alertes intégrées).
Les sept décisions :
- Approche de migration — exécution parallèle, bascule directe ou migration progressive par type de journal ?
- Stratégie pour les agents — OTel Collector direct, passerelle Filebeat → Vector → OTel ou Filebeat → Kafka → Vector → OTel ?
- Stratégie de schéma — schéma OTel par défaut, schéma personnalisé avec des colonnes matérialisées ou table source Null + vues matérialisées ? Donnez une réponse pour chacun des 3 types de journaux et pour les traces APM, proposez une clé
ORDER BYpour chacun et décidez du traitement des nouveaux champs apparus après le lancement (Map uniquement, promotion automatique, colonne JSON ou approche hybride). - Transposition des pipelines d'ingestion — où placer chaque processeur ES ? GeoIP (dictionnaire ou processeur du collecteur), analyse de l'agent utilisateur, analyse grok, dérivation de la gravité et horodatage d'ingestion.
- Cycle de vie des données — phases ILM devenues inutiles (et pourquoi), durée de conservation et clause TTL.
- Migration des alertes — choix de l'outil et méthode de recréation des 2 règles d'alerte Kibana (
High Error Rate: plus de 5 % de réponses 5xx sur 5 min ;Service Heartbeat: aucun journal d'un service pendant plus de 3 min). - Stratégie pour les données historiques — réimporter les quelque 62 millions de documents ES dans ClickHouse, repartir de zéro en conservant ES en lecture seule ou adopter une approche hybride ? En cas de réimportation, quel outil utiliser et comment dédupliquer les données ?
Jalon
Avant de passer à la partie 3, vérifiez les points suivants :
- Tous les champs de
exercises/worksheet.mdsont renseignés pour les 3 flux de données - Chaque section de flux de données comporte une proposition de conception de table ClickHouse (clé de partition, ORDER BY, colonnes matérialisées et TTL)
- Vous avez repéré les phases ILM qui deviennent inutiles dans ClickHouse Cloud et savez expliquer pourquoi
-
exercises/query-translation.mdcontient du SQL ClickHouse opérationnel pour les 5 requêtes -
exercises/adr-template.mdcontient un raisonnement pour chacune des 7 décisions (et pas seulement un choix) - Vous avez comparé les réponses de votre fiche et de votre ADR aux corrigés et savez expliquer chaque écart volontaire
Étape suivante : Partie 3 : Exécution de la migration →
01 Construire l'environnement source
Déployez Elasticsearch, Kibana, Filebeat, Elastic APM Server et OpenTelemetry Demo afin d'établir la référence de la migration.
03 Exécuter la migration
Provisionnez ClickHouse Cloud, mettez en place la double écriture avec OpenTelemetry, validez l'équivalence, explorez ClickStack et effectuez la bascule.