Optimisation de la JVM pour les performances d'Elasticsearch : conseils sur le tas et le ramasse-miettes

Débloquez des performances de pointe pour votre déploiement Elasticsearch en maîtrisant l'optimisation de la JVM. Ce guide détaille les paramètres critiques pour l'allocation de la mémoire tas (en suivant la règle des 50 % de RAM), l'optimisation du ramasse-miettes avec G1GC et les techniques de surveillance essentielles. Apprenez des configurations pratiques pour éliminer les pics de latence et assurer la stabilité à long terme du cluster pour des charges lourdes de recherche et d'indexation.

Optimisation de la JVM pour les performances d'Elasticsearch : conseils sur le tas et le ramasse-miettes

Elasticsearch fonctionne sur la JVM, donc le tas et le ramasse-miettes sont importants. Mais l'optimisation de la JVM n'est pas par là que je commencerais si un cluster est lent. Vérifiez d'abord le nombre de shards, la forme des requêtes, la pression d'indexation, la latence du disque et si le nœud est simplement sous-dimensionné. Les paramètres JVM sont importants car de mauvaises valeurs peuvent rendre un cluster sain instable. Ce ne sont pas un raccourci pour contourner une mauvaise conception d'index ou un matériel surchargé.

Ce guide se concentre sur l'optimisation de la JVM Elasticsearch qui est encore utile dans les opérations quotidiennes : dimensionnement du tas, symptômes du ramasse-miettes, pression mémoire et les vérifications pratiques qui vous disent si Java est vraiment le problème.


Comprendre les besoins en mémoire d'Elasticsearch

Elasticsearch a besoin de mémoire pour deux domaines principaux : Mémoire Tas et Mémoire Hors Tas. Une optimisation appropriée implique de définir correctement le tas et de s'assurer que le système d'exploitation dispose de suffisamment de mémoire physique restante pour les besoins hors tas.

1. Allocation de la mémoire tas (ES_JAVA_OPTS)

Le tas est l'endroit où résident les objets Elasticsearch, les index, les shards et les caches. C'est le paramètre le plus critique à configurer.

Définition de la taille du tas

Elasticsearch recommande fortement de définir la taille initiale du tas (-Xms) égale à la taille maximale du tas (-Xmx). Cela empêche la JVM de redimensionner dynamiquement le tas, ce qui peut provoquer des pauses de performance notables.

Meilleure pratique : la règle des 50 %

N'allouez jamais plus de 50 % de la RAM physique au tas Elasticsearch. La mémoire restante est cruciale pour le cache du système de fichiers du système d'exploitation (OS). L'OS utilise ce cache pour stocker les données d'index fréquemment consultées (index inversés, champs stockés) à partir du disque, ce qui est nettement plus rapide que la lecture depuis le disque.

Recommandation : Si une machine a 64 Go de RAM, définissez -Xms et -Xmx à 31g ou moins.

Emplacement de la configuration

Ces paramètres sont généralement configurés dans le fichier jvm.options situé dans le répertoire de configuration d'Elasticsearch (par exemple, $ES_HOME/config/jvm.options) ou via des variables d'environnement si vous préférez gérer les paramètres en externe (comme en utilisant ES_JAVA_OPTS).

Exemple de configuration (dans jvm.options) :

# Taille initiale du tas Java (par exemple, 30 Gigaoctets)
-Xms30g

# Taille maximale du tas Java (doit correspondre à -Xms)
-Xmx30g

Avertissement sur la taille du tas : Évitez de définir la taille du tas au-dessus de 31 Go (ou environ 32 Go). En effet, une JVM 64 bits utilise des pointeurs d'objet compressés (Compressed Oops) pour les tas inférieurs à ~32 Go, ce qui permet des dispositions d'objet plus efficaces en mémoire. Dépasser ce seuil annule souvent cet avantage d'efficacité.

2. Mémoire hors tas (mémoire directe)

