Optimisation des requêtes Elasticsearch lentes : meilleures pratiques pour le réglage des performances

Débloquez des performances maximales pour vos requêtes Elasticsearch. Ce guide fournit des stratégies concrètes pour lutter contre les performances de recherche lentes, couvrant des techniques essentielles allant de l'optimisation de la structure des requêtes et de l'utilisation de mécanismes de mise en cache puissants (nœud, shard et système de fichiers) à l'identification précise des goulots d'étranglement à l'aide de l'API Profile. Apprenez à créer des requêtes efficaces, à utiliser le filtrage `_source`, à implémenter la pagination `search_after` et à interpréter les résultats du profil pour diagnostiquer et résoudre les problèmes de performance dans les environnements de production. Élevez votre expertise Elasticsearch et assurez une expérience utilisateur ultra-rapide.

41 vues

Optimisation des requêtes Elasticsearch lentes : meilleures pratiques pour le réglage des performances

Elasticsearch est un moteur de recherche et d'analyse distribué et puissant, capable de gérer de vastes quantités de données. Cependant, même avec son architecture robuste, des requêtes inefficaces peuvent entraîner des performances lentes, ce qui nuit à l'expérience utilisateur et à la réactivité de l'application. L'identification et la résolution de ces goulots d'étranglement sont essentielles pour maintenir un cluster Elasticsearch sain et très performant.

Cet article explore en profondeur des stratégies pratiques pour améliorer les performances de recherche lentes. Nous verrons comment optimiser la structure de vos requêtes, exploiter efficacement divers mécanismes de mise en cache, et utiliser l'outil intégré Profile API d'Elasticsearch pour identifier la source exacte des problèmes de performance. En appliquant ces meilleures pratiques, vous pouvez réduire considérablement la latence des requêtes et garantir que votre cluster Elasticsearch fonctionne à son efficacité maximale.

Comprendre les goulots d'étranglement des performances des requêtes

Avant de plonger dans les solutions, il est utile de comprendre les raisons courantes des requêtes Elasticsearch lentes. Celles-ci incluent souvent :

  • Requêtes complexes : Requêtes comportant plusieurs clauses bool, des requêtes imbriquées, ou des opérations coûteuses comme wildcard ou regexp sur de grands ensembles de données.
  • Récupération de données inefficace : Récupération inutile de _source, ou récupération d'un grand nombre de documents pour la pagination.
  • Contraintes de ressources : Insuffisance de CPU, de mémoire ou d'E/S disque sur les nœuds de données.
  • Mappings sous-optimaux : Utilisation de types de données incorrects ou non-exploitation des doc_values pour les agrégations.
  • Déséquilibre ou surcharge des Shards : Trop de shards, pas assez de shards, ou distribution inégale des shards/données.
  • Absence de mise en cache : Non-utilisation des mécanismes de mise en cache intégrés d'Elasticsearch ou des caches externes au niveau de l'application.

Optimisation de la structure des requêtes

La manière dont vous construisez vos requêtes a un impact profond sur leurs performances. De petits changements peuvent entraîner des améliorations significatives.

1. Récupérer uniquement les champs nécessaires (Filtrage _source et stored_fields)

Par défaut, Elasticsearch renvoie l'intégralité du champ _source pour chaque document correspondant. Si votre application n'a besoin que de quelques champs, récupérer l'intégralité de _source est un gaspillage en termes de bande passante réseau et de temps d'analyse (parsing).

  • Filtrage _source : Utilisez le paramètre _source pour spécifier un tableau de champs à inclure ou à exclure.

    json GET /my-index/_search { "_source": ["title", "author", "publish_date"], "query": { "match": { "content": "Elasticsearch performance" } } }

  • stored_fields : Si vous avez stocké explicitement des champs spécifiques dans votre mapping (par exemple, "store": true), vous pouvez les récupérer directement en utilisant stored_fields. Cela contourne l'analyse de _source et peut être plus rapide si _source est volumineux.

    json GET /my-index/_search { "stored_fields": ["title", "author"], "query": { "match": { "content": "Elasticsearch performance" } } }

2. Préférer les types de requêtes efficaces

