Toute solution d’observability nécessite un moyen de collecter et d’exporter les logs et les traces. À cette fin, ClickHouse recommande le projet OpenTelemetry (OTel).
“OpenTelemetry est un framework et une boîte à outils d’observability conçus pour créer et gérer des données de télémétrie telles que les traces, les metrics et les logs.”
Contrairement à ClickHouse ou à Prometheus, OpenTelemetry n’est pas un backend d’observability ; il se concentre plutôt sur la génération, la collecte, la gestion et l’export de données de télémétrie. Si l’objectif initial d’OpenTelemetry était de vous permettre d’instrumenter facilement vos applications ou vos systèmes à l’aide de SDKs propres à chaque langage, il s’est depuis étendu à la collecte de logs via l’OpenTelemetry collector, un agent ou proxy qui reçoit, traite et exporte des données de télémétrie.
Composants pertinents pour ClickHouse
OpenTelemetry comprend un certain nombre de composants. En plus de fournir une spécification des données et des API, un protocole normalisé et des conventions de nommage pour les champs/colonnes, OTel offre deux capacités essentielles à la création d’une solution d’observabilité avec ClickHouse :
- L’OpenTelemetry Collector est un proxy qui reçoit, traite et exporte les données de télémétrie. Une solution reposant sur ClickHouse utilise ce composant à la fois pour collecter les logs et traiter les événements avant leur mise en lot et leur insertion.
- Les SDKs de langage qui implémentent la spécification, les API et l’export des données de télémétrie. Ces SDKs garantissent concrètement que les traces sont correctement enregistrées dans le code d’une application, en générant les spans qui les composent et en veillant à ce que le contexte soit propagé entre les services via les métadonnées, ce qui permet de constituer des traces distribuées et de corréler les spans. Ces SDKs sont complétés par un écosystème qui instrumente automatiquement les bibliothèques et frameworks courants, ce qui signifie que l’utilisateur n’a pas besoin de modifier son code et bénéficie d’une instrumentation prête à l’emploi.
Une solution d’observabilité reposant sur ClickHouse tire parti de ces deux outils.
Le collecteur OpenTelemetry existe en plusieurs distributions. Le receiver filelog ainsi que l’exporter ClickHouse, nécessaires pour une solution ClickHouse, ne sont disponibles que dans la distribution OpenTelemetry Collector Contrib.
Cette distribution contient de nombreux composants et permet d’expérimenter diverses configurations. Cependant, pour une exécution en production, il est recommandé de limiter le collecteur aux seuls composants nécessaires à l’environnement. Voici quelques raisons de le faire :
- Réduire la taille du collecteur, et donc le temps de déploiement du collecteur
- Améliorer la sécurité du collecteur en réduisant la surface d’attaque disponible
Il est possible de créer un collecteur personnalisé à l’aide d’OpenTelemetry Collector Builder.
Ingestion de données avec OTel
Rôles de déploiement du collecteur
Pour collecter les logs et les insérer dans ClickHouse, nous recommandons d’utiliser l’OpenTelemetry Collector. L’OpenTelemetry Collector peut être déployé selon deux rôles principaux :
- Agent - Les instances d’agent collectent les données en périphérie, par exemple sur des serveurs ou des nœuds Kubernetes, ou reçoivent directement des événements d’applications instrumentées avec un SDK OpenTelemetry. Dans ce dernier cas, l’instance d’agent s’exécute avec l’application ou sur le même hôte que celle-ci (par exemple en sidecar ou sous forme de DaemonSet). Les agents peuvent envoyer leurs données directement à ClickHouse ou à une instance de passerelle. Dans le premier cas, on parle du modèle de déploiement Agent.
- Gateway - Les instances de passerelle fournissent un service autonome (par exemple, un déploiement dans Kubernetes), généralement par cluster, par centre de données ou par région. Elles reçoivent les événements des applications (ou d’autres collecteurs agissant comme agents) via un point de terminaison OTLP unique. En règle générale, plusieurs instances de passerelle sont déployées, avec un équilibreur de charge prêt à l’emploi pour répartir la charge entre elles. Lorsque tous les agents et toutes les applications envoient leurs signaux vers ce point de terminaison unique, on parle souvent du modèle de déploiement Gateway.
Dans la suite, nous partons du principe qu’un collecteur agent simple envoie directement ses événements à ClickHouse. Voir Scaling with Gateways pour plus de détails sur l’utilisation des passerelles et les cas où elles sont pertinentes.
Le principal avantage de l’utilisation d’un collecteur est qu’il permet à vos services d’évacuer rapidement les données, en laissant au Collector le soin de gérer les traitements supplémentaires tels que les nouvelles tentatives, le regroupement par lots, le chiffrement ou encore le filtrage des données sensibles.
Le Collector utilise les termes receiver, processor et exporter pour ses trois principales étapes de traitement. Les receivers servent à collecter les données et peuvent fonctionner en mode pull ou push. Les processors permettent d’effectuer des transformations et d’enrichir les messages. Les exporters se chargent d’envoyer les données vers un service en aval. Bien que ce service puisse, en théorie, être un autre collecteur, nous partons ici du principe que toutes les données sont envoyées directement à ClickHouse.
Nous recommandons aux utilisateurs de se familiariser avec l’ensemble des receivers, processors et exporters.
Le collecteur fournit deux receivers principaux pour la collecte des logs :
Via OTLP - Dans ce cas, les logs sont envoyés (push) directement au collecteur depuis les OpenTelemetry SDKs via le protocole OTLP. La OpenTelemetry demo utilise cette approche, les exporters OTLP de chaque langage supposant un endpoint local du collecteur. Le collecteur doit alors être configuré avec l’OTLP receiver — voir la démo ci-dessus pour un exemple de configuration. L’avantage de cette approche est que les données de logs contiennent automatiquement des trace IDs, ce qui permet ensuite d’identifier les traces associées à un log donné, et inversement.
Cette approche exige que les utilisateurs instrumentent leur code avec le SDK adapté à leur langage.
- Scraping via le receiver filelog - Ce receiver lit les fichiers sur disque en continu, génère des messages de log, puis les envoie à ClickHouse. Il prend en charge des tâches complexes telles que la détection des messages multilignes, la gestion de la rotation des logs, les checkpoints pour une meilleure robustesse au redémarrage, ainsi que l’extraction de structure. Il peut également suivre les logs des conteneurs Docker et Kubernetes, et être déployé via un chart Helm, en extrayant leur structure et en les enrichissant avec les détails du pod.
La plupart des déploiements utiliseront une combinaison des receivers ci-dessus. Nous recommandons aux utilisateurs de lire la documentation du collecteur et de se familiariser avec les concepts de base, ainsi qu’avec la structure de configuration et les méthodes d’installation.
Astuce : otelbin.iootelbin.io est utile pour valider et visualiser les configurations.
Structuré ou non structuré
Les logs peuvent être structurés ou non structurés.
Un log structuré utilise un format de données tel que JSON, avec des champs de métadonnées comme le code HTTP et l’adresse IP source.
Les logs non structurés, bien qu’ils présentent eux aussi généralement une certaine structure inhérente pouvant être extraite à l’aide d’un motif regex, représentent le log uniquement sous forme de chaîne de caractères.
Nous recommandons aux utilisateurs d’adopter une journalisation structurée et, dans la mesure du possible, d’émettre les logs au format JSON (c.-à-d. ndjson). Cela simplifiera le traitement des logs par la suite, soit avant leur envoi vers ClickHouse avec les processors du collector, soit au moment de l’insert à l’aide de vues matérialisées. À terme, les logs structurés permettent d’économiser des ressources de traitement et de réduire le CPU nécessaire à votre solution ClickHouse.
À titre d’exemple, nous fournissons un jeu de données de logs structuré (JSON) et un jeu de données de logs non structuré, contenant chacun environ 10 M de lignes, disponibles aux liens suivants :
Nous utilisons le jeu de données structuré pour l’exemple ci-dessous. Assurez-vous d’avoir téléchargé et extrait ce fichier pour reproduire les exemples suivants.
Ce qui suit présente une configuration simple pour l’OTel Collector, qui lit ces fichiers sur le disque à l’aide du receiver filelog et écrit les messages obtenus dans stdout. Nous utilisons l’opérateur json_parser, puisque nos logs sont structurés. Modifiez le chemin vers le fichier access-structured.log.
config-structured-logs.yaml
Vous pouvez suivre les instructions officielles pour installer le collector localement. Veillez toutefois à adapter ces instructions afin d’utiliser la distribution contrib (qui contient le receiver filelog) ; par exemple, au lieu de otelcol_0.102.1_darwin_arm64.tar.gz, les utilisateurs devront télécharger otelcol-contrib_0.102.1_darwin_arm64.tar.gz. Les releases sont disponibles ici.
Une fois installé, l’OTel Collector peut être exécuté à l’aide des commandes suivantes :
En utilisant des logs structurés, les messages prendront la forme suivante en sortie :
Le contenu ci-dessus représente un seul message de log tel que produit par l’OTel collector. Nous ingérons ces mêmes messages dans ClickHouse dans les sections suivantes.
Le schéma complet des messages de log, ainsi que les colonnes supplémentaires qui peuvent être présentes si vous utilisez d’autres receivers, est maintenu ici. Nous recommandons vivement aux utilisateurs de se familiariser avec ce schéma.
L’élément essentiel ici est que la ligne de log elle-même est stockée sous forme de chaîne dans le champ Body, tandis que le JSON a été extrait automatiquement dans le champ Attributes grâce à json_parser. Ce même operator a également été utilisé pour extraire le timestamp vers la colonne Timestamp appropriée. Pour des recommandations sur le traitement des logs avec OTel, voir Processing.
OpérateursLes opérateurs constituent l’unité la plus élémentaire du traitement des logs. Chaque opérateur remplit une fonction unique, comme lire des lignes depuis un fichier ou analyser du JSON à partir d’un champ. Les opérateurs sont ensuite chaînés dans un pipeline afin d’obtenir le résultat souhaité.
Les messages ci-dessus n’ont pas de champ TraceID ni SpanID. S’ils étaient présents, par exemple dans les cas où des utilisateurs mettent en œuvre le traçage distribué, ils pourraient être extraits du JSON en utilisant les mêmes techniques que celles présentées ci-dessus.
Pour les utilisateurs qui doivent collecter des fichiers de log locaux ou des fichiers de log Kubernetes, nous recommandons de se familiariser avec les options de configuration disponibles pour le filelog receiver, ainsi qu’avec la façon dont le suivi des offsets et l’analyse multiligne des logs sont gérés.
Collecte des logs Kubernetes
Pour la collecte des logs Kubernetes, nous recommandons le guide de la documentation OpenTelemetry. Le Kubernetes Attributes Processor est recommandé pour enrichir les logs et les métriques avec les métadonnées des pods. Cela peut générer des métadonnées dynamiques, par exemple des labels, qui sont stockées dans la colonne ResourceAttributes. ClickHouse utilise actuellement le type Map(String, String) pour cette colonne. Voir Using Maps et Extracting from maps pour plus de détails sur la gestion et l’optimisation de ce type.
Pour les utilisateurs qui souhaitent instrumenter leur code et collecter des traces, nous recommandons de suivre la documentation officielle d’OTel.
Pour transmettre des événements à ClickHouse, vous devrez déployer un OTel collector afin de recevoir les événements de trace via le protocole OTLP à l’aide du receiver approprié. OpenTelemetry demo fournit un exemple d’instrumentation pour chaque langage pris en charge et d’envoi des événements à un collector. Un exemple de configuration de collector appropriée, qui envoie les événements vers stdout, est présenté ci-dessous :
Comme les traces doivent être reçues via OTLP, nous utilisons l’outil telemetrygen pour générer des données de trace. Suivez les instructions d’installation ici.
La configuration suivante reçoit des événements de trace via un receiver OTLP avant de les envoyer vers stdout.
config-traces.xml
Exécutez cette configuration comme suit :
Envoyez des événements de trace au collector avec telemetrygen :
Cela produira des messages de trace similaires à l’exemple ci-dessous, envoyés vers stdout :
Ce qui précède représente un seul message de trace tel que produit par l’OTel collector. Nous ingérons ces mêmes messages dans ClickHouse dans les sections suivantes.
Le schéma complet des messages de trace est documenté ici. Nous recommandons vivement aux utilisateurs de se familiariser avec ce schéma.
Comme l’a montré l’exemple précédent de définition du timestamp d’un événement de log, vous aurez presque toujours besoin de filtrer, transformer et enrichir les messages d’événement. Pour cela, OpenTelemetry offre plusieurs mécanismes :
-
Processors - Les processors prennent les données collectées par les receivers et les modifient ou les transforment avant de les envoyer aux exporters. Les processors sont appliqués dans l’ordre défini dans la section
processors de la collector configuration. Ils sont facultatifs, mais l’ensemble minimal est généralement recommandé. Lors de l’utilisation d’un OTel collector avec ClickHouse, nous recommandons de limiter les processors à :
- Un memory_limiter sert à éviter les situations de saturation mémoire sur le collector. Consultez Estimating Resources pour les recommandations.
- Tout processor qui effectue un enrichissement basé sur le contexte. Par exemple, le Kubernetes Attributes Processor permet de définir automatiquement les resource attributes des spans, metrics et logs à partir des métadonnées k8s, par ex. en enrichissant les événements avec l’identifiant de leur pod source.
- Le tail sampling ou head sampling, si nécessaire pour les traces.
- Le filtrage de base - suppression des événements inutiles si cela ne peut pas être fait via un opérateur (voir ci-dessous).
- Le batching - indispensable avec ClickHouse pour garantir l’envoi des données par lots. Voir « Exporting to ClickHouse ».
-
Opérateurs - Les opérateurs constituent l’unité de traitement la plus élémentaire disponible au niveau du receiver. Ils prennent en charge le parsing de base, ce qui permet de définir des champs tels que Severity et Timestamp. Le parsing JSON et regex est pris en charge ici, ainsi que le filtrage d’événements et les transformations de base. Nous recommandons d’effectuer le filtrage des événements à ce niveau.
Nous recommandons aux utilisateurs d’éviter un traitement excessif des événements à l’aide d’opérateurs ou de transform processors. Ceux-ci peuvent entraîner une surcharge importante en mémoire et en CPU, en particulier le parsing JSON. Il est possible d’effectuer tout le traitement dans ClickHouse au moment de l’insert à l’aide de materialized views et de colonnes, à quelques exceptions près — notamment l’enrichissement dépendant du contexte, par ex. l’ajout de métadonnées k8s. Pour plus de détails, voir Extracting structure with SQL.
Si le traitement est effectué à l’aide de l’OTel collector, nous recommandons d’exécuter les transformations sur les instances passerelle et de réduire au minimum le travail effectué sur les instances agent. Cela permet de limiter autant que possible les ressources requises par les agents en périphérie, exécutés sur les servers. En pratique, nous constatons que les utilisateurs n’effectuent généralement dans les agents que le filtrage (pour limiter l’utilisation réseau inutile), la définition des timestamps (via des opérateurs) et l’enrichissement lorsqu’il nécessite du contexte. Par exemple, si les instances passerelle se trouvent dans un Kubernetes cluster différent, l’enrichissement k8s devra être effectué dans l’agent.
La configuration suivante montre la collecte d’un fichier de logs non structuré. Notez l’utilisation d’opérateurs pour extraire une structure des lignes de log (regex_parser) et filtrer les événements, ainsi que d’un processeur pour traiter les événements par lot et limiter la consommation mémoire.
config-unstructured-logs-with-processor.yaml
Exportation vers ClickHouse
Les exporters envoient des données vers un ou plusieurs backends ou destinations. Les exporters peuvent être de type pull ou push. Pour envoyer des événements à ClickHouse, vous devez utiliser le ClickHouse exporter, qui fonctionne en mode push.
Un fichier de configuration complet est présenté ci-dessous.
clickhouse-config.yaml
Notez les principaux paramètres suivants :
- pipelines - La configuration ci-dessus met en évidence l’utilisation des pipelines, composés d’un ensemble de receivers, de processeurs et d’exporters, avec un pipeline pour les logs et un pour les traces.
- endpoint - La communication avec ClickHouse est configurée via le paramètre
endpoint. La chaîne de connexion tcp://localhost:9000?dial_timeout=10s&compress=lz4&async_insert=1 fait en sorte que la communication s’effectue sur TCP. Si vous préférez HTTP pour des raisons de basculement de trafic, modifiez cette chaîne de connexion comme décrit ici. Les détails complets de la connexion, y compris la possibilité de spécifier un nom d’utilisateur et un mot de passe dans cette chaîne de connexion, sont décrits ici.
Important : Notez que la chaîne de connexion ci-dessus active à la fois la compression (lz4) et les insertions asynchrones. Nous recommandons que les deux soient toujours activées. Consultez regroupement par lots pour plus de détails sur les insertions asynchrones. La compression doit toujours être spécifiée et ne sera pas activée par défaut sur les anciennes versions de l’exportateur.
- ttl - la valeur indiquée ici détermine la durée de conservation des données. Pour plus de détails, voir “Gestion des données”. Cette valeur doit être spécifiée sous forme d’une unité de temps en heures, par exemple 72h. Nous désactivons le TTL dans l’exemple ci-dessous, car nos données datent de 2019 et seront immédiatement supprimées par ClickHouse si elles sont insérées.
- traces_table_name et logs_table_name - détermine le nom des tables de logs et de traces.
- create_schema - détermine si les tables sont créées avec les schémas par défaut au démarrage. La valeur par défaut est true pour démarrer. Vous devriez le définir sur false et définir votre propre schéma.
- database - base de données cible.
- retry_on_failure - paramètres qui déterminent si les batches en échec doivent être retentés.
- batch - un batch processor garantit que les événements sont envoyés par batches. Nous recommandons une valeur d’au moins 10 000 avec un timeout de 5s (des valeurs allant jusqu’à 100 000 peuvent être utilisées si la mémoire le permet). Le premier de ces seuils atteint déclenchera l’envoi d’un batch à l’exportateur. Réduire ces valeurs permet d’obtenir un pipeline à plus faible latence, avec des données disponibles plus tôt pour les requêtes, au prix d’un plus grand nombre de connections et de batches envoyés à ClickHouse. Cela n’est pas recommandé si vous n’utilisez pas les insertions asynchrones, car cela peut provoquer des problèmes de Too many parts dans ClickHouse. À l’inverse, si vous utilisez les insertions asynchrones, la disponibilité de ces données pour les requêtes dépendra également des paramètres d’insertion asynchrone, même si les données seront tout de même transmises plus tôt par le connecteur. Consultez regroupement par lots pour plus de détails.
- sending_queue - contrôle la taille de la file d’envoi. Chaque élément de la file contient un batch. Si cette file est saturée, par exemple parce que ClickHouse est inaccessible alors que les événements continuent d’arriver, les batches seront abandonnés.
En supposant que les utilisateurs aient extrait le fichier de logs structuré et disposent d’une instance locale de ClickHouse en cours d’exécution (avec l’authentification par défaut), vous pouvez exécuter cette configuration à l’aide de la commande :
Pour envoyer des données de traçage à ce collecteur, exécutez la commande suivante à l’aide de l’outil telemetrygen :
Une fois en fonctionnement, vérifiez la présence d’événements de log à l’aide d’une requête simple :
ClickStack est livré avec un schéma par défaut optimiséClickStack fournit des schémas prêts à l’emploi pour les logs, les traces et les métriques qui intègrent les dernières fonctionnalités de ClickHouse (index textuels pour la recherche en texte intégral et par clé de Map, colonnes matérialisées et arrays ALIAS pour le filtrage en lecture directe, recherche de lignes par numéro de bloc) et ont été benchmarkés pour offrir de solides performances prêtes à l’emploi pour les charges de travail de logging et de traces. Utilisez-les comme point de départ pour votre propre conception.
Par défaut, le ClickHouse exporter crée les tables cibles pour les logs et les traces. Cela peut être désactivé avec le paramètre create_schema. En outre, les noms des tables de logs et de traces peuvent être modifiés par rapport à leurs valeurs par défaut, otel_logs et otel_traces, via les paramètres mentionnés ci-dessus.
Dans les schémas ci-dessous, nous supposons que TTL est configuré à 72 h.
Le schéma par défaut pour les logs est présenté ci-dessous (otelcol-contrib v0.102.1) :
Les colonnes présentées ici correspondent à la spécification officielle d’OTel pour les logs, décrite ici.
Quelques remarques importantes concernant ce schéma :
- Par défaut, la table est partitionnée par date via
PARTITION BY toDate(Timestamp). Cela permet de supprimer efficacement les données expirées.
- Le TTL est défini via
TTL toDateTime(Timestamp) + toIntervalDay(3) et correspond à la valeur définie dans la configuration du collector. ttl_only_drop_parts=1 signifie que seules des parts entières sont supprimées lorsque toutes les lignes qu’elles contiennent ont expiré. C’est plus efficace que de supprimer des lignes à l’intérieur des parts, ce qui implique un delete coûteux. Nous recommandons de toujours définir ce paramètre. Consultez Gestion des données avec TTL pour plus de détails.
- La table utilise le moteur classique
MergeTree. C’est la recommandation pour les logs et les traces, et il ne devrait pas être nécessaire de le modifier.
- La table est ordonnée par
ORDER BY (ServiceName, SeverityText, toUnixTimestamp(Timestamp), TraceId). Cela signifie que les requêtes seront optimisées pour les filtres sur ServiceName, SeverityText, Timestamp et TraceId : les colonnes placées plus tôt dans la liste seront filtrées plus rapidement que les suivantes ; par exemple, un filtrage sur ServiceName sera nettement plus rapide qu’un filtrage sur TraceId. Vous devez modifier cet ordre en fonction des schémas d’accès attendus ; voir Choisir une clé primaire.
- Le schéma ci-dessus applique
ZSTD(1) aux colonnes. Cela offre la meilleure compression pour les logs. Vous pouvez augmenter le niveau de compression ZSTD (au-dessus de la valeur par défaut de 1) pour obtenir une meilleure compression, bien que cela soit rarement avantageux. Augmenter cette valeur entraînera une surcharge CPU plus importante au moment de l’insert (pendant la compression), même si la décompression (et donc les requêtes) devrait rester comparable. Consultez ce lien pour plus de détails. Un encodage delta supplémentaire est appliqué à Timestamp afin de réduire sa taille sur disque.
- Notez que
ResourceAttributes, LogAttributes et ScopeAttributes sont des maps. Il est important de bien comprendre les différences entre ces éléments. Consultez “Utiliser les maps” pour savoir comment accéder à ces maps et optimiser l’accès aux clés qu’elles contiennent.
- La plupart des autres types ici, par exemple
ServiceName en LowCardinality, sont optimisés. Notez que Body, qui est du JSON dans nos logs d’exemple, est stocké sous forme de String.
- Des bloom filters sont appliqués aux clés et valeurs des maps, ainsi qu’à la colonne
Body. Ils visent à améliorer le temps d’exécution des requêtes qui accèdent à ces colonnes, mais ne sont généralement pas nécessaires. Consultez Indices secondaires / data skipping indices.
Là encore, cela s’alignera sur les colonnes correspondant à la spécification officielle d’OTel pour les traces, documentée ici. Le schéma utilise ici bon nombre des mêmes paramètres que le schéma de logs ci-dessus, avec des colonnes Link supplémentaires propres aux spans.
Nous recommandons aux utilisateurs de désactiver la création automatique du schéma et de créer leurs tables manuellement. Cela permet de modifier les clés primaire et secondaire, ainsi que d’ajouter des colonnes supplémentaires pour optimiser les performances des requêtes. Pour plus de détails, consultez Schema design.
Optimisation des insertions
Pour obtenir de bonnes performances d’insertion tout en bénéficiant de fortes garanties de cohérence, respectez quelques règles simples lorsque vous insérez des données d’observabilité dans ClickHouse via le collector. Avec une configuration correcte de l’OTel collector, les règles suivantes sont faciles à appliquer. Cela permet également d’éviter les problèmes courants que rencontrent les utilisateurs qui découvrent ClickHouse.
Par défaut, chaque insertion envoyée à ClickHouse amène ClickHouse à créer immédiatement une part de stockage contenant les données de l’insertion ainsi que les autres métadonnées à stocker. Par conséquent, envoyer moins d’insertions contenant chacune davantage de données, plutôt qu’un plus grand nombre d’insertions contenant chacune moins de données, réduit le nombre d’écritures nécessaires. Nous recommandons d’insérer les données par lots assez volumineux, d’au moins 1 000 lignes à la fois. Plus de détails ici.
Par défaut, les insertions dans ClickHouse sont synchrones et idempotentes lorsqu’elles sont identiques. Pour les tables de la famille de moteurs MergeTree, ClickHouse déduplique automatiquement les insertions par défaut. Cela signifie que les insertions tolèrent des situations comme les suivantes :
- (1) Si le nœud qui reçoit les données rencontre un problème, la requête d’insertion expire (ou renvoie une erreur plus spécifique) et ne reçoit pas d’accusé de réception.
- (2) Si les données ont bien été écrites par le nœud, mais que l’accusé de réception ne peut pas être renvoyé à l’émetteur de la requête en raison d’interruptions réseau, l’émetteur recevra soit un délai d’expiration, soit une erreur réseau.
Du point de vue du collector, il peut être difficile de distinguer (1) de (2). Cependant, dans les deux cas, l’insertion non acquittée peut simplement être retentée immédiatement. Tant que la requête d’insertion retentée contient les mêmes données dans le même ordre, ClickHouse ignorera automatiquement cette nouvelle tentative si l’insertion d’origine (non acquittée) a réussi.
Nous recommandons aux utilisateurs d’utiliser le batch processor présenté dans les configurations précédentes pour répondre à ces exigences. Cela garantit que les insertions sont envoyées sous forme de lots cohérents de lignes respectant les conditions ci-dessus. Si un collector doit traiter un débit élevé (événements par seconde) et qu’au moins 10 000 événements peuvent être envoyés à chaque insertion, c’est généralement le seul regroupement par lots nécessaire dans le pipeline. Des valeurs allant jusqu’à 100 000 peuvent être utilisées si la mémoire le permet. Dans ce cas, le collector flush les lots avant que le timeout du batch processor ne soit atteint, ce qui garantit une faible latence de bout en bout du pipeline ainsi qu’une taille de lot cohérente.
Utiliser les insertions asynchrones
En règle générale, les utilisateurs sont contraints d’envoyer des lots plus petits lorsque le débit d’un collector est faible, tout en s’attendant à ce que les données parviennent à ClickHouse avec une latence de bout en bout minimale. Dans ce cas, de petits lots sont envoyés à l’expiration du timeout du batch processor. Cela peut poser problème, et c’est précisément dans ce type de situation que les insertions asynchrones deviennent nécessaires. Ce cas se présente généralement lorsque des collectors dans le rôle d’agent sont configurés pour envoyer directement vers ClickHouse. Les passerelles, en jouant un rôle d’agrégation, peuvent atténuer ce problème ; voir Scaling with Gateways.
S’il n’est pas possible de garantir de gros lots, vous pouvez déléguer le regroupement par lots à ClickHouse en utilisant les insertions asynchrones. Avec les insertions asynchrones, les données sont d’abord insérées dans un buffer, puis écrites ultérieurement dans le stockage de la base de données, donc de manière asynchrone.
Lorsque les insertions asynchrones sont activées, quand ClickHouse ① reçoit une insert query, les données de la requête sont ② immédiatement écrites dans un buffer en mémoire. Lors du ③ prochain flush du buffer, les données du buffer sont triées et écrites sous forme de part dans le stockage de la base de données. Notez que les données ne peuvent pas être interrogées avant d’avoir été flushées vers le stockage de la base de données ; le flush du buffer est configurable.
Pour activer les insertions asynchrones pour le collector, ajoutez async_insert=1 à la chaîne de connexion. Nous recommandons d’utiliser wait_for_async_insert=1 (valeur par défaut) afin d’obtenir des garanties de livraison ; voir ici pour plus de détails.
Les données d’une async insert sont insérées une fois le buffer ClickHouse flushé. Cela se produit soit lorsque async_insert_max_data_size est dépassé, soit après async_insert_busy_timeout_ms millisecondes à partir de la première INSERT query. Si async_insert_stale_timeout_ms est défini sur une valeur non nulle, les données sont insérées après async_insert_stale_timeout_ms milliseconds depuis la dernière query. Vous pouvez ajuster ces settings pour contrôler la latence de bout en bout de votre pipeline. D’autres settings permettant d’ajuster le flush du buffer sont documentés ici. En règle générale, les valeurs par défaut conviennent.
Envisagez les insertions asynchrones adaptativesDans les cas où un petit nombre d’agents est utilisé, avec un débit faible mais des exigences strictes en matière de latence de bout en bout, les insertions asynchrones adaptatives peuvent être utiles. En règle générale, elles ne s’appliquent pas aux cas d’usage d’observability à haut débit, comme ceux observés avec ClickHouse.
Enfin, le comportement précédent de déduplication associé aux insertions synchrones dans ClickHouse n’est pas activé par défaut lors de l’utilisation d’insertions asynchrones. Si nécessaire, consultez le paramètre async_insert_deduplicate.
Tous les détails sur la configuration de cette fonctionnalité sont disponibles ici, avec une analyse approfondie ici.
Architectures de déploiement
Plusieurs architectures de déploiement sont possibles lors de l’utilisation de l’OTel collector avec ClickHouse. Nous décrivons ci-dessous chacune d’elles et indiquons dans quels cas elle est la plus adaptée.
Dans une architecture composée uniquement d’agents, les utilisateurs déploient l’OTel collector à la périphérie sous forme d’agents. Ceux-ci reçoivent les traces des applications locales (par ex. via un conteneur sidecar) et collectent les logs des serveurs et des nœuds Kubernetes. Dans ce mode, les agents envoient leurs données directement à ClickHouse.
Cette architecture convient aux déploiements de petite à moyenne taille. Son principal avantage est qu’elle ne nécessite pas de matériel supplémentaire et qu’elle réduit au minimum l’empreinte globale en ressources de la solution d’observabilité ClickHouse, avec une correspondance simple entre applications et collecteurs.
Vous devriez envisager de migrer vers une architecture basée sur des passerelles lorsque le nombre d’agents dépasse plusieurs centaines. Cette architecture présente plusieurs inconvénients qui compliquent sa mise à l’échelle :
- Mise à l’échelle des connexions - Chaque agent établit une connexion à ClickHouse. Bien que ClickHouse puisse maintenir des centaines, voire des milliers, de connexions d’insertion concurrentes, cela finit par devenir un facteur limitant et par rendre les insertions moins efficaces : ClickHouse consacre alors davantage de ressources au maintien de ces connexions. L’utilisation de passerelles réduit le nombre de connexions et améliore l’efficacité des insertions.
- Traitement à la périphérie - Dans cette architecture, toute transformation ou tout traitement des événements doit être effectué à la périphérie ou dans ClickHouse. En plus d’être contraignant, cela peut impliquer soit des vues matérialisées ClickHouse complexes, soit le déplacement d’une part importante du calcul vers la périphérie, où les ressources sont limitées et les services critiques peuvent être affectés.
- Petits lots et latences - Les collectors agents peuvent, individuellement, collecter très peu d’événements. Cela signifie généralement qu’ils doivent être configurés pour envoyer leurs données à intervalle défini afin de respecter les SLA de livraison. Le collector peut alors envoyer de petits lots à ClickHouse. Bien que ce soit un inconvénient, il peut être atténué avec les insertions asynchrones - voir Optimiser les insertions.
Mise à l’échelle avec des passerelles
Les OTel collectors peuvent être déployés en tant qu’instances passerelle pour pallier les limitations décrites ci-dessus. Ils fournissent un service autonome, généralement par centre de données ou par région. Ils reçoivent les événements des applications (ou d’autres collectors jouant le rôle d’agent) via un unique point de terminaison OTLP. En général, un ensemble d’instances passerelle est déployé, avec un load balancer prêt à l’emploi pour répartir la charge entre elles.
L’objectif de cette architecture est de décharger les agents des traitements intensifs en calcul, afin de réduire au minimum leur consommation de ressources. Ces passerelles peuvent effectuer des tâches de transformation qui, autrement, devraient être réalisées par les agents. En outre, en agrégeant les événements provenant de nombreux agents, les passerelles peuvent garantir l’envoi de batches volumineux vers ClickHouse, ce qui permet une insertion efficace. Ces passerelle collectors peuvent facilement passer à l’échelle à mesure que davantage d’agents sont ajoutés et que le débit des événements augmente. Un exemple de configuration passerelle, accompagné d’une configuration d’agent associée consommant le fichier journal structuré d’exemple, est présenté ci-dessous. Notez l’utilisation d’OTLP pour la communication entre l’agent et la passerelle.
clickhouse-agent-config.yaml
clickhouse-gateway-config.yaml
Vous pouvez exécuter ces configurations à l’aide des commandes suivantes.
Le principal inconvénient de cette architecture réside dans le coût et la charge opérationnelle liés à la gestion d’un ensemble de collecteurs.
Pour un exemple de gestion d’architectures de plus grande envergure basées sur des passerelles, ainsi que des enseignements qui en sont tirés, nous recommandons cet article de blog.
Les lecteurs remarqueront peut-être que les architectures ci-dessus n’utilisent pas Kafka comme file de messages.
L’utilisation d’une file Kafka comme tampon de messages est un modèle de conception courant dans les architectures de logs, popularisé par la stack ELK. Elle offre plusieurs avantages : principalement, elle permet de meilleures garanties de livraison des messages et aide à gérer le backpressure. Les messages sont envoyés par les agents de collecte vers Kafka, puis écrits sur disque. En théorie, une instance Kafka en cluster devrait fournir un tampon de messages à haut débit, car écrire des données de façon séquentielle sur disque demande moins de surcharge de calcul que d’analyser et de traiter un message — dans Elastic, par exemple, la tokenisation et l’indexation entraînent une surcharge importante. En déplaçant les données hors des agents, vous réduisez aussi le risque de perdre des messages à cause de la rotation des logs à la source. Enfin, cela offre des capacités de rejeu des messages et de réplication inter-régions, ce qui peut être intéressant pour certains cas d’usage.
Cependant, ClickHouse peut insérer des données très rapidement — des millions de lignes par seconde sur un matériel modeste. Le backpressure venant de ClickHouse est rare. Bien souvent, s’appuyer sur une file Kafka ajoute de la complexité architecturale et des coûts. Si vous adhérez au principe selon lequel les logs n’ont pas besoin des mêmes garanties de livraison que des transactions bancaires et d’autres données critiques, nous vous recommandons d’éviter la complexité de Kafka.
Cependant, si vous avez besoin de fortes garanties de livraison ou de la possibilité de rejouer les données (éventuellement vers plusieurs sources), Kafka peut être un ajout architectural utile.
Dans ce cas, les agents OTel peuvent être configurés pour envoyer les données vers Kafka via le Kafka exporter. Les instances passerelle, à leur tour, consomment les messages à l’aide du Kafka receiver. Nous vous recommandons de consulter la documentation de Confluent et d’OTel pour plus de détails.
Estimation des ressources
Les besoins en ressources de l’OTel collector dépendent du débit d’événements, de la taille des messages et du niveau de traitement appliqué. Le projet OpenTelemetry publie des benchmarks que les utilisateurs peuvent consulter pour estimer ces besoins.
D’après notre expérience, une instance gateway disposant de 3 cœurs et de 12 GB de RAM peut traiter environ 60 k événements par seconde. Cela suppose un pipeline de traitement minimal, limité au renommage des champs et n’utilisant aucune expression régulière.
Pour les instances agent chargées d’acheminer les événements vers une gateway et de simplement définir le timestamp de l’événement, nous recommandons de dimensionner les ressources en fonction du volume de logs attendu par seconde. Les valeurs suivantes sont des ordres de grandeur que vous pouvez utiliser comme point de départ :