Comment augmenter la vitesse de lecture depuis HBase jusqu'à 3 fois et depuis HDFS jusqu'à 5 fois

La haute performance est l’une des exigences clés lorsque l’on travaille avec du Big Data. Au service de chargement des données de la Sberbank, nous transférons presque toutes les transactions dans notre Data Cloud basé sur Hadoop et traitons donc de très gros flux d'informations. Naturellement, nous recherchons toujours des moyens d'améliorer les performances, et nous souhaitons maintenant vous expliquer comment nous avons réussi à patcher RegionServer HBase et le client HDFS, grâce auxquels nous avons pu augmenter considérablement la vitesse des opérations de lecture.
Comment augmenter la vitesse de lecture depuis HBase jusqu'à 3 fois et depuis HDFS jusqu'à 5 fois

Cependant, avant de passer à l'essence des améliorations, il convient de parler des restrictions qui, en principe, ne peuvent être contournées si vous êtes assis sur un disque dur.

Pourquoi le disque dur et les lectures rapides à accès aléatoire sont incompatibles
Comme vous le savez, HBase et de nombreuses autres bases de données stockent les données dans des blocs de plusieurs dizaines de kilo-octets. Par défaut, il fait environ 64 Ko. Imaginons maintenant que nous n'ayons besoin que de 100 octets et que nous demandions à HBase de nous fournir ces données en utilisant une certaine clé. Étant donné que la taille du bloc dans HFiles est de 64 Ko, la requête sera 640 fois plus grande (juste une minute !) que nécessaire.

Ensuite, puisque la requête passera par HDFS et son mécanisme de mise en cache des métadonnées Cache de court-circuit (qui permet un accès direct aux fichiers), cela conduit à lire déjà 1 Mo du disque. Cependant, cela peut être ajusté avec le paramètre dfs.client.read.shortcircuit.buffer.size et dans de nombreux cas, il est judicieux de réduire cette valeur, par exemple à 126 Ko.

Disons que nous faisons cela, mais en plus, lorsque nous commençons à lire des données via l'API Java, telles que des fonctions comme FileChannel.read et demandons au système d'exploitation de lire la quantité de données spécifiée, il lit « juste au cas où » 2 fois plus , c'est à dire. 256 Ko dans notre cas. En effet, Java ne dispose pas d'un moyen simple pour définir l'indicateur FADV_RANDOM pour empêcher ce comportement.

En conséquence, pour obtenir nos 100 octets, 2600 2 fois plus sont lus sous le capot. Il semblerait que la solution soit évidente, réduisons la taille du bloc à un kilo-octet, définissons l'indicateur mentionné et obtenons une grande accélération de l'illumination. Mais le problème est qu'en réduisant la taille du bloc de 2 fois, nous réduisons également de XNUMX fois le nombre d'octets lus par unité de temps.

Un certain gain en définissant l'indicateur FADV_RANDOM peut être obtenu, mais uniquement avec un multithreading élevé et avec une taille de bloc de 128 Ko, mais cela représente un maximum de quelques dizaines de pour cent :

Comment augmenter la vitesse de lecture depuis HBase jusqu'à 3 fois et depuis HDFS jusqu'à 5 fois

Les tests ont été effectués sur 100 fichiers de 1 Go chacun et situés sur 10 disques durs.

Calculons sur quoi nous pouvons, en principe, compter à cette vitesse :
Disons que nous lisons sur 10 disques à une vitesse de 280 Mo/s, c'est-à-dire 3 millions de fois 100 octets. Mais comme nous nous en souvenons, les données dont nous avons besoin sont 2600 3 fois inférieures à celles lues. Ainsi, on divise 2600 millions par XNUMX et on obtient 1100 enregistrements par seconde.

Déprimant, n'est-ce pas ? C'est la nature Accès aléatoire accès aux données sur le disque dur - quelle que soit la taille du bloc. Il s’agit de la limite physique de l’accès aléatoire et aucune base de données ne peut en extraire davantage dans de telles conditions.

Comment alors les bases de données atteignent-elles des vitesses beaucoup plus élevées ? Pour répondre à cette question, regardons ce qui se passe dans l'image suivante :

Comment augmenter la vitesse de lecture depuis HBase jusqu'à 3 fois et depuis HDFS jusqu'à 5 fois

Nous voyons ici que pendant les premières minutes, la vitesse est en réalité d'environ mille enregistrements par seconde. Cependant, en raison du fait que beaucoup plus de données sont lues que ce qui a été demandé, les données se retrouvent dans le buff/cache du système d'exploitation (Linux) et la vitesse augmente jusqu'à 60 XNUMX par seconde, plus décente.

Ainsi, nous traiterons plus loin de l'accélération de l'accès uniquement aux données qui se trouvent dans le cache du système d'exploitation ou situées dans des périphériques de stockage SSD/NVMe de vitesse d'accès comparable.

Dans notre cas, nous réaliserons des tests sur un banc de 4 serveurs dont chacun est facturé comme suit :

Processeur : Xeon E5-2680 v4 à 2.40 GHz 64 threads.
Mémoire : 730 Go.
version Java : 1.8.0_111

Et ici, le point clé est la quantité de données dans les tableaux qui doivent être lues. Le fait est que si vous lisez les données d’une table entièrement placée dans le cache HBase, il ne sera même pas possible de les lire à partir du buff/cache du système d’exploitation. Car HBase alloue par défaut 40% de la mémoire à une structure appelée BlockCache. Il s'agit essentiellement d'un ConcurrentHashMap, où la clé est le nom du fichier + le décalage du bloc, et la valeur correspond aux données réelles à ce décalage.

