Technique

Prise en charge de la gestion des versions d'Elasticsearch

L'un des principes fondamentaux sous-jacents d'Elasticsearch consiste à vous permettre de tirer pleinement parti de vos données. Dans le passé, la recherche était une opération en lecture seule au cours de laquelle des données provenant d'une seule source étaient chargées dans un moteur de recherche. Au fil de l'évolution des utilisations et à mesure que le rôle d'Elasticsearch est devenu plus central pour votre application, de nombreux composants doivent actualiser les données. Cette multiplicité d'éléments engendre une simultanéité qui, à son tour, génère des conflits. Pour les résoudre, vous pouvez compter sur le système de gestion des versions d'Elasticsearch.

La nécessité de la gestion des versions - étude d'un exemple

Pour bien comprendre la situation, prenons l'exemple d'un site web sur lequel les internautes notent des modèles de t-shirts. Ce site web est simple. Il répertorie l'ensemble des modèles et permet aux internautes de voter pour ou contre chacun d'entre eux à l'aide d'icônes en forme de pouce levé ou baissé, respectivement. Pour chaque t-shirt, le site web affiche le nombre de votes pour et de votes contre.

Un enregistrement similaire à ce qui suit est créé pour chaque moteur de recherche.

curl -XPOST 'http://localhost:9200/designs/shirt/1' -d'
{
"name": "elasticsearch",
"votes": 999
}'

Comme le montre l'exemple ci-dessus, chaque modèle de t-shirt est doté d'un nom et d'un nombre de votes afin d'en suivre le résultat.

Pour garantir simplicité et scalabilité, le site web est entièrement sans état. Lorsqu'une personne consulte une page et clique sur le bouton en forme de pouce levé, une requête AJAX est envoyée au serveur, qui devrait indiquer à Elasticsearch le besoin d'actualiser le nombre de votes. Ainsi, une implémentation naïve ajoute 1 au nombre total actuel des votes, puis l'envoie à Elasticsearch.

curl -XPOST 'http://localhost:9200/designs/shirt/1' -d'
{
"name": "elasticsearch",
"votes": 1000
}'

Cette approche a un défaut majeur : elle peut engendrer des pertes de votes. Imaginons que deux personnes consultent la même page au même moment. Cette page comptabilise actuellement 999 votes. Les deux personnes cliquent sur le bouton en forme de pouce levé afin de voter en faveur du modèle présenté. Désormais, Elasticsearch obtient deux exemplaires identiques de la requête illustrée ci-dessus afin d'actualiser le document, tâche que la solution exécute. Ainsi, au lieu d'obtenir un nombre total de votes de 1 001, le résultat qui s'affiche est de 1 000. Aïe.

Bien entendu, l' API Update vous permet d'indiquer de manière plus intelligente que le vote peut être augmenté au lieu de configurer une valeur spécifique.

curl -XPOST 'http://localhost:9200/designs/shirt/1/_update' -d'
{
"script" : "ctx._source.votes += 1"
}'

De cette manière, Elasticsearch récupère d'abord le document en interne, l'actualise, puis l'indexe à nouveau. Bien que cette approche soit bien plus susceptible d'être couronnée de succès, elle se confronte toujours au même problème éventuel qu'auparavant. En effet, les choses peuvent se gâter dans le court délai séparant la récupération de la réindexation du document.

Dans le but de gérer le scénario ci-dessus et de prendre en charge des approches plus complexes, Elasticsearch est dotée d'un système intégré de gestion des versions.

Système de gestion des versions d'Elasticsearch

Chaque document stocké dans Elasticsearch est doté d'un numéro de version. Il s'agit d'un nombre positif compris entre 1 et 2 63-1 (inclus). Lorsque vous indexez un document pour la première fois, la version 1 lui est attribuée. Vous pouvez voir cette information dans la réponse envoyée par Elasticsearch. Il s'agit notamment du résultat de la première commande cURL citée dans cet article.

{
"ok": true,
"_index": "designs",
"_type": "shirt",
"_id": "1",
"_version": 1
}

Lors de chaque opération d'écriture dans ce document, qu'il s'agisse d'une indexation, d'une actualisation ou d'une suppression, Elasticsearch ajoutera 1 à cette version. Cet ajout est atomique et s'applique automatiquement si l'opération est réussie.

