Technique

Prévention efficace de la duplication des données basées sur les événements dans Elasticsearch

La Suite Elastic est utilisée dans de nombreux cas d'utilisation. L'un des plus courants consiste à stocker et à analyser différents types de données basées sur des événements ou des séries temporelles, tels que les événements de sécurité, les logs et les indicateurs. Ces événements se composent généralement de données liées à un horodatage qui indique quand l'événement a eu lieu ou a été enregistré. Le plus souvent, aucune clé naturelle ne permet d'identifier l'événement de façon unique.

Selon les cas d'utilisation et les types de données, il est important que les données ne soient pas dupliquées dans Elasticsearch : les documents en double sont susceptibles de fausser les analyses et de provoquer des erreurs de recherche. L'année dernière, nous avions publié sur notre blog une introduction au traitement des doublons avec Logstash. Nous allons désormais approfondir le sujet et répondre à quelques questions fréquemment posées.

L'indexation dans Elasticsearch

Lorsque vous indexez des données dans Elasticsearch, seul le renvoi d'une réponse garantit l'indexation correcte des données. Si une anomalie, telle qu'une erreur de connexion ou un incident de nœud, empêche le renvoi d'une réponse, vous n'avez aucun moyen de savoir si les données ont été indexées. Lorsque les clients se trouvent dans cette situation, ils retentent le plus souvent l'opération pour s'assurer que l'indexation a bien eu lieu. Il se peut ainsi qu'un même document ait été indexé plusieurs fois.

Comme le décrit l'article sur le traitement des doublons, nous pouvons contourner ce problème en assignant un identifiant unique à chaque document dans le client plutôt que de demander à Elasticsearch d'en assigner un automatiquement lors de l'indexation. Pour éviter les doublons, l'indexation des documents en double est traitée comme une mise à jour, et les documents ne sont pas inscrits une deuxième fois.

UUID ou identifiants de document basés sur le hachage

Vous pouvez choisir parmi deux types d'identifiants.

Les identifiants universels uniques (UUID) reposent sur des numéros 128 bits qui peuvent être générés sur des systèmes distribués tout en étant uniques dans les faits. Ce type d'identifiant ne dépend généralement pas du contenu de l'événement auquel il est associé.

Si vous souhaitez utiliser les UUID pour éviter la présence de doublons, il est indispensable de les générer et de les assigner aux événements au bon moment pour garantir leur unicité. Dans la pratique, l'UUID doit le plus souvent être assigné au point d'origine. Si le système dont est issu l'événement ne peut pas générer d'UUID, un autre type d'identifiant pourrait être requis.

L'autre principal type d'identifiant utilise une fonction de hachage numérique qui repose sur le contenu de l'événement. La fonction de hachage générera systématiquement la même valeur pour un même fragment de contenu, mais l'unicité de la valeur générée n'est pas garantie. La probabilité qu'une collision de hachage survienne, c'est-à-dire que deux événements différents sont à l'origine d'une même valeur de hachage, dépend du nombre d'événements contenus dans l'index, du type de fonction de hachage utilisé ainsi que de la longueur des valeurs générées. Un hachage d'au moins 128 bits, tel que celui produit par les fonctions MD5 et SHA1, offre généralement un compromis intéressant entre longueur et faible risque de collision pour un grand nombre de scénarios. Vous pouvez utiliser des fonctions de hachage SHA256 pour limiter davantage le risque de collision.

Étant donné que les identifiants basés sur le hachage dépendent du contenu de l'événement, vous pouvez les assigner plus tard, car leur valeur sera la même à tout moment. Vous pouvez assigner ce type d'identifiant n'importe quand avant d'indexer les données dans Elasticsearch et ainsi bénéficier de plus de flexibilité pour concevoir votre pipeline d'ingestion.

Grâce au plug-in de filtre d'empreinte (fingerprint filter plugin), Logstash peut calculer des UUID ainsi qu'un ensemble de fonctions de hachage parmi les plus communes et populaires.

Choix d'un identifiant de document efficace

Quand Elasticsearch est autorisé à assigner l'identifiant de document lors de l'indexation, il peut procéder à des optimisations, car il sait que l'identifiant généré n'existe pas encore dans l'index. Cela a pour effet d'améliorer les performances d'indexation. Quant aux identifiants générés ailleurs et transmis avec le document, Elasticsearch doit vérifier s'il s'agit d'une mise à jour et si l'identifiant des documents existe dans les segments existants de l'index. Cette tâche supplémentaire ralentit l'opération.

Tous les identifiants de document externes ne sont pas créés de la même façon. Les identifiants qui augmentent progressivement en fonction de l'ordre de tri donnent généralement lieu à une indexation plus performante que les identifiants entièrement aléatoires. En effet, Elasticsearch est capable de déterminer rapidement l'existence d'un identifiant dans les segments antérieurs de l'index en prenant en compte uniquement le premier et le dernier identifiant du segment au lieu d'explorer ce dernier. Ce phénomène est décrit dans cet article de blog qui reste pertinent, même s'il ne date pas d'hier.

Les identifiants basés sur le hachage ainsi qu'un grand nombre d'UUID sont généralement de nature aléatoire. Lors du traitement d'événements horodatés, nous pouvons accoler l'horodatage comme préfixe à l'identifiant pour que ce dernier puisse être trié et ainsi améliorer les performances d'indexation.

