Ce billet invité de l'architecte de performance Intel Java Eric Kaczmarek (publié à l'origine ici) explore comment régler la récupération de place Java (GC) pour Apache HBase en se concentrant sur 100 % des lectures YCSB.
Apache HBase est un projet open source Apache offrant un stockage de données NoSQL. Souvent utilisé avec HDFS, HBase est largement utilisé dans le monde. Les utilisateurs bien connus incluent Facebook, Twitter, Yahoo, etc. Du point de vue du développeur, HBase est une "base de données distribuée, versionnée et non relationnelle inspirée de Bigtable de Google, un système de stockage distribué pour les données structurées". HBase peut facilement gérer un débit très élevé en augmentant (c'est-à-dire en déployant sur un serveur plus grand) ou en augmentant (c'est-à-dire en déployant sur plus de serveurs).
Du point de vue de l'utilisateur, la latence pour chaque requête est très importante. Alors que nous travaillons avec les utilisateurs pour tester, ajuster et optimiser les charges de travail HBase, nous rencontrons maintenant un nombre important qui veulent vraiment des latences de fonctionnement de 99e centile. Cela signifie un aller-retour, de la demande du client à la réponse du client, le tout en 100 millisecondes.
Plusieurs facteurs contribuent à la variation de la latence. L'un des intrus de latence les plus dévastateurs et les plus imprévisibles est les pauses "stop the world" de la machine virtuelle Java (JVM) pour la collecte des ordures (nettoyage de la mémoire).
Pour résoudre ce problème, nous avons essayé quelques expériences en utilisant le collecteur Oracle jdk7u21 et jdk7u60 G1 (Garbage 1st). Le système de serveur que nous avons utilisé était basé sur des processeurs Intel Xeon Ivy-bridge EP avec Hyper-threading (40 processeurs logiques). Il avait 256 Go de RAM DDR3-1600 et trois SSD de 400 Go comme stockage local. Cette petite configuration contenait un maître et un esclave, configurés sur un seul nœud avec une charge adaptée de manière appropriée. Nous avons utilisé HBase version 0.98.1 et le système de fichiers local pour le stockage HFile. La table de test HBase a été configurée avec 400 millions de lignes et sa taille était de 580 Go. Nous avons utilisé la stratégie de tas HBase par défaut :40 % pour le blockcache, 40 % pour le memstore. YCSB a été utilisé pour piloter 600 threads de travail envoyant des requêtes au serveur HBase.
Les graphiques suivants montrent que jdk7u21 exécute une lecture à 100 % pendant une heure en utilisant -XX:+UseG1GC -Xms100g -Xmx100g -XX:MaxGCPauseMillis=100
. Nous avons spécifié le ramasse-miettes à utiliser, la taille du segment de mémoire et le temps de pause "arrêter le monde" du ramasse-miettes (GC) souhaité.
Figure 1 :sautes sauvages dans le temps de pause du GC
Dans ce cas, nous avons eu des pauses GC extrêmement oscillantes. La pause du GC avait une plage de 7 millisecondes à 5 secondes complètes après un pic initial qui atteignait 17,5 secondes.
Le graphique suivant montre plus de détails, en régime permanent :
Figure 2 :Détails de la pause du GC, pendant l'état d'équilibre
La figure 2 nous indique que les pauses du GC se répartissent en trois groupes différents :(1) entre 1 et 1,5 seconde ; (2) entre 0,007 seconde et 0,5 seconde; (3) pics entre 1,5 seconde et 5 secondes. C'était très étrange, nous avons donc testé le dernier jdk7u60 pour voir si les données seraient différentes :
Nous avons exécuté les mêmes tests de lecture à 100 % en utilisant exactement les mêmes paramètres JVM :-XX:+UseG1GC -Xms100g -Xmx100g -XX:MaxGCPauseMillis=100
.
Figure 3 :gestion grandement améliorée des pics de temps de pause
Jdk7u60 a considérablement amélioré la capacité de G1 à gérer les pics de temps de pause après le pic initial pendant la phase de stabilisation. Jdk7u60 a réalisé 1029 GC jeunes et mixtes au cours d'une course d'une heure. GC se produisait environ toutes les 3,5 secondes. Jdk7u21 a réalisé 286 GC, chaque GC se produisant environ toutes les 12,6 secondes. Jdk7u60 a pu gérer un temps de pause entre 0,302 et 1 seconde sans pics majeurs.
La figure 4, ci-dessous, nous donne un aperçu plus détaillé de 150 pauses du GC pendant l'état d'équilibre :
Figure 4 :mieux, mais pas assez bien
Pendant l'état stable, jdk7u60 a pu maintenir le temps de pause moyen autour de 369 millisecondes. C'était bien mieux que jdk7u21, mais cela ne répondait toujours pas à notre exigence de 100 millisecondes donnée par –Xx:MaxGCPauseMillis=100
.
Pour déterminer ce que nous pouvions faire d'autre pour obtenir notre temps de pause de 100 millions de secondes, nous devions en savoir plus sur le comportement de la gestion de la mémoire de la JVM et du ramasse-miettes G1 (Garbage First). Les figures suivantes montrent comment G1 fonctionne sur la collection Young Gen.
Figure 5 :Diapositive de la présentation JavaOne 2012 par Charlie Hunt et Monica Beckwith :"G1 Garbage Collector Performance Tuning"
Lorsque la JVM démarre, en fonction des paramètres de lancement de la JVM, elle demande au système d'exploitation d'allouer un gros morceau de mémoire continue pour héberger le tas de la JVM. Ce morceau de mémoire est partitionné par la JVM en régions.
Figure 6 :Diapositive de la présentation JavaOne 2012 par Charlie Hunt et Monica Beckwith :"G1 Garbage Collector Performance Tuning"
Comme le montre la figure 6, chaque objet que le programme Java alloue à l'aide de l'API Java arrive d'abord dans l'espace Eden de la génération Young à gauche. Au bout d'un moment, l'Eden se remplit, et un GC Jeune génération se déclenche. Les objets qui sont toujours référencés (c'est-à-dire "vivants") sont copiés dans l'espace Survivant. Lorsque des objets survivent à plusieurs GC dans la jeune génération, ils sont promus dans l'espace de l'ancienne génération.
Lorsque Young GC se produit, les threads de l'application Java sont arrêtés afin de marquer et de copier en toute sécurité des objets en direct. Ces arrêts sont les fameuses pauses GC "stop-the-world", qui empêchent les applications de répondre jusqu'à ce que les pauses soient terminées.
Figure 7 :Diapositive de la présentation JavaOne 2012 par Charlie Hunt et Monica Beckwith :"G1 Garbage Collector Performance Tuning"
L'ancienne génération peut également devenir bondée. À un certain niveau—contrôlé par -XX:InitiatingHeapOccupancyPercent=?
où la valeur par défaut est de 45 % du tas total, un GC mixte est déclenché. Il recueille à la fois la jeune génération et la vieille génération. Les pauses du GC mixte sont contrôlées par le temps que prend la génération Young pour nettoyer lorsque le GC mixte se produit.
Ainsi, nous pouvons voir dans G1, les pauses GC "arrêter le monde" sont dominées par la vitesse à laquelle G1 peut marquer et copier des objets vivants hors de l'espace Eden. Dans cet esprit, nous analyserons comment le modèle d'allocation de mémoire HBase nous aidera à régler le G1 GC pour obtenir la pause souhaitée de 100 millisecondes.
Dans HBase, il existe deux structures en mémoire qui consomment la majeure partie de son tas :Le BlockCache
, la mise en cache des blocs de fichiers HBase pour les opérations de lecture et le Memstore mettant en cache les dernières mises à jour.
Figure 8 :dans HBase, deux structures en mémoire consomment la majeure partie de son tas.
L'implémentation par défaut de BlockCache
de HBase est le LruBlockCache
, qui utilise simplement un grand tableau d'octets pour héberger tous les blocs HBase. Lorsque les blocs sont "expulsés", la référence à l'objet Java de ce bloc est supprimée, permettant au GC de déplacer la mémoire.
Nouveaux objets formant le LruBlockCache
et Memstore
rendez-vous d'abord à l'espace Eden de la jeune génération. S'ils vivent assez longtemps (c'est-à-dire s'ils ne sont pas expulsés de LruBlockCache
ou débusqués de Memstore), puis après plusieurs jeunes générations de GC, ils se dirigent vers l'ancienne génération du tas Java. Lorsque l'espace libre de l'ancienne génération est inférieur à un threshOld
donné (InitiatingHeapOccupancyPercent
pour commencer), le GC mixte entre en jeu et efface certains objets morts de l'ancienne génération, copie les objets vivants de la jeune génération et recalcule l'Eden de la jeune génération et le HeapOccupancyPercent
de l'ancienne génération. . Finalement, lorsque HeapOccupancyPercent
atteint un certain niveau, un FULL GC
se produit, ce qui fait d'énormes pauses GC "arrêtez le monde" pour nettoyer tous les objets morts à l'intérieur de l'ancienne génération.
Après avoir étudié le journal GC produit par "-XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:+PrintAdaptiveSizePolicy
", nous avons remarqué HeapOccupancyPercent
n'a jamais atteint une taille suffisante pour induire un GC complet pendant la lecture à 100 % de HBase. Les pauses du GC que nous avons vues étaient dominées par les pauses "Arrêtez le monde" de la jeune génération et l'augmentation du traitement des références au fil du temps.
Une fois cette analyse terminée, nous avons apporté trois groupes de modifications au paramètre par défaut du G1 GC :
- Utilisez
-XX:+ParallelRefProcEnabled
Lorsque cet indicateur est activé, GC utilise plusieurs threads pour traiter les références croissantes pendant le GC Young et mixte. Avec cet indicateur pour HBase, le temps de remarquage du GC est réduit de 75 % et le temps de pause global du GC est réduit de 30 %. Set -XX:-ResizePLAB and -XX:ParallelGCThreads=8+(logical processors-8)(5/8)
Les tampons d'allocation locale de promotion (PLAB) sont utilisés lors de la collecte Young. Plusieurs threads sont utilisés. Chaque thread peut avoir besoin d'allouer de l'espace pour les objets copiés dans Survivor ou Old space. Les PLAB sont nécessaires pour éviter la concurrence des threads pour les structures de données partagées qui gèrent la mémoire libre. Chaque thread GC a un PLAB pour l'espace de survie et un pour l'ancien espace. Nous aimerions arrêter de redimensionner les PLAB pour éviter le coût de communication important entre les threads GC, ainsi que les variations au cours de chaque GC. Nous aimerions fixer le nombre de threads GC à la taille calculée par 8+(processeurs logiques-8)( 5/8). Cette formule a été récemment recommandée par Oracle.Avec les deux paramètres, nous sommes en mesure de voir des pauses GC plus fluides pendant l'exécution.- Changer
-XX:G1NewSizePercent
par défaut de 5 à 1 pour un tas de 100 Go Basé sur la sortie de-XX:+PrintGCDetails and -XX:+PrintAdaptiveSizePolicy
, nous avons remarqué que la raison de l'échec de G1 à respecter notre temps de pause de 100GC souhaité était le temps qu'il a fallu pour traiter Eden. En d'autres termes, G1 a mis en moyenne 369 millisecondes pour vider 5 Go d'Eden lors de nos tests. Nous avons ensuite changé la taille d'Eden en utilisant-XX:G1NewSizePercent=
drapeau de 5 à 1. Avec ce changement, nous avons vu le temps de pause du GC réduit à 100 millisecondes.
À partir de cette expérience, nous avons découvert que la vitesse de G1 pour nettoyer Eden est d'environ 1 Go par 100 millisecondes, ou 10 Go par seconde pour la configuration HBase que nous avons utilisée.
En fonction de cette vitesse, nous pouvons définir -XX:G1NewSizePercent=
la taille d'Eden peut donc être maintenue autour de 1 Go. Par exemple :
- Témoin de 32 Go,
-XX:G1NewSizePercent=3
- Témoin de 64 Go, –
XX:G1NewSizePercent=2
- 100 Go et plus,
-XX:G1NewSizePercent=1
- Donc, nos dernières options de ligne de commande pour le HRegionserver sont :
-XX:+UseG1GC
-Xms100g -Xmx100g
(Taille de tas utilisée dans nos tests)-XX:MaxGCPauseMillis=100
(Temps de pause souhaité du GC dans les tests)- –
XX:+ParallelRefProcEnabled
-XX:-ResizePLAB
-XX:ParallelGCThreads= 8+(40-8)(5/8)=28
-XX:G1NewSizePercent=1
Voici le tableau des temps de pause du GC pour exécuter une opération de lecture à 100 % pendant 1 heure :
Figure 9 :les pics de sédimentation initiaux les plus élevés ont été réduits de plus de moitié.
Dans ce graphique, même les pics de décantation initiaux les plus élevés ont été réduits de 3,792 secondes à 1,684 secondes. Les pics les plus initiaux étaient inférieurs à 1 seconde. Après le règlement, GC a pu maintenir un temps de pause d'environ 100 millisecondes.
Le tableau ci-dessous compare les exécutions de jdk7u60 avec et sans réglage, en régime permanent :
Figure 10 :jdk7u60 s'exécute avec et sans réglage, en régime permanent.
Le réglage simple du GC que nous avons décrit ci-dessus donne des temps de pause GC idéaux, d'environ 100 millisecondes, avec une moyenne de 106 millisecondes et un écart type de 7 millisecondes.
Résumé
HBase est une application critique en termes de temps de réponse qui nécessite un temps de pause du GC pour être prévisible et gérable. Avec Oracle jdk7u60, basé sur les informations GC rapportées par -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:+PrintAdaptiveSizePolicy
, nous sommes en mesure de régler le temps de pause du GC jusqu'à nos 100 millisecondes souhaitées.
Eric Kaczmarek est architecte de performance Java au sein du Software Solution Group d'Intel. Il dirige les efforts d'Intel pour activer et optimiser les frameworks Big Data (Hadoop, HBase, Spark, Cassandra) pour les plates-formes Intel.
Les logiciels et les charges de travail utilisés dans les tests de performances peuvent avoir été optimisés pour les performances uniquement sur les microprocesseurs Intel. Les tests de performance, tels que SYSmark et MobileMark, sont mesurés à l'aide de systèmes informatiques, de composants, de logiciels, d'opérations et de fonctions spécifiques. Toute modification de l'un de ces facteurs peut entraîner une variation des résultats. Vous devriez consulter d'autres informations et tests de performance pour vous aider à évaluer pleinement vos achats envisagés, y compris les performances de ce produit lorsqu'il est combiné avec d'autres produits.
Les numéros de processeur Intel ne sont pas une mesure des performances. Les numéros de processeur différencient les fonctionnalités au sein de chaque famille de processeurs. Pas dans différentes familles de processeurs. Accédez à :http://www.intel.com/products/processor_number.
Copyright 2014 Intel Corp. Intel, le logo Intel et Xeon sont des marques commerciales d'Intel Corporation aux États-Unis et/ou dans d'autres pays.