Elasticsearch utilise également de la mémoire en dehors du tas Java. Lucene s'appuie fortement sur le cache de pages du système d'exploitation, et Elasticsearch peut utiliser de la mémoire directe pour les opérations réseau et natives. Dans la plupart des installations, vous ne devez pas définir -XX:MaxDirectMemorySize sauf si la documentation Elastic ou les conseils de support pour votre version et charge de travail exactes vous le disent. Une limite de mémoire directe manuelle peut créer un nouveau mode de défaillance si elle est trop basse ou basée sur une hypothèse obsolète.

Optimisation du ramasse-miettes (GC)

Le ramasse-miettes est le processus par lequel la JVM récupère la mémoire utilisée par les objets qui ne sont plus référencés. Dans Elasticsearch, un GC mal géré peut provoquer des pics de latence importants, souvent appelés pauses "stop-the-world", qui peuvent entraîner des dépassements de délai et une instabilité des nœuds.

Choisir le bon collecteur

Les versions modernes d'Elasticsearch sont livrées avec les valeurs par défaut de la JVM prises en charge et utilisent généralement G1GC sur les versions Java récentes courantes. Considérez ces valeurs par défaut comme la référence. Ne modifiez les paramètres du collecteur que lorsque les journaux et les métriques montrent un véritable problème de ramasse-miettes.

Paramètres d'optimisation G1GC

Le paramètre principal pour l'optimisation G1GC est la définition de l'objectif de temps de pause maximal. Cela indique au collecteur avec quelle agressivité il doit nettoyer la mémoire.

Exemple de configuration G1GC :

# Exemple uniquement : n'ajoutez pas de flags GC sauf si votre version les prend en charge
# et que vous avez la preuve que le comportement par défaut est le problème.
-XX:MaxGCPauseMillis=200

Surveillance de l'activité GC

Une optimisation efficace nécessite de savoir quand le GC s'exécute et combien de temps cela prend. Elasticsearch vous permet de journaliser les événements GC directement dans un fichier, ce qui est essentiel pour résoudre les problèmes de latence.

Activation de la journalisation GC :

Ajoutez ces flags à votre fichier jvm.options pour activer la journalisation détaillée du GC :

# Activer la journalisation GC
-Xlog:gc*:file=logs/gc.log:time,level,tags

# Facultatif : Spécifier la taille de rotation du journal (par exemple, rotation après 10 Mo)
-Xlog:gc*:file=logs/gc.log:utctime,level,tags:filecount=10,filesize=10m

Analysez le fichier gc.log résultant à l'aide d'outils comme GCEasy ou de scripts spécifiques pour identifier :

  1. Fréquence : À quelle fréquence le GC s'exécute.
  2. Durée : La longueur des pauses (Temps total pour GC dans...).
  3. Taux de promotion : Quelle quantité de données survit assez longtemps pour passer à l'ancienne génération.

Si les pauses GC dépassent systématiquement la cible MaxGCPauseMillis (par exemple, en atteignant fréquemment 500 ms ou plus), cela indique une pression mémoire. Les solutions incluent l'augmentation de la taille du tas (si la RAM le permet, en respectant la règle des 50 %) ou l'optimisation des modèles d'indexation/requête pour réduire le renouvellement d'objets.

Flux de travail pratique d'optimisation et meilleures pratiques

Suivez cette approche systématique pour optimiser les paramètres JVM de votre Elasticsearch :

Étape 1 : Déterminer la capacité du nœud

Identifiez la RAM physique totale disponible sur la machine hébergeant le nœud Elasticsearch.

Étape 2 : Calculer la taille du tas

Calculez la taille maximale du tas : Tas max = RAM physique * 0,5 (arrondi à la baisse à la fraction sûre la plus proche, en laissant généralement 1 à 2 Go de tampon libre). Définissez -Xms et -Xmx à cette valeur.

Étape 3 : Laissez la mémoire directe tranquille sauf si vous avez une raison

Ne copiez pas les flags de mémoire directe à partir d'anciens articles de blog. Vérifiez d'abord la documentation de votre version d'Elasticsearch et les journaux de démarrage actuels.

Étape 4 : Configurer le GC

Assurez-vous que -XX:+UseG1GC est présent et envisagez de définir un objectif raisonnable comme -XX:MaxGCPauseMillis=100.