En outre, Elasticsearch affichera la version actuelle des documents avec la réponse des opérations GET (qui, rappelons-le, s'obtiennent en temps réel). La solution peut aussi recevoir des instructions en vue de communiquer cette information avec chaque résultat de recherche.

Verrouillage optimiste

Si nous revenons à notre exemple, nous avions besoin d'une solution pour le cas où deux internautes essaieraient d'actualiser le même document de manière simultanée. Traditionnellement, un tel problème est résolu à l'aide du verrouillage : avant de mettre à jour un document, une personne le verrouille, l'actualise, puis le déverrouille. Lorsque vous verrouillez un document, vous savez que personne d'autre ne pourra le modifier.

Dans de nombreuses applications, cela signifie que, si une personne modifie un document, personne d'autre ne pourra le lire jusqu'à ce que la modification voulue ait été apportée. Ce type de verrouillage fonctionne, mais est onéreux. Dans le contexte de systèmes à haut débit, cette solution présente deux inconvénients majeurs :

  • Dans bien des cas, cette technique n'est tout simplement pas nécessaire. Si elle est effectuée correctement, les collisions sont rares. Bien entendu, elles peuvent survenir, mais seulement pour une petite partie des opérations réalisées par le système.
  • Le verrouillage suppose que vous vous préoccupez de ce problème. Si vous voulez uniquement afficher une page web, vous vous contentez probablement d'une valeur cohérente, mais légèrement obsolète, même si le système sait qu'une modification va être apportée. Il n'est pas toujours nécessaire d'attendre la fin des opérations d'écriture pour permettre la lecture d'un contenu.

Le système de gestion des versions d'Elasticsearch vous permet également d'utiliser un autre schéma appelé verrouillage optimiste. Au lieu de verrouiller un contenu à chaque fois, vous indiquez à Elasticsearch la version du document que vous vous attendez à trouver. Si le document n'a pas été modifié entre-temps, votre opération se termine avec succès, sans aucun verrouillage. Si le document a été modifié et s'il en existe une nouvelle version, Elasticsearch vous alerte afin que vous preniez les mesures appropriées.

En reprenant l'exemple des votes sur le moteur de recherche ci-dessus, voici comment cela se déroule. Lorsque nous affichons une page sur un modèle de t-shirt, nous notons la version actuelle du document. Elle s'affiche dans la réponse à la requête get que nous exécutons pour la page.

curl -XGET 'http://localhost:9200/designs/shirt/1'
{
"_index": "designs",
"_type": "shirt",
"_id": "1",
"_version": 4,
"exists": true,
"_source": {
"name": "elasticsearch",
"votes": 1002
}
}

Une fois qu'un vote a été réalisé, nous pouvons demander à Elasticsearch d'indexer uniquement la nouvelle valeur (1003) si aucune modification n'a été apportée entre-temps. (Notez le paramètre supplémentaire de chaîne de requête version.)

curl -XPOST 'http://localhost:9200/designs/shirt/1?version=4' -d'
{
"name": "elasticsearch",
"votes": 1003
}'

En interne, Elasticsearch doit seulement comparer le numéro des deux versions. Cette opération est plus simple que le verrouillage-déverrouillage. Si personne n'a modifié le document, l'opération s'exécutera avec succès et le code d'état 200 OK s'affiche. En revanche, si le document a été modifié (ce qui en augmente le numéro de version en interne), l'opération échoue et le code d'état 409 Conflict s'affiche. Notre site web peut désormais réagir de la manière adéquate. Il récupère le nouveau document, augmente le nombre de votes et réessaie en utilisant la nouvelle version. Il est très probable que cette opération réussisse. Dans le cas contraire, nous recommençons simplement la procédure.

Ce schéma est si courant que le point de terminaison update d'Elasticsearch peut le faire pour vous. Vous pouvez configurer le paramètre retry_on_conflict afin de lui demander de réessayer d'exécuter l'opération si un conflit de versions survient. Cette possibilité est particulièrement utile lors d'une mise à jour scriptée. Par exemple, la cURL ci-dessous indiquera à Elasticsearch d'essayer d'actualiser le document jusqu'à 5 fois avant d'échouer.

curl -XPOST 'http://localhost:9200/designs/shirt/1/_update?retry_on_conflict=5' -d'
{
"script" : "ctx._source.votes += 1"
}'

Il convient de noter que la vérification des versions est entièrement facultative. Vous pouvez décider de l'appliquer lors de la mise à jour de certains champs (comme votes) et de l'ignorer lors de l'actualisation d'autres champs (en règle générale, des champs textes à l'instar de name). Tout dépend des exigences de votre application et de vos compromis.

Vous possédez déjà un système de gestion des versions ?

Outre sa prise en charge interne, Elasticsearch s'accorde bien avec les versions de documents gérées par d'autres systèmes. Par exemple, vos données sont peut-être stockées dans une autre base de données qui gère les versions pour vous ou bien vous disposez d'une logique spécifique aux applications qui détermine la méthode de gestion des versions de votre choix. Dans ces situations, vous pouvez toujours utiliser la prise en charge de la gestion des versions d'Elasticsearch et lui indiquer d'utiliser un type de version external. Elasticsearch fonctionnera avec tout système de gestion des versions numériques (dont le numéro est un nombre compris entre 1 et 263-1) à condition que le numéro augmente bien à chaque modification du document.

Pour indiquer à Elasticsearch d'utiliser la gestion des versions externe, ajoutez un paramètre version_type au paramètre version dans chaque requête modifiant les données. Par exemple :

curl -XPOST 'http://localhost:9200/designs/shirt/1?version=526&version_type=external' -d'
{
"name": "elasticsearch",
"votes": 1003
}'

Si les versions sont gérées dans un autre système, Elasticsearch ne connaît pas forcément toutes les modifications apportées, ce qui a de subtiles implications sur la manière dont la gestion des versions est mise en œuvre.

Prenez la commande d'indexation ci-dessus. Avec la gestion des versions internal, cela signifie qu'il faut "uniquement indexer cette actualisation de document si sa version actuelle correspond à 526". Si c'est bien le cas, Elasticsearch ajoutera 1 au numéro de version et stockera le document. Toutefois, avec un système externe de gestion des versions, nous ne pouvons pas appliquer cette exigence. Il se peut que le système n'ajoute pas 1 au numéro de version à chaque fois, qu'il passe à un autre numéro de manière arbitraire (selon une gestion des versions fondée sur le temps) ou qu'il soit difficile de communiquer chaque modification de version à Elasticsearch. Pour toutes ces raisons, la prise en charge de la gestion des versions external se comporte de manière légèrement différente.

Lorsque version_type est configuré sur external, Elasticsearch stockera le numéro de version tel quel et ne l'augmentera pas. En outre, au lieu de vérifier la présence d'une correspondance exacte, Elasticsearch récupérera uniquement une erreur de collision de versions si la version actuellement stockée est supérieure ou égale à celle contenue dans la commande d'indexation. Dans les faits, cela signifie qu'il faut "stocker uniquement ces informations si personne d'autre n'a fourni la même version ou une version plus récente entre-temps". Concrètement, la requête ci-dessus réussira si le numéro de la version stockée est inférieur à 526. S'il est égal ou supérieur, la requête échouera.

Important : Lorsque vous utilisez un système de gestion des versions external, assurez-vous de toujours ajouter la version actuelle (et version_type) à toute demande d'indexation, d'actualisation ou de suppression. Si vous oubliez, Elasticsearch utilisera son système interne afin de traiter cette requête, ce qui entraînera une augmentation erronée du numéro de version.

Considérations finales sur les suppressions

La suppression de données est problématique pour un système de gestion des versions. Une fois les données effacées, le système ne peut pas savoir de manière appropriée si les nouvelles requêtes sont datées ou contiennent de nouvelles informations. Par exemple, imaginons que nous exécutons la commande suivante pour supprimer un enregistrement.

curl -XDELETE 'http://localhost:9200/designs/shirt/1?version=1000&version_type=external'

Cette opération de suppression est la version 1 000 du document concerné. Si nous oublions tout ce que nous savons à ce sujet, une requête similaire à celle ci-dessous qui s'exécutera de manière désynchronisée réalisera la mauvaise opération.

curl -XPOST 'http://localhost:9200/designs/shirt/1/?version=999&version_type=external' -d'
{
"name": "elasticsearch",
"votes": 3001
}'

Si nous oublions que le document a existé, nous devrions simplement accepter cette requête et créer un document. Cependant, la version de l'opération (999) nous indique que ces informations sont obsolètes et que le document devrait bien être supprimé.

La solution peut sembler simple : il ne faut pas vraiment tout supprimer, mais conserver en mémoire les opérations de suppression, les identifiants du document concerné et leur version. Même si cette solution résout bien ce problème, elle est onéreuse. Nous allons rapidement manquer de ressources si les internautes indexent des documents à répétition, puis les suppriment.

La recherche d'Elasticsearch est un bon compromis entre les deux. Elle conserve des enregistrements des suppressions, puis les oublie au bout d'une minute. Cette opération s'appelle la récupération de mémoire des suppressions. Pour les cas d'utilisation les plus pratiques, 60 secondes laissent suffisamment de temps au système afin qu'il rattrape son retard et que les requêtes retardées soient exécutées. Si cette solution ne vous convient pas, vous pouvez la modifier en configurant index.gc_deletes dans votre index selon un autre intervalle de temps.