Certains types de requêtes consomment intrinsèquement plus de ressources que d'autres.

  • Éviter les jokers et les expressions régulières en tête (Leading Wildcards and Regexps) : Les requêtes wildcard, regexp et prefix sont coûteuses en calcul, surtout lorsqu'elles sont utilisées avec un joker en tête (par exemple, *test). Elles doivent scanner l'intégralité du dictionnaire de termes pour trouver les termes correspondants. Si possible, repensez votre application pour les éviter ou utilisez les completion suggesters pour la correspondance de préfixes.

    ```json

    Inefficace - éviter le joker en tête

    {
    "query": {
    "wildcard": {
    "name.keyword": {
    "value": "*search"
    }
    }
    }
    }

    Meilleur - si vous connaissez le préfixe

    {
    "query": {
    "prefix": {
    "name.keyword": {
    "value": "Elastic"
    }
    }
    }
    }
    ```

  • Utiliser match_phrase au lieu de clauses match multiples pour les phrases : Pour la correspondance de phrase exacte, match_phrase est plus efficace que de combiner plusieurs requêtes match au sein d'une requête bool.

  • constant_score pour le filtrage : Lorsque vous vous souciez uniquement de savoir si un document correspond à un filtre et non de la qualité de son score, enveloppez votre requête dans une requête constant_score. Cela contourne les calculs de scoring, ce qui peut économiser des cycles CPU.

    json GET /my-index/_search { "query": { "constant_score": { "filter": { "term": { "status": "active" } } } } }

3. Optimiser les requêtes booléennes

  • Ordre des clauses : Placez les clauses les plus restrictives (celles qui filtrent le plus de documents) au début de votre requête bool. Elasticsearch traite les requêtes de gauche à droite, et l'élagage précoce peut réduire considérablement le nombre de documents traités par les clauses suivantes.
  • minimum_should_match : Utilisez minimum_should_match dans les requêtes bool pour spécifier le nombre minimum de clauses should qui doivent correspondre. Cela peut aider à élaguer les résultats rapidement.

4. Pagination efficace (search_after et scroll)

La pagination traditionnelle from/size devient très inefficace pour les pages profondes (par exemple, from: 10000, size: 10). Elasticsearch doit récupérer et trier tous les documents jusqu'à from + size sur chaque shard, puis ignorer from documents.

  • search_after : Pour la pagination profonde en temps réel, search_after est recommandé. Il utilise l'ordre de tri du dernier document de la page précédente pour trouver l'ensemble de résultats suivant, similaire aux curseurs dans les bases de données traditionnelles. Il est sans état (stateless) et s'adapte mieux à l'échelle.

    ```json

    Première requête

    GET /my-index/_search
    {
    "size": 10,
    "query": {"match_all": {}},
    "sort": [{"timestamp": "asc"}, {"_id": "asc"}]
    }

    Requête suivante utilisant les valeurs de tri du dernier document de la première requête

    GET /my-index/_search
    {
    "size": 10,
    "query": {"match_all": {}},
    "search_after": [1678886400000, "doc_id_XYZ"],
    "sort": [{"timestamp": "asc"}, {"_id": "asc"}]
    }
    ```

  • scroll API : Pour la récupération en masse de grands ensembles de données (par exemple, pour la réindexation ou la migration de données), l'API scroll est idéale. Elle prend un instantané de l'index et renvoie un ID de défilement (scroll ID), qui est ensuite utilisé pour récupérer les lots suivants. Elle ne convient pas à la pagination en temps réel destinée aux utilisateurs.

5. Optimisation des agrégations

Les agrégations peuvent être gourmandes en ressources, en particulier sur les champs à forte cardinalité.

  • Pré-calcul des agrégations : Envisagez d'exécuter des agrégations complexes non en temps réel pendant l'indexation ou selon un calendrier pour pré-calculer les résultats et les stocker dans un index séparé.
  • doc_values : Assurez-vous que les champs utilisés dans les agrégations ont doc_values activés (ce qui est la valeur par défaut pour la plupart des champs non text). Cela permet à Elasticsearch de charger efficacement les données pour les agrégations sans charger _source.
  • eager_global_ordinals : Pour les champs keyword fréquemment utilisés dans les agrégations terms, définir eager_global_ordinals: true dans le mapping peut améliorer les performances en pré-construisant les ordinaux globaux. Cela engendre un coût lors de l'actualisation de l'index (index refresh time) mais accélère les agrégations au moment de la requête.

Exploiter les techniques de mise en cache

Elasticsearch offre plusieurs couches de mise en cache qui peuvent accélérer considérablement les requêtes répétées.