Étape 5 : Activer et surveiller la journalisation

Activez la journalisation GC et laissez le cluster fonctionner sous une charge de production typique pendant plusieurs heures ou jours. Examinez les journaux.

Étape 6 : Itérer en fonction des journaux

  • Si les pauses sont trop longues : Vous devrez peut-être réduire la charge d'indexation, ou si la RAM le permet, augmenter légèrement la taille du tas et réévaluer la règle des 50 %.
  • Si le GC s'exécute très fréquemment mais que les pauses sont courtes : Votre tas est peut-être légèrement trop petit, ce qui provoque des collections mineures excessives, ou vous créez trop d'objets à courte durée de vie.

Conseil sur le dimensionnement des shards : L'optimisation de la JVM fonctionne mieux lorsqu'elle est combinée à des stratégies d'indexation appropriées. Le sur-partitionnement (trop de petits shards) oblige la JVM à gérer un nombre massif d'objets dans de nombreuses structures, augmentant la surcharge du GC. Visez des shards plus grands (par exemple, 10 Go à 50 Go) pour réduire la surcharge par nœud.

À quoi ressemble la pression du tas dans les clusters réels

La pression du tas s'annonce rarement comme "pression du tas" à la personne de garde. Elle se manifeste par des pics de latence de recherche, des rejets d'indexation, des mises à jour lentes de l'état du cluster, des nœuds qui quittent et rejoignent, ou des tableaux de bord qui semblent corrects jusqu'à ce que le trafic atteigne son maximum. Le signal utile est de savoir si le tas JVM augmente, si le ramasse-miettes s'exécute et si le tas revient à un niveau sain par la suite.

Si le tas augmente pendant une période chargée puis baisse après le ramasse-miettes, le nœud travaille peut-être simplement dur. Si le tas augmente et reste élevé après les collections de l'ancienne génération, vous pourriez avoir une pression soutenue. Si de longues pauses GC coïncident avec des déconnexions de nœuds, des élections de maître ou des dépassements de délai client, le comportement de la JVM fait probablement partie de l'incident.

Utilisez les statistiques des nœuds Elasticsearch pour vérifier le comportement de la JVM :

curl -s "http://localhost:9200/_nodes/stats/jvm,indices,thread_pool?pretty"

Regardez le pourcentage de tas utilisé, le nombre et le temps de ramasse-miettes, la mémoire fielddata, le cache de requêtes, le cache de requêtes, la pression d'indexation et les tâches de pool de threads rejetées. Une seule métrique peut vous induire en erreur. Par exemple, un tas élevé sans tâches rejetées peut être moins urgent qu'un tas modéré avec des rejets de recherche et de longues pauses de l'ancienne génération.

La règle des 50 pour cent a une raison

Le conseil courant de maintenir le tas Elasticsearch à ou en dessous d'environ la moitié de la RAM système n'est pas arbitraire. Lucene lit les fichiers d'index à partir du disque, et le cache de pages du système d'exploitation rend les lectures répétées beaucoup plus rapides. Si vous donnez presque toute la mémoire à la JVM, le tas peut sembler généreux tandis que les performances de recherche se détériorent car l'OS ne peut pas mettre en cache efficacement les segments chauds.

Sur un nœud de 64 Go, un tas d'environ 30 Go ou 31 Go est un plafond courant. Sur un nœud de 16 Go, 8 Go peuvent être un point de départ. Sur un petit nœud de développement, Elasticsearch peut fonctionner avec beaucoup moins. La bonne valeur dépend de la charge de travail, de la version et du rôle du nœud. Les nœuds dédiés éligibles au maître ont généralement besoin de beaucoup moins de tas que les nœuds de données chaudes. Les nœuds de coordination uniquement peuvent avoir besoin d'un tas significatif s'ils répartissent de grandes recherches et fusionnent de grandes réponses.

N'augmentez pas le tas simplement parce que le tas est parfois élevé. Demandez-vous d'abord ce qui l'utilise. Trop de shards, des agrégations coûteuses, des fielddata volumineux, des requêtes en bloc volumineuses, d'énormes fenêtres de résultats de recherche et un état de cluster lourd peuvent tous faire monter le tas. L'augmentation du tas peut retarder le symptôme tandis que la conception sous-jacente continue de s'aggraver.