Ainsi, en lisant uniquement cette structure, nous nous voyons une excellente vitesse, comme un million de requêtes par seconde. Mais imaginons que nous ne puissions pas allouer des centaines de gigaoctets de mémoire uniquement pour les besoins des bases de données, car de nombreuses autres choses utiles s'exécutent sur ces serveurs.

Par exemple, dans notre cas, le volume de BlockCache sur un RS est d'environ 12 Go. Nous avons atterri deux RS sur un nœud, c'est-à-dire 96 Go sont alloués pour BlockCache sur tous les nœuds. Et il y a beaucoup plus de données, par exemple, qu'il s'agisse de 4 tables de 130 régions chacune, dans lesquelles les fichiers ont une taille de 800 Mo, compressés par FAST_DIFF, c'est-à-dire un total de 410 Go (il s'agit de données pures, c'est-à-dire sans tenir compte du facteur de réplication).

Ainsi, BlockCache ne représente qu’environ 23 % du volume total de données, ce qui est beaucoup plus proche des conditions réelles de ce qu’on appelle le BigData. Et c'est là que le plaisir commence - car évidemment, moins il y a d'accès au cache, plus les performances sont mauvaises. Après tout, si vous manquez, vous devrez faire beaucoup de travail - c'est-à-dire descendre à l’appel des fonctions système. Cependant, cela ne peut être évité, alors regardons un aspect complètement différent : qu'arrive-t-il aux données à l'intérieur du cache ?

Simplifions la situation et supposons que nous disposons d'un cache qui ne contient qu'un seul objet. Voici un exemple de ce qui se passera lorsque l'on essaiera de travailler avec un volume de données 1 fois plus grand que le cache, il faudra :

1. Placez le bloc 1 dans le cache
2. Supprimez le bloc 1 du cache
3. Placez le bloc 2 dans le cache
4. Supprimez le bloc 2 du cache
5. Placez le bloc 3 dans le cache

5 actions réalisées ! Cependant, cette situation ne peut pas être qualifiée de normale : en fait, nous obligeons HBase à faire un tas de travaux complètement inutiles. Il lit constamment les données du cache du système d'exploitation, les place dans BlockCache, pour ensuite les supprimer presque immédiatement car une nouvelle partie des données est arrivée. L'animation au début de l'article montre l'essence du problème - Garbage Collector déraille, l'atmosphère se réchauffe, la petite Greta dans la Suède lointaine et chaude s'énerve. Et nous, les informaticiens, n’aimons vraiment pas que les enfants soient tristes, alors nous commençons à réfléchir à ce que nous pouvons faire pour y remédier.