1. Cache de requêtes de nœud (Node Query Cache)

  • Mécanisme : Met en cache les résultats des clauses de filtre au sein des requêtes bool qui sont utilisées fréquemment. Il s'agit d'un cache en mémoire au niveau du nœud.
  • Efficacité : Plus efficace pour les filtres qui sont constants sur de nombreuses requêtes et qui correspondent à un nombre relativement faible de documents (moins de 10 000 documents).
  • Configuration : Activé par défaut. Vous pouvez contrôler sa taille avec indices.queries.cache.size (par défaut 10 % du tas JVM).

2. Cache de requêtes de shard (Shard Request Cache)

  • Mécanisme : Met en cache la réponse entière d'une requête de recherche (y compris les hits, les agrégations, et les suggestions) par shard. Il ne fonctionne que pour les requêtes où size=0 et pour les requêtes qui utilisent uniquement des clauses de filtre (sans scoring).
  • Efficacité : Excellent pour les requêtes de tableau de bord ou les applications analytiques où la même requête (y compris les agrégations) est exécutée à plusieurs reprises avec des paramètres identiques.
  • Comment l'utiliser : Activez-le explicitement dans votre requête en utilisant "request_cache": true.

    json GET /my-index/_search?request_cache=true { "size": 0, "query": { "bool": { "filter": [ {"term": {"status.keyword": "active"}}, {"range": {"timestamp": {"gte": "now-1h"}}} ] } }, "aggs": { "messages_per_minute": { "date_histogram": { "field": "timestamp", "fixed_interval": "1m" } } } }

  • Mises en garde : Le cache est invalidé chaque fois qu'un shard est actualisé (de nouveaux documents sont indexés ou ceux existants sont mis à jour). N'est utile que pour les requêtes qui renvoient fréquemment des résultats identiques.

3. Cache du système de fichiers (Niveau OS)

  • Mécanisme : Le cache du système de fichiers du système d'exploitation joue un rôle essentiel. Elasticsearch s'appuie fortement dessus pour mettre en cache les segments d'index fréquemment consultés.
  • Efficacité : Crucial pour les performances des requêtes. Si les segments d'index sont dans la RAM, les E/S disque sont entièrement contournées, ce qui conduit à une exécution des requêtes beaucoup plus rapide.
  • Meilleure pratique : Allouez au moins la moitié de la RAM de votre serveur au cache du système de fichiers, et l'autre moitié au tas JVM (heap) d'Elasticsearch. Par exemple, si vous disposez de 64 Go de RAM, allouez 32 Go au tas Elasticsearch et laissez 32 Go pour le cache du système de fichiers de l'OS.

4. Mise en cache au niveau de l'application

  • Mécanisme : Implémenter un cache au niveau de votre couche application (par exemple, en utilisant Redis, Memcached ou un cache en mémoire) pour les résultats de recherche fréquemment demandés.
  • Efficacité : Peut fournir les temps de réponse les plus rapides en contournant complètement Elasticsearch pour les requêtes répétées. Idéal pour les résultats de recherche statiques ou qui changent lentement.
  • Considérations : La stratégie d'invalidation du cache est essentielle. Nécessite une conception minutieuse pour garantir la cohérence des données.

Utilisation de l'API Profile pour l'identification des goulots d'étranglement

L'Profile API est un outil inestimable pour comprendre exactement comment Elasticsearch exécute une requête et où le temps est passé. Il décompose le temps d'exécution pour chaque composant de votre requête et agrégation.

Comment utiliser l'API Profile

Ajoutez simplement "profile": true au corps de votre requête de recherche.

GET /my-index/_search
{
  "profile": true,
  "query": {
    "bool": {
      "must": [
        {"match": {"title": "Elasticsearch"}},
        {"term": {"status.keyword": "published"}}
      ],
      "filter": [
        {"range": {"publish_date": {"gte": "2023-01-01"}}}
      ]
    }
  },
  "aggs": {
    "top_authors": {
      "terms": {
        "field": "author.keyword",
        "size": 10
      }
    }
  }
}

Interprétation des résultats de l'API Profile

La réponse inclura une section profile détaillant l'exécution de la requête et de l'agrégation sur chaque shard. Les métriques clés à surveiller comprennent :

  • description : Le composant spécifique de la requête ou de l'agrégation.
  • time_in_nanos : Le temps passé à exécuter ce composant.
  • breakdown : Des sous-métriques détaillées telles que build_scorer_time, collect_time, set_weight_time pour les requêtes, et reduce_time pour les agrégations.
  • children : Composants imbriqués, montrant comment le temps est distribué au sein des requêtes complexes.

Exemple d'interprétation :

