Click here to load reader
Upload
ebiznext
View
240
Download
0
Embed Size (px)
Citation preview
Elasticsearch Performance tuning
Contexte L’objet de cette note est un retour d’expérience sur l’optimisation des performances d’un cluster elasticsearch dans le cadre de la mise en œuvre d’une solution de recommandation en ligne qui s’appuie sur MLLib / Spark /elasticsearch. Le diagramme ci-‐dessous illustre le découpage logique :
elasticsearch est une solution de search distribuée et permet donc de répartir les données sur plusieurs serveurs. L’objet de cette note est de décrire les paramétrages réalisés sur chacun des nœuds pour obtenir une performance optimale. Pour les impatients, les optimisations réalisées sont les suivantes : Amélioration des temps d’indexation par :
• Optimisation 1 : Utilisation de l’API bulk • Optimisation 2 : Débrider la bande passante pour les accès disque • Optimisation 3 : Réduire la fréquence des opérations de refresh • Optimisation 4 : Tuning des paramètres de flush • Optimisation 5 : Réduction du nombre de replicas
Amélioration des performances de "search" par :
• Optimisation 6 : Réduction à un seul segment Lucene
Machine Learning(MLLib)
Framework de parallelisation et d'exécution
(Apache Spark)
HDFS(ElasticSearch)
• Optimisation 7 : Utilisation du cache de filtres • Optimisation 8 : Utilisation sélective du cache de données • Optimisation 9 : Dimensionnement adéquat de la JVM et du cache système • Optimisation 10 : Spécialisation des nœuds elasticsearch
D’autres mécanismes d’optimisation sont offerts par Elasticsearch tels que le field data cache pour les agrégations, le routing pour l’orientation des requêtes ou encore l’API warmer qui consiste à précharger les caches et éviter les temps de latence lors des premières sollicitations. Notre objectif n’est pas de tous les énumérer. Les optimisations sont celles que nos avons mis en œuvre dans un contexte BigData mais qui s’appliquent également à d’autres contextes.
Optimisation de l’indexation
Optimisation 1 : Optimisation applicative par insertion de masse Dans le cadre de traitement de masse, les opérations sont la plupart du temps des opérations ensemblistes :
• Insertion de masse : Les données sont injectées en masse dans la base Elasticsearch pour ensuite être recherchées
• Mise à jour / suppression de masse : Suite à un calcul distribué sur Spark , des groupes de données stockés dans Elasticsearch sont potentiellement mises à jour / supprimés.
• Les groupes de données sont extraits d’elasticsearch et accédés au travers de RDD Spark.
Pour éviter l’invocation séquentielle des opérations d’indexation / mise à jour / suppression, nous avons pris le parti d’utiliser plutôt les opérations ensemblistes et de les invoquer au travers de l’API _bulk d’Elasticsearch. Cela permet à Elasticsearch d’indexer plusieurs documents en une seule fois. Le nombre de documents à inclure dans une requête _bulk est fonction du nombre de documents à indexer et de la taille des documents. Pour notre part, chaque opération d’indexation embarquait environ 5000 documents de 1,5Ko soit 7,5Mo de données indexées par Elasticsearch en une seule opération.
Les opérations d’indexation de masse sont également parallélisées sur Spark.
Optimisation 2 : Pas de limite dans l’utilisation de la bande passante pour les accès disque Elasticsearch par défaut limite la bande passante sur les accès disques à 20Mb/s. Dans notre contexte de disques rapides, nous avons également supprimé cette limitation curl -‐XPUT 'localhost:9200/moncluster/_settings' -‐d '{ "transient" : { "indices.store.throttle.type" : "none" } }' Noter la valeur "transient" qui signifie que nous souhaitons ce paramétrage uniquement dans la phase courante. Un arrêt / relance d’Elasticsearch ne conservera pas ces valeurs.
Les étapes de l’indexation dans elasticsearch L’indexation d’un document dans Lucene se déroule en quatre phases (bien que ces phases ne soient pas tout à fait séquentielles, je les présente ainsi pour faciliter l’explication) :
• Phase 1 (Indexation mémoire) : Le document est indexé dans un segment en mémoire. Le document ainsi indexé est encore invisible aux requêtes de recherche.
• Phase 2 (Log sur disque): Enregistrement de la requête d’indexation dans un log de transactions. Comme pour les SGBD classiques, cela permet à Elasticsearch d’être résilient aux pannes et de régénérer les informations d’indexation qui n’ont pas encore été sauvegardées en cas de crash.
• Phase 3 (Refresh) : Périodiquement, les informations d’indexation sont rendues accessibles au search. Cette phase, va provoquer l’invalidation des caches.
Noeud Spark
Noeud Spark
Noeud Spark
Cluster Elasticsearch
7,5Mo / opération
7,5Mo / opération
7,5Mo / opération
• Phase 4 (Flush disque) : Périodiquement, les informations d’indexation sont persistées sur disque. Cette phase va persister sur disque les segments en mémoire et vider le fichier de logs de transactions.
Comme on peut le deviner, les phases 3 et 4 sont celles qui ont un impact sur les performances.
Optimisation 3 : Réduction du temps d’indexation par réduction de la fréquence de refresh L’opération de « refresh » (phase 3 ci-‐dessus) a pour rôle de rendre disponible les informations récemment indexées pour le search. Cette opération couteuse en phase d’indexation peut être désactivée avant le chargement des données et réactivée une fois l’indexation terminée avec les commandes suivantes : Commande 1 : Désactivation du refresh curl -‐XPUT 'localhost:9200/monindex/_settings' -‐d '{ "index" : { "refresh_interval" : -‐1 } }' Commande 2 : Insertion des données Commande 3 : Réactivation du refresh curl -‐XPUT 'localhost:9200/monindex/_settings' -‐d '{ "index" : { // 1 seconde (valeur par défaut dans ES) "refresh_interval" : 1s } }'
Optimisation 4 : Réduction du temps d’indexation par optimisation du flush Dans la phase de flush (phase 4 ci-‐dessus), les segments sont éventuellement regroupés en segments de taille plus importante. La phase 4 se produit dans les conditions suivantes :
• Le segment mémoire est plein • Le délai paramétré de flush a été atteint • Le log de transactions est plein
Au moment du flush, les segments sont potentiellement fusionnés. Cette opération est d’autant plus couteuse que la taille du segment est importante. Enfin un nombre trop important de petits segments va ralentir les opérations de search.
Optimisation 5 : Réduction du temps d’indexation par réduction du nombre de replicas Une opération d’indexation est terminée lorsque le document est ajout à la partition primaire et à tous les replicas. Le nombre de replicas a donc une incidence directe sur le délai d’indexation. Dans notre cas, les données sont sauvegardées par ailleurs et les données indexées dans le cluster sont "reconstructibles". Nous avons donc tout simplement supprimé les replicas en réduisant le nombre de replicas à 0 dans le fichier de configuration elasticsearch.yml.
Optimisation de la recherche Une fois les données indexées, nous avons besoin de les accéder. Dans notre contexte, la base ainsi alimentée devient une base dédiée à a consultation exclusivement. Cela est d’autant plus simple que les optimisations d’indexation sont souvent contradictoires avec les optimisations du search. C’est d ‘ailleurs le cas avec notre choix de ne pas avoir de replicas, qui est bénéfique pour l’indexation mais qui l’est moins pour le search.
Document JSON
Segment mémoire Transition Log disque
Indexation
timeoutou
segment pleinou
transaction log plein
déclencher le flush
Optimisation 6 : réduction à un seul segment Lucene Nous sommes particulièrement intéressés dans notre exemple par la rapidité du search et le nombre de segments a un impact direct sur les performances. Une fois l’indexation terminée et la base ES destinée exclusivement à être interrogée, nous avons réduit le nombre de segments à 1 par la commande suivante : curl -‐XGET 'localhost:9200/monindex/_optimize?max_num_segments=1&wait_for_merge=false’
Optimisation 7 : Choisir soigneusement les types de filtres Deux types de cache existent dans elasticsearch ; Un cache pour les filtres et un cache pour les données de query. Les requêtes de type "filter" sont les requêtes dont le résultat est une valeur booléenne et les requêtes de type "query" sont destinées à l’extraction de documents. Cache de filtres Les requêtes de type filter sont beaucoup plus performantes car Elasticsearch va mémoriser dans un bitset les documents qui vérifient chacun des filtres d’un groupe de filtres afin de pouvoir les réutiliser indépendamment les uns des autres. Les opérations mettant en œuvre le "bool filter" sont à privilégier. Les types de filtres les plus performants sont ceux qui mettent en œuvre les bitset. Il s’agit des types "term" / "exist" / "missing" / "prefix". Les types de filtres "nested" / "has_parent" / "has_child" / "script" ne mettent pas en œuvre les bitset et ne sont pas non mis en cache par défaut non plus.
Optimisation 8 : Mettre en œuvre le cache de manière sélective Par défaut, les résultats des queries sont mis en cache. Un cache qui se remplit trop vite va provoquer le mécanisme consommateur d’éviction LRU de cache d’elasticsearch. Pour éviter un remplissage un peu trop rapide du cache, nous forçons la propriété "_cache" à la valeur "false" pour les requêtes dont le résultat ne sera pas réutilisé d’un appel à l’autre.
Optimisation 9 : Cache de la JVM et cache système Le réflexe le plus courant consiste à dédier le maximum de mémoire vive à la JVM et ne laisser que le strict minimum au système d’exploitation. L’OS s’adapte et réduit du coup la taille du cache disque système, ce qui détériore les performances lors de l’accès aux données. La règle préconisée consiste à octroyer 50% de la mémoire à la JVM et 50% à l’OS. Quant à la taille du tas (heap size) de la JVM, au delà de 32Go, les références sont encodées en 64 bits au lieu d’être encodées en 32 bits, c’est donc autant de mémoire de perdue. Une JVM avec un tas de 48Go ne sera sans doute pas plus performante qu’une JVM paramétrée à 32Go. Nous avons donc observé la règle suivante : Une JVM pour 32Go de mémoire.
Optimisation 10 : Dédier des nœuds à la collecte des données. Le noeud qui reçoit la requête de search, réalise les opérations suivantes :
1. Parsing de la requête HTTP 2. Soumission de la requête à l’ensemble des noeuds de données 3. Collecte de l’ensemble des résultats 4. Agrégation des données reçues 5. Renvoi à l’émetteur
Les étapes 3 et 4 ci-‐dessous sont d’autant plus consommatrices de CPU et de mémoire que le volume de données à renvoyer est important. Nous décidons de soulager les noeuds de données elasticsearch en mettant en frontal des nœuds clients chargés de réaliser les opérations 1, 3, 4 et 5 ci-‐dessus. La mémoire et la CPU des noeuds de données sont ainsi exclusivement dédiés au search.
Les attributs node.data et node.master ont la valeur "false" dans le fichier de configuration elasticsearh.yml pour les nœuds client qui n’hébergent pas de données et sont chargés exclusivement de soumettre les requêtes et de collecter puis agréger les résultats.
Noeud de données
Noeud de données
Noeud de données
Noeud clientHTTP