Pointeurs d'objet compressés et le piège des 32 Go

De nombreux déploiements Java évitent les tas au-dessus d'environ 32 Go car la JVM peut perdre les pointeurs d'objet ordinaires compressés, souvent appelés compressed oops. Lorsque cela se produit, les références d'objet peuvent prendre plus de mémoire, et le tas supplémentaire peut ne pas acheter autant d'espace utilisable que prévu. Le seuil exact peut varier, alors vérifiez les journaux de démarrage plutôt que de traiter 32 Go comme un nombre magique.

Elasticsearch journalise l'ergonomie de la JVM lors du démarrage. Si vous êtes proche du seuil, confirmez si compressed oops est activé. Un tas de 31g est souvent choisi pour rester en dessous de la ligne avec une certaine marge de sécurité. Si un nœud a vraiment besoin de beaucoup plus de mémoire, il peut être préférable d'ajouter des nœuds, de réduire la pression des shards ou de diviser les rôles plutôt que de créer un tas géant avec un comportement GC douloureux.

Les shards, les mappings et les requêtes peuvent créer des problèmes JVM

L'optimisation de la JVM ne peut pas sauver un cluster d'un nombre excessif de shards. Chaque shard a une surcharge : structures de données, métadonnées de segment, caches, coordination de recherche et travail de récupération. Des milliers de minuscules shards peuvent consommer du tas et ralentir les opérations du cluster même lorsque chaque shard contient très peu de données. Si votre problème de tas est apparu après avoir ajouté de nombreux index quotidiens, la solution peut être la gestion du cycle de vie des index et la consolidation des shards, pas un flag GC.

Les mappings comptent aussi. Les champs de texte, les champs de mot-clé, les doc values, les fielddata, les documents imbriqués et les champs d'exécution ont un comportement mémoire différent. L'activation de fielddata sur de grands champs de texte peut être particulièrement coûteuse. Si le tas augmente pendant les agrégations, vérifiez si les utilisateurs agrègent sur des champs qui n'ont pas été conçus pour cela.

Les requêtes peuvent créer des pics d'utilisation de la mémoire. La pagination profonde avec de grandes valeurs from, les requêtes génériques larges, les agrégations à haute cardinalité et les grandes tailles de résultats exercent tous une pression sur les nœuds de coordination et de données. Utilisez search_after, les recherches point-in-time, des filtres plus étroits et des agrégations bien conçues là où elles conviennent. Une requête qui semble inoffensive en développement peut faire très mal lorsqu'elle s'exécute sur des centaines de shards.

Indexation en bloc et tas

L'indexation en bloc est une autre source courante de confusion. Des requêtes en bloc plus grandes peuvent améliorer le débit jusqu'à un certain point, mais les requêtes surdimensionnées consomment de la mémoire, augmentent le temps d'attente et rendent les nouvelles tentatives plus coûteuses. Si vous voyez une pression d'indexation, des rejets de pool de threads d'écriture ou des pics de GC pendant l'ingestion, réduisez la taille des requêtes en bloc ou la concurrence avant de modifier les flags JVM.

Une approche pratique consiste à tester les tailles de bloc avec des documents de type production. Commencez modestement, augmentez jusqu'à ce que le débit cesse de s'améliorer, puis reculez. Surveillez le CPU, le tas, le GC, les E/S disque, l'activité de fusion et les comptes de rejet. Si le nœud passe la plupart de son temps à fusionner des segments ou à attendre le disque, l'optimisation du tas ne résoudra pas le goulot d'étranglement d'ingestion.

L'intervalle d'actualisation affecte également le comportement d'indexation. Pour une ingestion lourde où la recherche en temps quasi réel n'est pas requise, l'augmentation de refresh_interval peut réduire le renouvellement des segments. C'est un paramètre d'index, pas d'optimisation JVM, mais il améliore souvent les symptômes que les gens attribuent à la JVM.

