Elasticsearch MigrationClickHouse Workshops

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
  • jq est installé (brew install jq sous macOS, apt-get install jq sous Debian/Ubuntu)

Livrables

Trois livrables, tous dans exercises/ :

N°LivrableFichier
2AFiche 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 3exercises/worksheet.md
2BSQL ClickHouse correspondant à 5 requêtes Elasticsearch représentativesexercises/query-translation.md
2CRelevé de décisions d'architecture portant sur 7 décisions stratégiquesexercises/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 ClickHouseDifférence essentielle
IndexTableES crée de nouveaux index à intervalles réguliers (roulement ILM) ; CH utilise une seule table comportant des partitions
Flux de donnéesUne seule table MergeTreeES 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
DocumentLigneLe schéma des documents ES est flexible ; celui des lignes CH est fixe (avec les types JSON/Map pour préserver de la souplesse)
ChampColonneDes 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 composableSchéma de table (DDL)ES assemble des modèles de composants ; CH utilise une seule instruction CREATE TABLE
ShardShard (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éplicaRéplicaES 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 sautES 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 + dictionnairesES 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 + AggregatingMergeTreeLes 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 KibanaAlertes Grafana / alertes HyperDX / vues matérialisées précalculéesClickHouse 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
KibanaHyperDXCe sont deux interfaces d'observabilité ; HyperDX est nativement compatible avec OTel
Elastic Agent / FilebeatOpenTelemetry CollectorES utilise des agents propriétaires ; ClickStack emploie l'OTel Collector, indépendant des fournisseurs
ECS (Elastic Common Schema)Conventions sémantiques OTelLes 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
@timestampTimestamp (DateTime64(9) de premier niveau)
messageBody (String de premier niveau)
log.level / levelSeverityText + SeverityNumberOTel ajoute un niveau numérique de 1 à 24
event.severitySeverityText
service / service.nameServiceName (LowCardinality(String) de premier niveau)
service.versionServiceVersion / ResourceAttributes['service.version']
host.hostname / hostnamehost.name (ResourceAttributes['host.name'])ECS accepte l'alias host.name ; OTel utilise uniquement host.name
host.iphost.ip (attribut de ressource)
trace.idTraceId (String de premier niveau)
span.id / transaction.idSpanId
parent.idParentSpanId
http.request.methodhttp.request.methodIdentique dans les deux : OTel a adopté le nom ECS
http.response.status_codehttp.response.status_codeIdentique
client.ip / source.ip / remote_addrclient.addressStocké sous forme de chaîne
user_agent.original / user_agentuser_agent.original
user_agent.name / user_agent_parsed.nameuser_agent.nameRésultat de l'analyseur d'agent utilisateur
error.messageexception.message (événement de span)
error.stack_traceexception.stacktrace
transaction.namespan.name (pour les transactions)
transaction.duration.usDuration (Int64 en nanosecondes)Changement d'unité important : µs → ns
labels.*ResourceAttributes / SpanAttributes / LogAttributesType 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"
done

Vé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 .aggregations

Inspection : 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 .aggregations

Centiles 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 .aggregations

Nombre 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'
done

Notez 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 :

ColonneTypeCorrespond au champ ES
TimestampDateTime64(9)@timestamp
ServiceNameLowCardinality(String)service
BodyStringmessage / ligne de journal brute
SeverityTextLowCardinality(String)event.severity
LogAttributesMap(LowCardinality(String), String)tous les autres champs de journal (request_path, status, remote_addr, …)
TraceIdStringtrace.id (table des traces uniquement)

Pourquoi une Map plutôt que des colonnes individuelles ? Le schéma OTel utilise une Map pour stocker les attributs de façon flexible et indépendante des fournisseurs. Dans la partie 3, vous extrairez les champs souvent interrogés (comme status) 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 :

  1. Approche de migration — exécution parallèle, bascule directe ou migration progressive par type de journal ?
  2. Stratégie pour les agents — OTel Collector direct, passerelle Filebeat → Vector → OTel ou Filebeat → Kafka → Vector → OTel ?
  3. 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 BY pour chacun et décidez du traitement des nouveaux champs apparus après le lancement (Map uniquement, promotion automatique, colonne JSON ou approche hybride).
  4. 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.
  5. Cycle de vie des données — phases ILM devenues inutiles (et pourquoi), durée de conservation et clause TTL.
  6. 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).
  7. 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.md sont 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.md contient du SQL ClickHouse opérationnel pour les 5 requêtes
  • exercises/adr-template.md contient 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 →

Sur cette page

Suivre votre progression ?

Facultatif. Nous envoyons un lien par e-mail pour confirmer votre adresse ; la progression est enregistrée après son ouverture.

Utilisez votre adresse e-mail professionnelle, et non une adresse personnelle.

Le suivi de la progression exige aussi d’accepter les Conditions d’utilisation actuelles dans les Paramètres de confidentialité.

FR