Comment réduire de 90 % le temps de récupération de place Java ? Tout le monde devrait être familier avec GC en Java. La manière dont l'optimisation GC est effectuée est expliquée en détail ci-dessous. Le mécanisme GC de la JVM protège les développeurs des détails de la gestion de la mémoire et améliore l'efficacité du développement. apache php mysql
Il n'y a pas si longtemps, nous nous sommes préparés à surmonter ce problème communément reconnu sur Ali-HBase. À cette fin, nous avons mené une analyse approfondie et un travail d'innovation complet, et avons obtenu des résultats relativement bons. En prenant comme exemple le scénario de contrôle des risques Ant, le temps de GC en ligne de HBase a été réduit de 120 ms à 15 ms. En combinaison avec ZenGC, un outil fourni par l'équipe Alibaba JDK, il a encore atteint 5 ms dans l'environnement de test de résistance en laboratoire. Cet article présente principalement certains de nos travaux antérieurs et des idées techniques dans ce domaine.
Le mécanisme GC de JVM protège les développeurs des détails de la gestion de la mémoire et améliore l'efficacité du développement. En parlant de GC, la première réaction de nombreuses personnes peut être que la JVM s'arrête pendant une longue période ou que FGC bloque le processus et le rend inutilisable. Mais pour les services de stockage de Big Data comme HBase, les défis GC posés par JVM sont assez complexes et difficiles. Il y a trois raisons :
1. La taille de la mémoire est énorme. La plupart des processus HBase en ligne sont de gros tas de 96G. Cette année, de nouveaux modèles ont été lancés avec des configurations de tas de plus de 160G
2. Le statut de l'objet est complexe. Le serveur HBase maintient en interne un grand nombre de caches de lecture et d'écriture, atteignant une échelle de plusieurs dizaines de Go. HBase fournit des données de service ordonnées sous forme de tableaux, et les données sont organisées dans une certaine structure. Ces structures de données génèrent plus de 100 millions d'objets et de références
3. La fréquence des jeunes GC est élevée. Plus la pression d'accès est élevée, plus la consommation de mémoire dans la zone jeune est rapide. Certains clusters occupés peuvent atteindre 1 à 2 jeunes GC par seconde. Une grande zone jeune peut réduire la fréquence du GC, mais cela entraînera des pauses plus importantes pour les jeunes GC et endommagera le système. exigences en temps réel.
En tant que système de stockage, HBase utilise une grande quantité de mémoire comme tampon d'écriture et cache de lecture, comme un grand tas de 96 Go (4 Go jeune + 92 Go ancien ), le tampon d'écriture + le cache de lecture occuperont plus de 70 % de la mémoire (environ 70 Go), le niveau de mémoire dans le tas lui-même sera contrôlé à 85 % et la mémoire occupée restante ne sera que de 10 Go. Par conséquent, si nous pouvons auto-gérer cette mémoire de 70 Go+ au niveau de l'application, alors pour la JVM, la pression GC d'un grand tas de 100 Go sera équivalente à la pression GC d'un petit tas de 10 Go, et elle sera également confrontée à des des tas à l’avenir. N’aggravera pas les ballonnements. Grâce à cette solution, notre temps de jeune GC en ligne a été optimisé de 120 ms à 15 ms.
Dans un système de services à haut débit et gourmand en données, un grand nombre d'objets temporaires sont fréquemment créés et recyclés. Comment gérer l'allocation et le recyclage de ces objets temporaires de manière ciblée. , développé par l'équipe AliJDK Un nouvel algorithme GC basé sur des locataires : ZenGC. La HBase du Groupe a été transformée sur la base de ce nouvel algorithme ZenGC. Le temps de jeune GC que nous avons mesuré en laboratoire a été réduit de 15 ms à 5 ms. Il s'agit d'un effet inattendu et extrême.
Ce qui suit présentera une par une les technologies clés utilisées dans l'optimisation GC de la version Ali-HBase.
Le modèle de stockage actuellement utilisé par HBase est le modèle LSMTree. Les données écrites seront temporairement stockées dans la mémoire jusqu'à une certaine taille puis transférées sur le disque. constituer un dossier.
Nous l'appellerons ci-dessous cache d'écriture. Le cache d'écriture est interrogeable, ce qui nécessite que les données soient ordonnées en mémoire. Afin d'améliorer l'efficacité de la lecture et de l'écriture simultanées et de répondre aux exigences de base en matière de commande de données et de prise en charge de la recherche et de l'analyse, SkipList est une structure de données largement utilisée.
Nous prenons le ConcurrentSkipListMap fourni avec JDK comme exemple d'analyse. Il présente les trois problèmes suivants :
Il y en a. de nombreux objets internes. Chaque fois qu'un élément est stocké, une moyenne de 4 objets (index+nœud+clé+valeur, la hauteur moyenne de la couche est de 1)
Les objets nouvellement insérés se trouvent dans la zone jeune, et les anciens les objets sont dans l'ancienne zone. Lorsque des éléments sont insérés en continu, la relation de référence interne changera fréquemment. Qu'il s'agisse de la marque CardTable de l'algorithme ParNew ou de la marque RSet de l'algorithme G1, il est possible de déclencher l'ancienne analyse de zone.
L'élément KeyValue écrit par l'entreprise n'est pas de longueur régulière Lorsqu'il est promu vers l'ancienne zone, un grand nombre de fragments de mémoire peuvent être générés.
Le problème 1 rend le coût de numérisation des objets du jeune GC de zone très élevé, et davantage d'objets sont promus au cours du jeune GC. Le problème 2 entraîne l’expansion de l’ancienne zone qui doit être analysée lors du jeune GC. Le problème 3 augmente la probabilité de FGC causée par la fragmentation de la mémoire. Le problème devient plus grave lorsque les éléments à écrire sont plus petits. Nous avons réalisé des statistiques sur le processus RegionServer en ligne et constaté qu'il y a jusqu'à 120 millions d'objets actifs !
Après avoir analysé le plus grand ennemi du jeune GC actuel, une idée audacieuse est venue puisque l'allocation, l'accès, la destruction et le recyclage du cache en écriture sont tous gérés par nos soins, si la JVM "ne peut pas voir" Avec. cache d'écriture, nous gérons nous-mêmes le cycle de vie du cache d'écriture et le problème du GC sera naturellement résolu.
En parlant de rendre la JVM "invisible", beaucoup de gens peuvent penser à la solution hors tas, mais ce n'est pas si simple pour la mise en cache en écriture, car même si la KeyValue est placée hors tas, elle ne peut pas éviter les questions 1 et 2. Et 1 et 2 sont aussi les plus gros problèmes pour les jeunes GC.
La question se transforme maintenant en : Comment créer une carte ordonnée qui prend en charge l'accès simultané sans utiliser d'objets JVM.
Bien sûr, nous ne pouvons pas accepter une perte de performances, car la vitesse d'écriture de Map est étroitement liée au débit d'écriture de HBase.
La demande est à nouveau renforcée : comment créer une carte ordonnée prenant en charge l'accès simultané sans utiliser d'objets et sans aucune perte de performances.
Afin d'atteindre cet objectif, nous avons conçu une telle structure de données :
Elle utilise la mémoire continue (à l'intérieur du tas ou à l'extérieur du tas), et nous contrôlons le structure interne via le code Plutôt que de s'appuyer sur le mécanisme objet de la JVM,
est également logiquement une SkipList, prenant en charge l'écriture et les requêtes simultanées sans verrouillage
le pointeur de contrôle et les données sont stockés dans la mémoire continue
La figure ci-dessus montre la structure de la mémoire de CCSMap (CompactedConcurrentSkipListMap). Nous demandons de la mémoire cache en écriture sous la forme de grands segments de mémoire (Chunk). Chaque morceau contient plusieurs nœuds et chaque nœud correspond à un élément. Les éléments nouvellement insérés sont toujours placés à la fin de la mémoire utilisée. La structure complexe à l'intérieur de Node stocke des informations de maintenance et des données telles que Index/Suivant/Clé/Valeur. Les éléments nouvellement insérés doivent être copiés dans la structure Node. Lorsqu'un vidage du cache en écriture se produit dans HBase, tous les morceaux de l'ensemble du CCSMap seront recyclés. Lorsqu'un élément est supprimé, nous "expulsons" logiquement l'élément de la liste chaînée et ne récupérons pas réellement l'élément de la mémoire (bien sûr, il existe des moyens de procéder à une récupération réelle, mais en ce qui concerne HBase, il existe ce n'est pas nécessaire).
Bien qu'il y ait une copie supplémentaire lors de l'insertion des données KeyValue, dans la plupart des cas, la copie sera plus rapide. Étant donné que, de par la structure de CCSMap, le nœud de contrôle et la KeyValue d'un élément dans une Map sont adjacents en mémoire, l'utilisation du cache CPU est plus efficace et la recherche sera plus rapide. Pour SkipList, la vitesse d'écriture est en fait limitée par la vitesse de recherche, et la surcharge provoquée par la copie réelle est bien inférieure à la surcharge de recherche. Selon nos tests, par rapport au ConcurrentSkipListMap fourni avec le JDK, le débit de lecture et d'écriture a augmenté de 20 à 30 % dans le test KV d'une longueur de 50 octets.
Comme il n'y a pas d'objets JVM, chaque objet JVM occupe au moins 16 octets d'espace et peut être enregistré (8 octets sont réservés aux balises et 8 octets sont des pointeurs de type). En prenant comme exemple la KeyValue d'une longueur de 50 octets, par rapport au ConcurrentSkipListMap fourni avec le JDK, l'utilisation de la mémoire de CCSMap est réduite de 40 %.
Après le lancement de CCSMap en production, l'effet d'optimisation réel : le jeune GC a été réduit de 120 ms+ à 30 ms
Avant l'optimisation
Après l'optimisation
Après avoir utilisé CCSMap, les 120 millions d'objets survivants d'origine ont été réduits à moins de 10 millions, ce qui a considérablement réduit la pression du GC. Grâce à la disposition compacte de la mémoire, le débit d'écriture a également été amélioré de 30 %.
HBase organise les données sur le disque sous forme de blocs. La taille typique d’un bloc HBase est comprise entre 16 Ko et 64 Ko. HBase gère BlockCache en interne pour réduire les E/S disque. BlockCache, comme le cache d'écriture, n'est pas conforme à l'hypothèse générationnelle de la théorie de l'algorithme GC et est intrinsèquement hostile à l'algorithme GC - il n'est ni éphémère ni permanent.
Un morceau de données de bloc est chargé du disque dans la mémoire de la JVM. Le cycle de vie varie de quelques minutes à plusieurs mois. La plupart des blocs entreront dans l'ancienne zone et ne seront recyclés par la JVM que lors du GC majeur. . Ses problèmes se reflètent principalement dans :
La taille du bloc HBase n'est pas fixe et relativement grande, et la mémoire est facilement fragmentée
Dans l'algorithme ParNew, la promotion est gênante. Le problème ne se reflète pas dans le coût de la copie, mais dans la grande taille et le coût élevé de la recherche d'un espace approprié pour stocker le bloc HBase.
L'idée de l'optimisation du cache de lecture est d'appliquer à la JVM un morceau de mémoire qui ne sera jamais renvoyé sous forme de BlockCache. Nous segmentons nous-mêmes la mémoire en segments de taille fixe. chargé dans la mémoire, nous copions le bloc en segments et marqués comme utilisé. Lorsque ce bloc n'est plus nécessaire, nous marquerons l'intervalle comme disponible et de nouveaux blocs pourront être restaurés. Il s'agit du BucketCache. Concernant l'allocation et le recyclage de l'espace mémoire dans BucketCache (la conception et le développement de ce domaine ont été achevés il y a de nombreuses années)
BucketCache
De nombreux frameworks RPC basés sur la mémoire hors tas géreront également l'allocation et le recyclage de la mémoire hors tas, généralement via une libération explicite. Mais pour HBase, il existe quelques difficultés. Nous considérons les objets Block comme des segments de mémoire qui doivent être autogérés. Le bloc peut être référencé par plusieurs tâches. Pour résoudre le problème du recyclage des blocs, le moyen le plus simple est de copier le bloc dans la pile pour chaque tâche (le bloc copié n'est généralement pas promu dans l'ancienne zone) et de le transférer vers la JVM pour gestion.
En fait, nous avons déjà utilisé cette méthode, qui est simple à mettre en œuvre, approuvée par JVM, sûre et fiable. Mais il s'agit d'une méthode de gestion de mémoire avec perte. Afin de résoudre le problème du GC, un coût de copie pour chaque requête est introduit. Puisque la copie vers la pile nécessite des coûts supplémentaires de copie CPU et des coûts d'allocation mémoire zone jeune, ce prix semble élevé aujourd'hui alors que les CPU et les bus deviennent de plus en plus précieux.
Nous nous sommes donc tournés vers l'utilisation du comptage de références pour gérer la mémoire. Les principales difficultés rencontrées dans HBase sont :
Il y aura plusieurs tâches dans HBase référençant le même bloc
Il peut y avoir plusieurs variables faisant référence au même bloc dans la même tâche. La référence peut être une variable temporaire sur la pile ou un champ objet sur le tas.
La logique de traitement sur Block est relativement complexe. Block sera transmis entre plusieurs fonctions et objets sous la forme de paramètres, de valeurs de retour et d'affectations de champs.
Le bloc peut être géré par nous, ou il peut ne pas être géré par nous (certains blocs doivent être libérés manuellement, d'autres non).
Block peut être converti en un sous-type de Block.
En prenant ces points ensemble, c'est un défi d'écrire un code correct. Mais en C++, il est naturel d'utiliser des pointeurs intelligents pour gérer le cycle de vie des objets. Pourquoi est-ce difficile en Java ?
L'affectation de variable en Java, au niveau du code utilisateur, ne produit qu'un comportement d'affectation de référence, tandis que l'affectation de variable en C++ peut utiliser le constructeur et le destructeur de l'objet pour faire beaucoup de choses, des pointeurs intelligents, c'est-à-dire sur cette base implémentation (bien sûr, une mauvaise utilisation des constructeurs et des destructeurs C++ entraînera également de nombreux problèmes, chacun avec ses propres avantages et inconvénients, qui ne seront pas abordés ici)
Nous avons donc fait référence aux pointeurs intelligents de C++ et avons conçu un Block Le cadre de gestion et de recyclage des références ShrableHolder est utilisé pour éliminer diverses difficultés de codage if else. Il a le paradigme suivant :
ShrableHolder peut gérer des objets avec comptage de références et des objets sans comptage de références.
ShrableHolder est utilisé lorsqu'il est Quand réaffectation, l'objet précédent est libéré. S'il s'agit d'un objet géré, le compteur de références est décrémenté de 1, sinon il n'y a aucun changement.
ShrableHolder doit être appelé réinitialisé à la fin de la tâche ou à la fin du segment de code
ShrableHolder ne peut pas être attribué directement. La méthode fournie par ShrableHolder doit être appelée pour transférer du contenu
Étant donné que ShrableHolder ne peut pas être affecté directement, lorsque vous devez transmettre un bloc contenant la sémantique du cycle de vie à une fonction, ShrableHolder ne peut pas être utilisé comme un paramètre de la fonction.
Le code écrit selon ce paradigme présente peu de changements par rapport à la logique du code d'origine et aucun if else n'est introduit. Bien qu'il semble encore y avoir une certaine complexité, heureusement, la plage affectée par cela est encore limitée à une couche inférieure très locale, ce qui reste acceptable pour HBase. Pour être prudent et éviter les fuites de mémoire, nous avons ajouté un mécanisme de détection à ce framework pour détecter les références inactives depuis longtemps. Une fois trouvées, elles seront marquées de force pour suppression.
Après avoir utilisé BucketCache, les frais généraux de promotion de BlockCache sont réduits et le temps de jeune GC est réduit :
( Effet d'optimisation CCSMap + BucketCache)
Après les deux optimisations majeures ci-dessus, le temps de jeune GC de l'environnement de production Ant Risk Control a été réduit à 15 ms. Comme il est déjà difficile d’optimiser l’algorithme ParNew+CMS à cette échelle, nous nous sommes tournés vers ZenGC. ZenGC a apporté des améliorations en profondeur basées sur l'algorithme G1. Le tas de mémoire autogéré de HBase et ZenGC a produit une bonne réaction chimique.
ZenGC est le nom collectif de l'algorithme GC optimisé par l'équipe Alibaba JVM basé sur l'algorithme G1 et orienté vers des scénarios d'application à grand tas (LargeHeap). Ici, nous introduisons principalement le GC multi-tenant.
Le GC multi-tenant contient trois couches de logique de base : 1) Sur JavaHeap, l'allocation d'objets est isolée en fonction des locataires, et différents locataires utilisent différentes zones de tas 2) Permettre au GC de se produire dans les locataires à moindre coût ; Granularité, pas seulement l'application globale ; 3) Permettre aux applications de couche supérieure de cartographier de manière flexible les locataires en fonction des besoins de l'entreprise.
ZenGC divise la région mémoire en plusieurs locataires et déclenche GC indépendamment dans chaque locataire. Sur cette base, nous divisons la mémoire en locataires ordinaires et locataires à cycle de vie moyen. Les objets à durée de vie moyenne sont des objets qui ne sont ni éphémères ni permanents. En raison des deux optimisations majeures ci-dessus, le nombre d'objets du cycle de vie et l'utilisation de la mémoire dans le tas sont désormais très faibles. Cependant, les objets de cycle de vie moyen seront référencés par les anciens objets de zone lorsqu'ils seront générés, et chaque jeune GC devra analyser le RSet, qui reste la partie la plus longue du jeune GC.
A l'aide de la fonction ObjectTrace de l'équipe AJDK, nous découvrons la "plus grande" partie des objets du cycle de vie moyen, et allouons directement ces objets à l'ancienne zone du locataire du cycle de vie moyen lorsqu'ils sont générés, en évitant la marque RSet. Les locataires normaux allouent la mémoire de la manière normale.
La fréquence GC des locataires ordinaires est très élevée, mais comme il y a peu d'objets promus et peu de références transgénérationnelles, le temps GC dans la zone Young est bien contrôlé. Dans l’environnement de simulation de scènes de laboratoire, nous avons optimisé le jeune GC à 5 ms.
(Effet optimisé ZenGC, problème d'unité, nous voilà)
Ali-HBase fournit actuellement des services commerciaux sur Alibaba Cloud. Tout utilisateur dans le besoin peut utiliser le service HBase à guichet unique profondément amélioré sur Alibaba Cloud. Par rapport à HBase auto-construit, la version cloud HBase présente de nombreuses améliorations en termes d'exploitation et de maintenance, de fiabilité, de performances, de stabilité, de sécurité et de coût.
Articles connexes :
5 suggestions pour réduire les frais généraux de collecte des ordures Java
Vidéos associées :
Ce qui précède est le contenu détaillé de. pour plus d'informations, suivez d'autres articles connexes sur le site Web de PHP en chinois!