Que se passe-t-il si vous ne mettez pas tous les blocs dans le cache, mais seulement un certain pourcentage d'entre eux, afin que le cache ne déborde pas ? Commençons par ajouter simplement quelques lignes de code au début de la fonction permettant de mettre des données dans BlockCache :

  public void cacheBlock(BlockCacheKey cacheKey, Cacheable buf, boolean inMemory) {
    if (cacheDataBlockPercent != 100 && buf.getBlockType().isData()) {
      if (cacheKey.getOffset() % 100 >= cacheDataBlockPercent) {
        return;
      }
    }
...

Le point ici est le suivant : le décalage est la position du bloc dans le fichier et ses derniers chiffres sont répartis de manière aléatoire et uniforme de 00 à 99. Par conséquent, nous ignorerons uniquement ceux qui entrent dans la plage dont nous avons besoin.

Par exemple, définissez cacheDataBlockPercent = 20 et voyez ce qui se passe :

Comment augmenter la vitesse de lecture depuis HBase jusqu'à 3 fois et depuis HDFS jusqu'à 5 fois

Le résultat est évident. Dans les graphiques ci-dessous, il devient clair pourquoi une telle accélération s'est produite - nous économisons beaucoup de ressources GC sans faire le travail de Sisyphe consistant à placer les données dans le cache pour ensuite les jeter immédiatement aux oubliettes des chiens martiens :

Comment augmenter la vitesse de lecture depuis HBase jusqu'à 3 fois et depuis HDFS jusqu'à 5 fois

Dans le même temps, l’utilisation du processeur augmente, mais est bien inférieure à la productivité :

Comment augmenter la vitesse de lecture depuis HBase jusqu'à 3 fois et depuis HDFS jusqu'à 5 fois

Il convient également de noter que les blocs stockés dans BlockCache sont différents. La plupart, environ 95 %, sont des données elles-mêmes. Et le reste, ce sont des métadonnées, comme les filtres Bloom ou LEAF_INDEX et т.д.. Ces données ne suffisent pas, mais elles sont très utiles, car avant d'accéder directement aux données, HBase se tourne vers la méta pour comprendre s'il est nécessaire de chercher ici plus loin et, si oui, où se trouve exactement le bloc qui nous intéresse.

Par conséquent, dans le code, nous voyons une condition de vérification buf.getBlockType().isData() et grâce à cette méta, nous le laisserons dans le cache de toute façon.

Augmentons maintenant la charge et resserrons légèrement la fonctionnalité d'un seul coup. Lors du premier test, nous avons fixé le pourcentage de coupure à 20 et BlockCache était légèrement sous-utilisé. Maintenant, réglons-le à 23 % et ajoutons 100 threads toutes les 5 minutes pour voir à quel moment la saturation se produit :

Comment augmenter la vitesse de lecture depuis HBase jusqu'à 3 fois et depuis HDFS jusqu'à 5 fois

Nous voyons ici que la version originale atteint presque immédiatement le plafond à environ 100 300 requêtes par seconde. Alors que le patch donne une accélération allant jusqu'à XNUMX XNUMX. Dans le même temps, il est clair qu'une accélération supplémentaire n'est plus aussi « gratuite » ; l'utilisation du processeur augmente également.

Cependant, ce n'est pas une solution très élégante, car on ne sait pas à l'avance quel pourcentage de blocs doivent être mis en cache, cela dépend du profil de charge. Par conséquent, un mécanisme a été mis en place pour ajuster automatiquement ce paramètre en fonction de l’activité des opérations de lecture.

Trois options ont été ajoutées pour contrôler cela :

hbase.lru.cache.heavy.eviction.count.limit - définit combien de fois le processus d'expulsion des données du cache doit être exécuté avant de commencer à utiliser l'optimisation (c'est-à-dire sauter des blocs). Par défaut, il est égal à MAX_INT = 2147483647 et signifie en fait que la fonctionnalité ne commencera jamais à fonctionner avec cette valeur. Parce que le processus d'expulsion démarre toutes les 5 à 10 secondes (cela dépend de la charge) et 2147483647 * 10/60/60/24/365 = 680 ans. Cependant, nous pouvons définir ce paramètre sur 0 et faire fonctionner la fonctionnalité immédiatement après le lancement.

Cependant, il y a aussi une charge utile dans ce paramètre. Si notre charge est telle que les lectures à court terme (par exemple pendant la journée) et les lectures à long terme (la nuit) sont constamment entrecoupées, nous pouvons alors nous assurer que la fonctionnalité n'est activée que lorsque des opérations de lecture longue sont en cours.

Par exemple, nous savons que les lectures à court terme durent généralement environ 1 minute. Il n'est pas nécessaire de commencer à jeter des blocs, le cache n'aura pas le temps de devenir obsolète et nous pourrons alors définir ce paramètre égal à, par exemple, 10. Cela conduira au fait que l'optimisation ne commencera à fonctionner que lorsque longtemps- la lecture active du terme a commencé, c'est-à-dire en 100 secondes. Ainsi, si nous avons une lecture à court terme, alors tous les blocs iront dans le cache et seront disponibles (sauf ceux qui seront expulsés par l'algorithme standard). Et lorsque nous effectuons des lectures à long terme, la fonctionnalité est activée et nous aurions des performances bien supérieures.

hbase.lru.cache.heavy.eviction.mb.size.limit - définit le nombre de mégaoctets que nous souhaitons placer dans le cache (et, bien sûr, expulser) en 10 secondes. La fonctionnalité tentera d’atteindre cette valeur et de la maintenir. Le fait est le suivant : si nous mettons des gigaoctets dans le cache, alors nous devrons expulser des gigaoctets, et cela, comme nous l'avons vu ci-dessus, coûte très cher. Cependant, vous ne devez pas essayer de le définir trop petit, car cela entraînerait la sortie prématurée du mode de saut de bloc. Pour les serveurs puissants (environ 20 à 40 cœurs physiques), il est optimal de définir environ 300 à 400 Mo. Pour la classe moyenne (~10 cœurs) 200-300 Mo. Pour les systèmes faibles (2 à 5 cœurs), 50 à 100 Mo peuvent être normaux (non testés sur ceux-ci).

Voyons comment cela fonctionne : disons que nous définissons hbase.lru.cache.heavy.eviction.mb.size.limit = 500, il y a une sorte de charge (lecture), puis toutes les ~ 10 secondes, nous calculons le nombre d'octets. expulsé selon la formule :

Overhead = Somme des octets libérés (Mo) * 100 / Limite (Mo) - 100 ;

Si en fait 2000 XNUMX Mo ont été supprimés, alors les frais généraux sont égaux à :

2000 * 100 / 500 - 100 = 300 %

Les algorithmes essaient de ne maintenir que quelques dizaines de pour cent, la fonctionnalité réduira donc le pourcentage de blocs mis en cache, mettant ainsi en œuvre un mécanisme de réglage automatique.

Cependant, si la charge chute, disons que seulement 200 Mo sont expulsés et que les frais généraux deviennent négatifs (ce qu'on appelle le dépassement) :

200 * 100 / 500 - 100 = -60%

Au contraire, la fonctionnalité augmentera le pourcentage de blocs mis en cache jusqu'à ce que Overhead devienne positif.

Vous trouverez ci-dessous un exemple de ce à quoi cela ressemble sur des données réelles. Il n’est pas nécessaire de chercher à atteindre 0%, c’est impossible. C'est très bien lorsqu'il est d'environ 30 à 100 %, cela permet d'éviter une sortie prématurée du mode d'optimisation lors de surtensions à court terme.

hbase.lru.cache.heavy.eviction.overhead.coefficient - définit la rapidité avec laquelle nous souhaitons obtenir le résultat. Si nous sommes sûrs que nos lectures sont pour la plupart longues et que nous ne voulons pas attendre, nous pouvons augmenter ce ratio et obtenir des performances élevées plus rapidement.

Par exemple, nous fixons ce coefficient = 0.01. Cela signifie que les frais généraux (voir ci-dessus) seront multipliés par ce nombre par le résultat obtenu et que le pourcentage de blocs mis en cache sera réduit. Supposons que Overhead = 300 % et coefficient = 0.01, alors le pourcentage de blocs mis en cache sera réduit de 3 %.

Une logique de « contre-pression » similaire est également implémentée pour les valeurs de surcharge négatives (dépassement). Les fluctuations à court terme du volume de lectures et d'expulsions étant toujours possibles, ce mécanisme permet d'éviter une sortie prématurée du mode d'optimisation. La contre-pression a une logique inversée : plus le dépassement est fort, plus de blocs sont mis en cache.

Comment augmenter la vitesse de lecture depuis HBase jusqu'à 3 fois et depuis HDFS jusqu'à 5 fois

Code d'implémentation

        LruBlockCache cache = this.cache.get();
        if (cache == null) {
          break;
        }
        freedSumMb += cache.evict()/1024/1024;
        /*
        * Sometimes we are reading more data than can fit into BlockCache
        * and it is the cause a high rate of evictions.
        * This in turn leads to heavy Garbage Collector works.
        * So a lot of blocks put into BlockCache but never read,
        * but spending a lot of CPU resources.
        * Here we will analyze how many bytes were freed and decide
        * decide whether the time has come to reduce amount of caching blocks.
        * It help avoid put too many blocks into BlockCache
        * when evict() works very active and save CPU for other jobs.
        * More delails: https://issues.apache.org/jira/browse/HBASE-23887
        */

        // First of all we have to control how much time
        // has passed since previuos evict() was launched
        // This is should be almost the same time (+/- 10s)
        // because we get comparable volumes of freed bytes each time.
        // 10s because this is default period to run evict() (see above this.wait)
        long stopTime = System.currentTimeMillis();
        if ((stopTime - startTime) > 1000 * 10 - 1) {
          // Here we have to calc what situation we have got.
          // We have the limit "hbase.lru.cache.heavy.eviction.bytes.size.limit"
          // and can calculte overhead on it.
          // We will use this information to decide,
          // how to change percent of caching blocks.
          freedDataOverheadPercent =
            (int) (freedSumMb * 100 / cache.heavyEvictionMbSizeLimit) - 100;
          if (freedSumMb > cache.heavyEvictionMbSizeLimit) {
            // Now we are in the situation when we are above the limit
            // But maybe we are going to ignore it because it will end quite soon
            heavyEvictionCount++;
            if (heavyEvictionCount > cache.heavyEvictionCountLimit) {
              // It is going for a long time and we have to reduce of caching
              // blocks now. So we calculate here how many blocks we want to skip.
              // It depends on:
             // 1. Overhead - if overhead is big we could more aggressive
              // reducing amount of caching blocks.
              // 2. How fast we want to get the result. If we know that our
              // heavy reading for a long time, we don't want to wait and can
              // increase the coefficient and get good performance quite soon.
              // But if we don't sure we can do it slowly and it could prevent
              // premature exit from this mode. So, when the coefficient is
              // higher we can get better performance when heavy reading is stable.
              // But when reading is changing we can adjust to it and set
              // the coefficient to lower value.
              int change =
                (int) (freedDataOverheadPercent * cache.heavyEvictionOverheadCoefficient);
              // But practice shows that 15% of reducing is quite enough.
              // We are not greedy (it could lead to premature exit).
              change = Math.min(15, change);
              change = Math.max(0, change); // I think it will never happen but check for sure
              // So this is the key point, here we are reducing % of caching blocks
              cache.cacheDataBlockPercent -= change;
              // If we go down too deep we have to stop here, 1% any way should be.
              cache.cacheDataBlockPercent = Math.max(1, cache.cacheDataBlockPercent);
            }
          } else {
            // Well, we have got overshooting.
            // Mayby it is just short-term fluctuation and we can stay in this mode.
            // It help avoid permature exit during short-term fluctuation.
            // If overshooting less than 90%, we will try to increase the percent of
            // caching blocks and hope it is enough.
            if (freedSumMb >= cache.heavyEvictionMbSizeLimit * 0.1) {
              // Simple logic: more overshooting - more caching blocks (backpressure)
              int change = (int) (-freedDataOverheadPercent * 0.1 + 1);
              cache.cacheDataBlockPercent += change;
              // But it can't be more then 100%, so check it.
              cache.cacheDataBlockPercent = Math.min(100, cache.cacheDataBlockPercent);
            } else {
              // Looks like heavy reading is over.
              // Just exit form this mode.
              heavyEvictionCount = 0;
              cache.cacheDataBlockPercent = 100;
            }
          }
          LOG.info("BlockCache evicted (MB): {}, overhead (%): {}, " +
            "heavy eviction counter: {}, " +
            "current caching DataBlock (%): {}",
            freedSumMb, freedDataOverheadPercent,
            heavyEvictionCount, cache.cacheDataBlockPercent);

          freedSumMb = 0;
          startTime = stopTime;
       }

Voyons maintenant tout cela à l'aide d'un exemple réel. Nous avons le script de test suivant :

  1. Commençons par Scan (25 threads, batch = 100)
  2. Après 5 minutes, ajoutez des multi-gets (25 threads, batch = 100)
  3. Après 5 minutes, désactivez les multi-gets (seul le scan reste à nouveau)

Nous effectuons deux exécutions, d'abord hbase.lru.cache.heavy.eviction.count.limit = 10000 (ce qui désactive en fait la fonctionnalité), puis définissons limit = 0 (l'active).

Dans les journaux ci-dessous, nous voyons comment la fonctionnalité est activée et réinitialise le dépassement à 14-71 %. De temps en temps, la charge diminue, ce qui active la contre-pression et HBase met à nouveau plus de blocs en cache.

Journaliser le serveur de région
expulsés (Mo) : 0, ratio 0.0, surcharge (%) : -100, compteur d'expulsions lourdes : 0, mise en cache actuelle DataBlock (%) : 100
expulsés (Mo) : 0, ratio 0.0, surcharge (%) : -100, compteur d'expulsions lourdes : 0, mise en cache actuelle DataBlock (%) : 100
expulsés (Mo) : 2170 1.09, ratio 985, surcharge (%) : 1, compteur d'expulsions lourdes : 91, mise en cache actuelle DataBlock (%) : XNUMX < début
expulsés (Mo) : 3763, ratio 1.08, surcharge (%) : 1781, compteur d'expulsions lourdes : 2, mise en cache actuelle DataBlock (%) : 76
expulsés (Mo) : 3306, ratio 1.07, surcharge (%) : 1553, compteur d'expulsions lourdes : 3, mise en cache actuelle DataBlock (%) : 61
expulsés (Mo) : 2508, ratio 1.06, surcharge (%) : 1154, compteur d'expulsions lourdes : 4, mise en cache actuelle DataBlock (%) : 50
expulsés (Mo) : 1824, ratio 1.04, surcharge (%) : 812, compteur d'expulsions lourdes : 5, mise en cache actuelle DataBlock (%) : 42
expulsés (Mo) : 1482, ratio 1.03, surcharge (%) : 641, compteur d'expulsions lourdes : 6, mise en cache actuelle DataBlock (%) : 36
expulsés (Mo) : 1140, ratio 1.01, surcharge (%) : 470, compteur d'expulsions lourdes : 7, mise en cache actuelle DataBlock (%) : 32
expulsés (Mo) : 913, ratio 1.0, surcharge (%) : 356, compteur d'expulsions lourdes : 8, mise en cache actuelle DataBlock (%) : 29
expulsés (Mo) : 912, ratio 0.89, surcharge (%) : 356, compteur d'expulsions lourdes : 9, mise en cache actuelle DataBlock (%) : 26
expulsés (Mo) : 684, ratio 0.76, surcharge (%) : 242, compteur d'expulsions lourdes : 10, mise en cache actuelle DataBlock (%) : 24
expulsés (Mo) : 684, ratio 0.61, surcharge (%) : 242, compteur d'expulsions lourdes : 11, mise en cache actuelle DataBlock (%) : 22
expulsés (Mo) : 456, ratio 0.51, surcharge (%) : 128, compteur d'expulsions lourdes : 12, mise en cache actuelle DataBlock (%) : 21
expulsés (Mo) : 456, ratio 0.42, surcharge (%) : 128, compteur d'expulsions lourdes : 13, mise en cache actuelle DataBlock (%) : 20
expulsés (Mo) : 456, ratio 0.33, surcharge (%) : 128, compteur d'expulsions lourdes : 14, mise en cache actuelle DataBlock (%) : 19
expulsés (Mo) : 342, ratio 0.33, surcharge (%) : 71, compteur d'expulsions lourdes : 15, mise en cache actuelle DataBlock (%) : 19
expulsés (Mo) : 342, ratio 0.32, surcharge (%) : 71, compteur d'expulsions lourdes : 16, mise en cache actuelle DataBlock (%) : 19
expulsés (Mo) : 342, ratio 0.31, surcharge (%) : 71, compteur d'expulsions lourdes : 17, mise en cache actuelle DataBlock (%) : 19
expulsés (Mo) : 228, ratio 0.3, surcharge (%) : 14, compteur d'expulsions lourdes : 18, mise en cache actuelle DataBlock (%) : 19
expulsés (Mo) : 228, ratio 0.29, surcharge (%) : 14, compteur d'expulsions lourdes : 19, mise en cache actuelle DataBlock (%) : 19
expulsés (Mo) : 228, ratio 0.27, surcharge (%) : 14, compteur d'expulsions lourdes : 20, mise en cache actuelle DataBlock (%) : 19
expulsés (Mo) : 228, ratio 0.25, surcharge (%) : 14, compteur d'expulsions lourdes : 21, mise en cache actuelle DataBlock (%) : 19
expulsés (Mo) : 228, ratio 0.24, surcharge (%) : 14, compteur d'expulsions lourdes : 22, mise en cache actuelle DataBlock (%) : 19
expulsés (Mo) : 228, ratio 0.22, surcharge (%) : 14, compteur d'expulsions lourdes : 23, mise en cache actuelle DataBlock (%) : 19
expulsés (Mo) : 228, ratio 0.21, surcharge (%) : 14, compteur d'expulsions lourdes : 24, mise en cache actuelle DataBlock (%) : 19
expulsés (Mo) : 228, ratio 0.2, surcharge (%) : 14, compteur d'expulsions lourdes : 25, mise en cache actuelle DataBlock (%) : 19
expulsés (Mo) : 228, ratio 0.17, surcharge (%) : 14, compteur d'expulsions lourdes : 26, mise en cache actuelle DataBlock (%) : 19
expulsés (Mo) : 456, ratio 0.17, surcharge (%) : 128, compteur d'expulsion lourd : 27, mise en cache actuelle DataBlock (%) : 18 < ajouts d'obtentions (mais tableau identique)
expulsés (Mo) : 456, ratio 0.15, surcharge (%) : 128, compteur d'expulsions lourdes : 28, mise en cache actuelle DataBlock (%) : 17
expulsés (Mo) : 342, ratio 0.13, surcharge (%) : 71, compteur d'expulsions lourdes : 29, mise en cache actuelle DataBlock (%) : 17
expulsés (Mo) : 342, ratio 0.11, surcharge (%) : 71, compteur d'expulsions lourdes : 30, mise en cache actuelle DataBlock (%) : 17
expulsés (Mo) : 342, ratio 0.09, surcharge (%) : 71, compteur d'expulsions lourdes : 31, mise en cache actuelle DataBlock (%) : 17
expulsés (Mo) : 228, ratio 0.08, surcharge (%) : 14, compteur d'expulsions lourdes : 32, mise en cache actuelle DataBlock (%) : 17
expulsés (Mo) : 228, ratio 0.07, surcharge (%) : 14, compteur d'expulsions lourdes : 33, mise en cache actuelle DataBlock (%) : 17
expulsés (Mo) : 228, ratio 0.06, surcharge (%) : 14, compteur d'expulsions lourdes : 34, mise en cache actuelle DataBlock (%) : 17
expulsés (Mo) : 228, ratio 0.05, surcharge (%) : 14, compteur d'expulsions lourdes : 35, mise en cache actuelle DataBlock (%) : 17
expulsés (Mo) : 228, ratio 0.05, surcharge (%) : 14, compteur d'expulsions lourdes : 36, mise en cache actuelle DataBlock (%) : 17
expulsés (Mo) : 228, ratio 0.04, surcharge (%) : 14, compteur d'expulsions lourdes : 37, mise en cache actuelle DataBlock (%) : 17
expulsés (Mo) : 109, rapport 0.04, surcharge (%) : -46, compteur d'expulsions lourdes : 37, mise en cache actuelle DataBlock (%) : 22 < contre-pression
expulsés (Mo) : 798, ratio 0.24, surcharge (%) : 299, compteur d'expulsions lourdes : 38, mise en cache actuelle DataBlock (%) : 20
expulsés (Mo) : 798, ratio 0.29, surcharge (%) : 299, compteur d'expulsions lourdes : 39, mise en cache actuelle DataBlock (%) : 18
expulsés (Mo) : 570, ratio 0.27, surcharge (%) : 185, compteur d'expulsions lourdes : 40, mise en cache actuelle DataBlock (%) : 17
expulsés (Mo) : 456, ratio 0.22, surcharge (%) : 128, compteur d'expulsions lourdes : 41, mise en cache actuelle DataBlock (%) : 16
expulsés (Mo) : 342, ratio 0.16, surcharge (%) : 71, compteur d'expulsions lourdes : 42, mise en cache actuelle DataBlock (%) : 16
expulsés (Mo) : 342, ratio 0.11, surcharge (%) : 71, compteur d'expulsions lourdes : 43, mise en cache actuelle DataBlock (%) : 16
expulsés (Mo) : 228, ratio 0.09, surcharge (%) : 14, compteur d'expulsions lourdes : 44, mise en cache actuelle DataBlock (%) : 16
expulsés (Mo) : 228, ratio 0.07, surcharge (%) : 14, compteur d'expulsions lourdes : 45, mise en cache actuelle DataBlock (%) : 16
expulsés (Mo) : 228, ratio 0.05, surcharge (%) : 14, compteur d'expulsions lourdes : 46, mise en cache actuelle DataBlock (%) : 16
expulsés (Mo) : 222, ratio 0.04, surcharge (%) : 11, compteur d'expulsions lourdes : 47, mise en cache actuelle DataBlock (%) : 16
expulsés (Mo) : 104, ratio 0.03, surcharge (%) : -48, compteur d'expulsions lourdes : 47, mise en cache actuelle DataBlock (%) : 21 < interruption obtenue
expulsés (Mo) : 684, ratio 0.2, surcharge (%) : 242, compteur d'expulsions lourdes : 48, mise en cache actuelle DataBlock (%) : 19
expulsés (Mo) : 570, ratio 0.23, surcharge (%) : 185, compteur d'expulsions lourdes : 49, mise en cache actuelle DataBlock (%) : 18
expulsés (Mo) : 342, ratio 0.22, surcharge (%) : 71, compteur d'expulsions lourdes : 50, mise en cache actuelle DataBlock (%) : 18
expulsés (Mo) : 228, ratio 0.21, surcharge (%) : 14, compteur d'expulsions lourdes : 51, mise en cache actuelle DataBlock (%) : 18
expulsés (Mo) : 228, ratio 0.2, surcharge (%) : 14, compteur d'expulsions lourdes : 52, mise en cache actuelle DataBlock (%) : 18
expulsés (Mo) : 228, ratio 0.18, surcharge (%) : 14, compteur d'expulsions lourdes : 53, mise en cache actuelle DataBlock (%) : 18
expulsés (Mo) : 228, ratio 0.16, surcharge (%) : 14, compteur d'expulsions lourdes : 54, mise en cache actuelle DataBlock (%) : 18
expulsés (Mo) : 228, ratio 0.14, surcharge (%) : 14, compteur d'expulsions lourdes : 55, mise en cache actuelle DataBlock (%) : 18
expulsés (Mo) : 112, rapport 0.14, surcharge (%) : -44, compteur d'expulsions lourdes : 55, mise en cache actuelle DataBlock (%) : 23 < contre-pression
expulsés (Mo) : 456, ratio 0.26, surcharge (%) : 128, compteur d'expulsions lourdes : 56, mise en cache actuelle DataBlock (%) : 22
expulsés (Mo) : 342, ratio 0.31, surcharge (%) : 71, compteur d'expulsions lourdes : 57, mise en cache actuelle DataBlock (%) : 22
expulsés (Mo) : 342, ratio 0.33, surcharge (%) : 71, compteur d'expulsions lourdes : 58, mise en cache actuelle DataBlock (%) : 22
expulsés (Mo) : 342, ratio 0.33, surcharge (%) : 71, compteur d'expulsions lourdes : 59, mise en cache actuelle DataBlock (%) : 22
expulsés (Mo) : 342, ratio 0.33, surcharge (%) : 71, compteur d'expulsions lourdes : 60, mise en cache actuelle DataBlock (%) : 22
expulsés (Mo) : 342, ratio 0.33, surcharge (%) : 71, compteur d'expulsions lourdes : 61, mise en cache actuelle DataBlock (%) : 22
expulsés (Mo) : 342, ratio 0.33, surcharge (%) : 71, compteur d'expulsions lourdes : 62, mise en cache actuelle DataBlock (%) : 22
expulsés (Mo) : 342, ratio 0.33, surcharge (%) : 71, compteur d'expulsions lourdes : 63, mise en cache actuelle DataBlock (%) : 22
expulsés (Mo) : 342, ratio 0.32, surcharge (%) : 71, compteur d'expulsions lourdes : 64, mise en cache actuelle DataBlock (%) : 22
expulsés (Mo) : 342, ratio 0.33, surcharge (%) : 71, compteur d'expulsions lourdes : 65, mise en cache actuelle DataBlock (%) : 22
expulsés (Mo) : 342, ratio 0.33, surcharge (%) : 71, compteur d'expulsions lourdes : 66, mise en cache actuelle DataBlock (%) : 22
expulsés (Mo) : 342, ratio 0.32, surcharge (%) : 71, compteur d'expulsions lourdes : 67, mise en cache actuelle DataBlock (%) : 22
expulsés (Mo) : 342, ratio 0.33, surcharge (%) : 71, compteur d'expulsions lourdes : 68, mise en cache actuelle DataBlock (%) : 22
expulsés (Mo) : 342, ratio 0.32, surcharge (%) : 71, compteur d'expulsions lourdes : 69, mise en cache actuelle DataBlock (%) : 22
expulsés (Mo) : 342, ratio 0.32, surcharge (%) : 71, compteur d'expulsions lourdes : 70, mise en cache actuelle DataBlock (%) : 22
expulsés (Mo) : 342, ratio 0.33, surcharge (%) : 71, compteur d'expulsions lourdes : 71, mise en cache actuelle DataBlock (%) : 22
expulsés (Mo) : 342, ratio 0.33, surcharge (%) : 71, compteur d'expulsions lourdes : 72, mise en cache actuelle DataBlock (%) : 22
expulsés (Mo) : 342, ratio 0.33, surcharge (%) : 71, compteur d'expulsions lourdes : 73, mise en cache actuelle DataBlock (%) : 22
expulsés (Mo) : 342, ratio 0.33, surcharge (%) : 71, compteur d'expulsions lourdes : 74, mise en cache actuelle DataBlock (%) : 22
expulsés (Mo) : 342, ratio 0.33, surcharge (%) : 71, compteur d'expulsions lourdes : 75, mise en cache actuelle DataBlock (%) : 22
expulsés (Mo) : 342, ratio 0.33, surcharge (%) : 71, compteur d'expulsions lourdes : 76, mise en cache actuelle DataBlock (%) : 22
expulsés (Mo) : 21, ratio 0.33, surcharge (%) : -90, compteur d'expulsions lourdes : 76, mise en cache actuelle DataBlock (%) : 32
expulsés (Mo) : 0, ratio 0.0, surcharge (%) : -100, compteur d'expulsions lourdes : 0, mise en cache actuelle DataBlock (%) : 100
expulsés (Mo) : 0, ratio 0.0, surcharge (%) : -100, compteur d'expulsions lourdes : 0, mise en cache actuelle DataBlock (%) : 100

Les analyses étaient nécessaires pour montrer le même processus sous la forme d'un graphique de la relation entre deux sections de cache - simple (où les blocs qui n'ont jamais été demandés auparavant) et multi (les données « demandées » au moins une fois sont stockées ici) :

Comment augmenter la vitesse de lecture depuis HBase jusqu'à 3 fois et depuis HDFS jusqu'à 5 fois

Et enfin, à quoi ressemble le fonctionnement des paramètres sous forme de graphique. A titre de comparaison, le cache a été complètement désactivé au début, puis HBase a été lancé avec mise en cache et retardement du début des travaux d'optimisation de 5 minutes (30 cycles d'expulsion).

Le code complet peut être trouvé dans Pull Request HBASE23887 sur github.

Cependant, 300 XNUMX lectures par seconde ne sont pas tout ce qui peut être réalisé sur ce matériel dans ces conditions. Le fait est que lorsque vous devez accéder aux données via HDFS, le mécanisme ShortCircuitCache (ci-après dénommé SSC) est utilisé, ce qui vous permet d'accéder directement aux données, en évitant les interactions réseau.

Le profilage a montré que bien que ce mécanisme apporte un gain important, il devient également à un moment donné un goulot d'étranglement, car presque toutes les opérations lourdes se déroulent à l'intérieur d'une serrure, ce qui conduit la plupart du temps à un blocage.

Comment augmenter la vitesse de lecture depuis HBase jusqu'à 3 fois et depuis HDFS jusqu'à 5 fois

Ayant réalisé cela, nous avons réalisé que le problème pouvait être contourné en créant un ensemble de SSC indépendants :

private final ShortCircuitCache[] shortCircuitCache;
...
shortCircuitCache = new ShortCircuitCache[this.clientShortCircuitNum];
for (int i = 0; i < this.clientShortCircuitNum; i++)
  this.shortCircuitCache[i] = new ShortCircuitCache(…);

Et puis travaillez avec eux, en excluant également les intersections au dernier chiffre de décalage :

public ShortCircuitCache getShortCircuitCache(long idx) {
    return shortCircuitCache[(int) (idx % clientShortCircuitNum)];
}

Vous pouvez maintenant commencer les tests. Pour ce faire, nous allons lire les fichiers depuis HDFS avec une simple application multithread. Définissez les paramètres :

conf.set("dfs.client.read.shortcircuit", "true");
conf.set("dfs.client.read.shortcircuit.buffer.size", "65536"); // по дефолту = 1 МБ и это сильно замедляет чтение, поэтому лучше привести в соответствие к реальным нуждам
conf.set("dfs.client.short.circuit.num", num); // от 1 до 10

Et lisez simplement les fichiers :

FSDataInputStream in = fileSystem.open(path);
for (int i = 0; i < count; i++) {
    position += 65536;
    if (position > 900000000)
        position = 0L;
    int res = in.read(position, byteBuffer, 0, 65536);
}

Ce code est exécuté dans des threads séparés et nous augmenterons le nombre de fichiers lus simultanément (de 10 à 200 - axe horizontal) et le nombre de caches (de 1 à 10 - graphiques). L'axe vertical montre l'accélération qui résulte d'une augmentation de SSC par rapport au cas où il n'y a qu'un seul cache.

Comment augmenter la vitesse de lecture depuis HBase jusqu'à 3 fois et depuis HDFS jusqu'à 5 fois

Comment lire le graphique : Le temps d'exécution pour 100 64 lectures dans des blocs de 78 Ko avec un cache nécessite 5 secondes. Alors qu’avec 16 caches cela prend 5 secondes. Ceux. il y a une accélération d'environ 50 fois. Comme le montre le graphique, l'effet n'est pas très perceptible pour un petit nombre de lectures parallèles, il commence à jouer un rôle notable lorsqu'il y a plus de 6 lectures de threads. On remarque également qu'en augmentant le nombre de SSC de XNUMX et supérieur donne une augmentation des performances nettement inférieure.

Remarque 1 : les résultats des tests étant assez volatiles (voir ci-dessous), 3 essais ont été effectués et les valeurs résultantes ont été moyennées.

Remarque 2 : le gain de performances lié à la configuration de l'accès aléatoire est le même, bien que l'accès lui-même soit légèrement plus lent.

Il faut cependant préciser que, contrairement au cas de HBase, cette accélération n’est pas toujours gratuite. Ici, nous « débloquons » la capacité du processeur à travailler davantage, au lieu de nous accrocher aux verrous.

Comment augmenter la vitesse de lecture depuis HBase jusqu'à 3 fois et depuis HDFS jusqu'à 5 fois

Ici, vous pouvez observer qu'en général, une augmentation du nombre de caches entraîne une augmentation à peu près proportionnelle de l'utilisation du processeur. Il existe cependant un peu plus de combinaisons gagnantes.

Par exemple, regardons de plus près le réglage SSC = 3. L'augmentation des performances sur la gamme est d'environ 3.3 fois. Vous trouverez ci-dessous les résultats des trois analyses distinctes.

Comment augmenter la vitesse de lecture depuis HBase jusqu'à 3 fois et depuis HDFS jusqu'à 5 fois

Alors que la consommation du processeur augmente d'environ 2.8 fois. La différence n'est pas très grande, mais la petite Greta est déjà heureuse et aura peut-être le temps d'aller à l'école et de prendre des cours.

Ainsi, cela aura un effet positif pour tout outil utilisant un accès groupé à HDFS (par exemple Spark, etc.), à condition que le code de l'application soit léger (c'est-à-dire que la prise est côté client HDFS) et qu'il y ait de la puissance CPU libre. . Pour vérifier, testons quel effet aura l'utilisation combinée de l'optimisation BlockCache et du réglage SSC pour la lecture à partir de HBase.