La création d'un identifiant dont le préfixe est l'horodatage permet aussi de réduire le risque de collision de hachage, la valeur de hachage n'ayant qu'à être unique par horodatage. Ainsi, il devient possible d'utiliser des valeurs de hachage plus courtes, même avec des volumes importants d'ingestion.

Nous pouvons créer ces types d'identifiants dans Logstash en utilisant le plug-in de filtre d'empreinte pour générer un UUID ou un hachage, ainsi qu'un filtre Ruby pour créer une représentation de chaîne hexadécimale de l'horodatage. Supposons que nous disposons d'un champ message que nous pouvons hacher et que l'horodatage de l'événement a déjà été analysé dans le champ @timestamp, nous pouvons créer les composants de l'identifiant et les stocker dans les métadonnées comme suit :

fingerprint {
  source => "message"
  target => "[@metadata][fingerprint]"
  method => "MD5"
  key => "test"
}
ruby {
  code => "event.set('@metadata[tsprefix]', event.get('@timestamp').to_i.to_s(16))"
}

Ces deux champs peuvent alors être utilisés pour générer un identifiant de document dans le plug-in de sortie d'Elasticsearch :

elasticsearch {
  document_id => "%{[@metadata][tsprefix]}%{[@metadata][fingerprint]}"
}

En résultera un identifiant de document hexadécimal comportant 40 caractères, par exemple 4dad050215ca59aa1e3a26a222a9bbcaced23039. Un exemple complet de configuration est proposé dans ce gist.

Conséquences en matière de performances d'indexation

Les effets liés à l'utilisation de différents types d'identifiants dépendront énormément de vos données, du matériel utilisé et de votre cas d'utilisation. Bien que nous puissions émettre quelques recommandations d'ordre général, nous vous conseillons d'effectuer des comparaisons pour évaluer avec exactitude les impacts sur votre cas d'utilisation.

Pour ce qui est du taux d'indexation, l'utilisation d'identifiants générés automatiquement par Elasticsearch reste l'option la plus efficace. Les vérifications de mise à jour n'étant pas requises, le taux d'indexation varie très peu à mesure que la taille des index et des partitions augmente. Cette option est donc recommandée chaque fois qu'elle est applicable.

Les vérifications de mise à jour qui résultent de l'utilisation d'identifiants externes requièrent plus d'accès au disque. L'ampleur des effets occasionnés dépendra de l'efficacité dont bénéficie le système d'exploitation pour mettre en cache les données requises, de la rapidité du stockage et de la capacité de ce dernier à traiter des lectures aléatoires. La vitesse d'indexation diminue souvent à mesure que les index et les partitions grandissent, et que la quantité de segments à vérifier augmente.

Utilisation de l'API de substitution (rollover API)

Les index temporels traditionnels reposent sur des index qui couvrent chacun d'eux une période précise. Cela signifie que la taille des index et des partitions est susceptible de varier considérablement si le volume des données fluctue au fil du temps. La présence de partitions de tailles inégales n'est pas souhaitable, car de telles disparités peuvent causer des problèmes de performance.

L'API de substitution d'index (rollover index API) offre une manière flexible de gérer les index temporels en fonction de plusieurs critères en plus du critère temporel. Elle permet d'utiliser un index de substitution lorsque l'index d'origine atteint une certaine taille, un certain nombre de documents ou un certain âge. La taille des partitions et des index qui en découle est bien plus prévisible.

Toutefois, cela rompt le lien entre l'horodatage des événements et l'index auquel il appartient. Lorsque les index reposaient uniquement sur une base temporelle, un même événement était systématiquement assigné au même index, quel que soit le moment de son insertion. C'est ce principe qui permet d'éviter les doublons en utilisant des identifiants externes. Lorsque vous utilisez l'API de substitution, il n'est par conséquent plus possible d'empêcher complètement les doublons, bien que le risque soit réduit. Il se peut que deux événements en double soient insérés dans des segments différents de l'index de substitution, finissent par se trouver dans des index différents bien qu'ils aient le même horodatage et ne fassent pas l'objet d'une mise à jour.

Ainsi, l'utilisation de l'API de substitution n'est pas recommandée si la duplication de documents doit être rigoureusement évitée.

Adaptation face à l'imprévisibilité du trafic

Si l'API de substitution ne peut pas être utilisée, il est possible d'ajuster la taille des partitions lorsque des fluctuations du trafic produisent des index temporels trop petits ou trop volumineux.

Si les partitions sont trop volumineuses en raison, par exemple, d'un pic de trafic, vous pouvez utiliser l'API de partition d'index (split index API) pour diviser l'index en un plus grand nombre de partitions. Pour que l'API fonctionne, un paramètre doit être appliqué dès la création de l'index. Celui-ci doit donc être ajouté au moyen d'un modèle d'index.

En revanche, si une chute du trafic est à l'origine de partitions inhabituellement petites, l'API de réduction d'index (shrink index API) permet de réduire le nombre de partitions dans l'index.

Conclusions

Cet article de blog montre qu'il est possible d'éviter les doublons dans Elasticsearch en assignant à chaque document un identifiant externe avant d'indexer les données dans Elasticsearch. Le type et la structure de l'identifiant peuvent avoir un impact significatif sur les performances d'indexation. Étant donné que l'impact occasionné varie selon les cas d'utilisation, nous vous recommandons d'effectuer des évaluations comparatives pour optimiser votre choix selon votre scénario.