Si vous voyez un time_in_nanos élevé pour une WildcardQuery, cela confirme qu'il s'agit d'une partie coûteuse de votre requête. Si collect_time est élevé, cela suggère que la récupération et le traitement des documents après une correspondance est un goulot d'étranglement, potentiellement dû à l'analyse de _source ou à la pagination profonde. Un reduce_time élevé dans les agrégations pourrait indiquer une charge importante lors de la phase de fusion finale (final merge phase).

En examinant ces métriques, vous pouvez identifier les clauses de requête ou les champs d'agrégation spécifiques qui consomment le plus de ressources, puis appliquer les techniques d'optimisation abordées précédemment.

Meilleures pratiques générales pour les performances

Au-delà des optimisations spécifiques aux requêtes, plusieurs meilleures pratiques au niveau du cluster et de l'index contribuent à la performance globale de la recherche.

1. Mappings d'index optimaux

  • text vs. keyword : Utilisez text pour la recherche en texte intégral et keyword pour la correspondance de valeur exacte, le tri et les agrégations. Des types mal assortis peuvent entraîner des requêtes inefficaces.
  • doc_values : Assurez-vous que doc_values est activé pour les champs sur lesquels vous avez l'intention de trier ou d'agréger. Il est activé par défaut pour les types keyword et numériques, mais le désactiver explicitement pour un champ text pourrait économiser de l'espace disque au détriment des performances d'agrégation si vous devez ultérieurement agréger sur celui-ci.
  • norms : Désactivez norms ("norms": false) pour les champs où vous n'avez pas besoin de normalisation de la longueur du document (par exemple, les champs d'ID). Cela économise de l'espace disque et améliore la vitesse d'indexation, avec un impact minimal sur les performances des requêtes pour les requêtes sans scoring.
  • index_options : Pour les champs text, utilisez index_options: docs si vous avez seulement besoin de savoir si un terme existe dans un document, et index_options: positions (la valeur par défaut) si vous avez besoin de requêtes de phrases et de recherches de proximité.

2. Surveiller la santé et les ressources du cluster

  • Statut du cluster Vert (Green Cluster Status) : Assurez-vous que votre cluster est toujours vert. Le statut jaune ou rouge indique des shards non alloués ou manquants, ce qui peut gravement affecter la fiabilité et les performances des requêtes.
  • Surveillance des ressources : Surveillez régulièrement le CPU, la RAM, les E/S disque et l'utilisation du réseau sur vos nœuds de données. Les pics dans ces métriques sont souvent corrélés avec des requêtes lentes.
  • Tas JVM (JVM Heap) : Gardez un œil sur l'utilisation du tas JVM. Une utilisation élevée peut entraîner de fréquentes pauses dues au garbage collection, rendant les requêtes lentes. Optimisez les requêtes pour réduire la pression sur le tas.

3. Allocation appropriée des Shards

  • Trop de Shards : Chaque shard consomme des ressources (CPU, RAM, descripteurs de fichiers). Avoir trop de petits shards sur un nœud peut entraîner une surcharge. Visez des shards de taille raisonnable (par exemple, 10 Go à 50 Go pour la plupart des cas d'utilisation).
  • Pas assez de Shards : Limite le parallélisme. Les requêtes sur un index avec trop peu de shards ne pourront pas exploiter efficacement tous les nœuds de données disponibles.

4. Stratégie d'indexation

  • Intervalle d'actualisation (Refresh Interval) : Un refresh_interval plus bas (par défaut 1 seconde) rend les données visibles plus rapidement mais augmente la charge d'indexation. Pour les charges de travail très axées sur la recherche, envisagez de l'augmenter légèrement (par exemple, 5 à 10 secondes) pour réduire la pression d'actualisation.

Conclusion

L'optimisation des requêtes Elasticsearch lentes est un processus continu qui implique de comprendre vos données, vos modèles d'accès et le fonctionnement interne d'Elasticsearch. En appliquant une construction de requêtes réfléchie, en utilisant efficacement les mécanismes de mise en cache d'Elasticsearch et en tirant parti d'outils de diagnostic puissants comme l'Profile API, vous pouvez améliorer considérablement les performances et la réactivité de vos applications de recherche.

Une surveillance régulière, associée à une analyse approfondie des requêtes lentes spécifiques à l'aide de l'API Profile, vous permettra d'affiner continuellement votre configuration Elasticsearch, garantissant une expérience de recherche rapide et efficace pour vos utilisateurs. N'oubliez pas qu'un index bien structuré et un cluster sain sont les fondations sur lesquelles toutes les optimisations de requêtes sont construites.