Comment augmenter la vitesse de lecture depuis HBase jusqu'à 3 fois et depuis HDFS jusqu'à 5 fois

On voit que dans de telles conditions l'effet n'est pas aussi important que dans des tests raffinés (lecture sans aucun traitement), mais il est tout à fait possible d'extraire ici 80K supplémentaires. Ensemble, les deux optimisations offrent une accélération jusqu'à 4x.

Un PR a également été réalisé pour cette optimisation [HDFS-15202], qui a été fusionné et cette fonctionnalité sera disponible dans les prochaines versions.

Et enfin, il était intéressant de comparer les performances de lecture d’une base de données similaire à larges colonnes, Cassandra et HBase.

Pour ce faire, nous avons lancé des instances de l'utilitaire de test de charge standard YCSB à partir de deux hôtes (800 threads au total). Côté serveur - 4 instances de RegionServer et Cassandra sur 4 hôtes (pas ceux sur lesquels s'exécutent les clients, pour éviter leur influence). Les lectures provenaient de tableaux de tailles :

HBase – 300 Go sur HDFS (100 Go de données pures)

Cassandra - 250 Go (facteur de réplication = 3)

Ceux. le volume était à peu près le même (un peu plus dans HBase).

Paramètres HBase :

dfs.client.short.circuit.num = 5 (Optimisation du client HDFS)

hbase.lru.cache.heavy.eviction.count.limit = 30 - cela signifie que le patch commencera à fonctionner après 30 expulsions (~5 minutes)

hbase.lru.cache.heavy.eviction.mb.size.limit = 300 — volume cible de mise en cache et d'expulsion

Les journaux YCSB ont été analysés et compilés dans des graphiques Excel :

Comment augmenter la vitesse de lecture depuis HBase jusqu'à 3 fois et depuis HDFS jusqu'à 5 fois

Comme vous pouvez le constater, ces optimisations permettent de comparer les performances de ces bases de données dans ces conditions et d'atteindre 450 mille lectures par seconde.

Nous espérons que ces informations pourront être utiles à quelqu'un pendant la lutte passionnante pour la productivité.

Source: habr.com

Ajouter un commentaire