Limites de mémoire des conteneurs

Elasticsearch dans les conteneurs nécessite une attention particulière car la JVM voit les limites du conteneur différemment selon la version de Java et la configuration. Si le conteneur a une limite de mémoire de 4 Go et que vous définissez un tas de 4 Go, le processus peut toujours être tué car la mémoire hors tas, les piles de threads, la mémoire native et le cache du système de fichiers ont également besoin d'espace.

Définissez le tas par rapport à la limite de mémoire du conteneur, pas à la mémoire de l'hôte. Laissez de la place pour la mémoire non tas. Surveillez les événements OOMKilled dans Kubernetes ou les journaux d'exécution du conteneur. Un pod qui disparaît sans une erreur Elasticsearch propre peut avoir été tué par la plateforme plutôt que de planter à l'intérieur de Java.

Pour Kubernetes, les demandes et les limites doivent refléter le profil mémoire réel. Une limite trop proche du tas invite aux OOM kills. Une demande trop faible peut placer le pod sur un nœud où il entre en concurrence intense avec d'autres charges de travail. Elasticsearch bénéficie d'une mémoire et d'E/S disque prévisibles plus que d'un sur-engagement opportuniste.

Quand modifier les paramètres GC

La plupart des opérateurs devraient éviter les expériences de collecteur. Elasticsearch teste et livre avec des paramètres JVM pris en charge pour chaque version. L'ajout aléatoire d'anciens flags CMS, d'objectifs de pause agressifs ou de bundles d'optimisation copiés peut empêcher le démarrage ou aggraver le comportement.

Modifiez les paramètres GC uniquement après avoir pu décrire le problème dans les journaux : les pauses GC anciennes sont trop longues, le jeune GC est trop fréquent, le tas ne récupère pas, ou les événements de pause coïncident avec l'instabilité du cluster. Même dans ce cas, préférez les petits changements et conservez un chemin de retour arrière. Les flags JVM font partie de la configuration de production et doivent passer par la même révision que l'allocation de shards ou les modifications de sécurité.

Si vous modifiez un objectif de pause tel que MaxGCPauseMillis, rappelez-vous qu'il s'agit d'un objectif, pas d'une promesse. La JVM peut ne pas l'atteindre sous une forte pression d'allocation. Si l'application crée trop d'objets trop rapidement, le collecteur ne peut pas transformer cela en performances gratuites.

Une courte liste de contrôle d'incident

Lorsque la latence d'Elasticsearch augmente et que la JVM est suspectée, je vérifierais ceux-ci dans l'ordre :

  1. Un ou deux nœuds sont-ils malsains, ou tout le cluster est-il affecté ?
  2. L'utilisation du tas a-t-elle augmenté en même temps que la latence ?
  3. Des pauses GC de l'ancienne génération se sont-elles produites, et combien de temps ont-elles duré ?
  4. Les pools de threads de recherche ou d'écriture rejettent-ils du travail ?
  5. Le taux d'indexation, la taille des blocs ou le volume de requêtes ont-ils changé ?
  6. Le nombre de shards, le nombre de segments ou la taille de l'état du cluster ont-ils récemment augmenté ?
  7. La latence des E/S disque est-elle élevée ?
  8. Un déploiement, un changement de mapping ou une nouvelle requête de tableau de bord ont-ils commencé à peu près au même moment ?

Cette liste de contrôle maintient l'investigation ancrée. L'optimisation de la JVM est un levier, mais c'est un levier parmi plusieurs.

L'essentiel pratique

Des paramètres JVM appropriés aident Elasticsearch à rester stable, mais la plupart des gains proviennent d'un dimensionnement minutieux du tas, de laisser de la place pour le cache du système de fichiers, de surveiller le comportement réel du GC et de corriger les problèmes de shards ou de requêtes qui créent une pression mémoire en premier lieu. Gardez -Xms et -Xmx égaux, restez conservateur près du seuil des compressed oops, faites confiance aux valeurs par défaut de la version jusqu'à preuve du contraire et traitez les journaux GC comme des preuves opérationnelles plutôt que comme une décoration.