Jak zwiększyć prędkość odczytu z HBase do 3 razy i z HDFS do 5 razy

Wysoka wydajność jest jednym z kluczowych wymagań podczas pracy z dużymi zbiorami danych. W dziale ładowania danych w Sbierbanku pompujemy prawie wszystkie transakcje do naszej chmury danych opartej na Hadoop i dlatego mamy do czynienia z naprawdę dużymi przepływami informacji. Naturalnie zawsze szukamy sposobów na poprawę wydajności, a teraz chcemy opowiedzieć, jak udało nam się załatać RegionServer HBase i klienta HDFS, dzięki czemu udało nam się znacznie zwiększyć prędkość operacji odczytu.
Jak zwiększyć prędkość odczytu z HBase do 3 razy i z HDFS do 5 razy

Zanim jednak przejdziemy do istoty usprawnień, warto porozmawiać o ograniczeniach, których w zasadzie nie da się obejść siedząc na dysku HDD.

Dlaczego odczyty z dysku twardego i szybkiego dostępu swobodnego są niekompatybilne
Jak wiadomo, HBase i wiele innych baz danych przechowuje dane w blokach o rozmiarze kilkudziesięciu kilobajtów. Domyślnie jest to około 64 KB. Teraz wyobraźmy sobie, że potrzebujemy tylko 100 bajtów i prosimy HBase o przekazanie nam tych danych za pomocą określonego klucza. Ponieważ rozmiar bloku w HFiles wynosi 64 KB, żądanie będzie 640 razy większe (w ciągu zaledwie minuty!) niż jest to konieczne.

Następnie, ponieważ żądanie przejdzie przez system HDFS i jego mechanizm buforowania metadanych Pamięć podręczna krótkiego obwodu (co umożliwia bezpośredni dostęp do plików), prowadzi to do odczytania już 1 MB z dysku. Można to jednak dostosować za pomocą parametru dfs.client.read.shortcircuit.buffer.size i w wielu przypadkach sensowne jest zmniejszenie tej wartości, na przykład do 126 KB.

Powiedzmy, że to robimy, ale dodatkowo, gdy zaczynamy czytać dane przez API Java, takie jak funkcje takie jak FileChannel.read i poprosimy system operacyjny o odczytanie określonej ilości danych, odczyta „na wszelki wypadek” 2 razy więcej , tj. w naszym przypadku 256 KB. Dzieje się tak, ponieważ w Javie nie ma łatwego sposobu ustawienia flagi FADV_RANDOM, aby zapobiec takiemu zachowaniu.

W rezultacie, aby uzyskać nasze 100 bajtów, pod maską wczytuje się 2600 razy więcej. Wydawać by się mogło, że rozwiązanie jest oczywiste, zmniejszmy rozmiar bloku do kilobajta, ustawmy wspomnianą flagę i uzyskajmy duże przyspieszenie oświecenia. Problem jednak w tym, że zmniejszając rozmiar bloku 2 razy, zmniejszamy także 2 razy liczbę bajtów odczytywanych w jednostce czasu.

Można uzyskać pewien zysk z ustawienia flagi FADV_RANDOM, ale tylko przy dużej wielowątkowości i rozmiarze bloku 128 KB, ale jest to maksymalnie kilkadziesiąt procent:

Jak zwiększyć prędkość odczytu z HBase do 3 razy i z HDFS do 5 razy

Testy przeprowadzono na 100 plikach o rozmiarze 1 GB każdy, znajdujących się na 10 dyskach twardych.

Obliczmy na co w zasadzie możemy liczyć przy tej prędkości:
Załóżmy, że czytamy z 10 dysków z prędkością 280 MB/s, tj. 3 miliony razy 100 bajtów. Ale jak pamiętamy, danych, których potrzebujemy, jest 2600 razy mniej niż to, co odczytujemy. Zatem dzielimy 3 miliony przez 2600 i otrzymujemy 1100 rekordów na sekundę.

Przygnębiające, prawda? To jest natura Losowy dostęp dostęp do danych na dysku twardym – niezależnie od wielkości bloku. Jest to fizyczny limit losowego dostępu i żadna baza danych nie jest w stanie wycisnąć więcej w takich warunkach.

Jak zatem bazy danych osiągają znacznie wyższe prędkości? Aby odpowiedzieć na to pytanie, spójrzmy, co dzieje się na poniższym obrazku:

Jak zwiększyć prędkość odczytu z HBase do 3 razy i z HDFS do 5 razy

Tutaj widzimy, że przez pierwsze kilka minut prędkość wynosi tak naprawdę około tysiąca rekordów na sekundę. Jednak dalej, w związku z tym, że odczytano znacznie więcej, niż żądano, dane trafiają do bufora/pamięci podręcznej systemu operacyjnego (linux) i prędkość wzrasta do przyzwoitych 60 tys. na sekundę

Zatem w dalszej części zajmiemy się przyspieszaniem dostępu tylko do danych znajdujących się w pamięci podręcznej systemu operacyjnego lub znajdujących się na urządzeniach pamięci masowej SSD/NVMe o porównywalnej szybkości dostępu.

W naszym przypadku testy przeprowadzimy na ławce składającej się z 4 serwerów, z których każdy jest rozliczany w następujący sposób:

Procesor: Xeon E5-2680 v4 @ 2.40 GHz 64 wątki.
Pamięć: 730 GB.
wersja Java: 1.8.0_111

I tutaj kluczową kwestią jest ilość danych w tabelach, które należy przeczytać. Faktem jest, że jeśli odczytasz dane z tabeli, która jest w całości umieszczona w pamięci podręcznej HBase, to nie dojdzie nawet do odczytu z bufora/pamięci podręcznej systemu operacyjnego. Ponieważ HBase domyślnie przydziela 40% pamięci strukturze o nazwie BlockCache. Zasadniczo jest to ConcurrentHashMap, gdzie kluczem jest nazwa pliku + przesunięcie bloku, a wartością są rzeczywiste dane w tym przesunięciu.

Zatem czytając tylko z tej struktury, my widzimy doskonałą prędkość, czyli milion żądań na sekundę. Ale wyobraźmy sobie, że nie możemy przydzielić setek gigabajtów pamięci tylko na potrzeby baz danych, ponieważ na tych serwerach działa wiele innych przydatnych rzeczy.

Na przykład w naszym przypadku objętość BlockCache na jednym RS wynosi około 12 GB. Wylądowaliśmy dwa RS na jednym węźle, tj. Na wszystkie węzły przydzielono 96 GB dla BlockCache. A danych jest wielokrotnie więcej, niech to będą np. 4 tabele po 130 regionów każda, w których pliki mają rozmiar 800 MB, skompresowane metodą FAST_DIFF, czyli tzw. łącznie 410 GB (to czyste dane, czyli bez uwzględnienia współczynnika replikacji).

Zatem BlockCache stanowi tylko około 23% całkowitego wolumenu danych i jest to znacznie bliższe rzeczywistym warunkom tzw. BigData. I tu zaczyna się zabawa – bo oczywiście im mniej trafień w pamięć podręczną, tym gorsza wydajność. Przecież jeśli spudłujesz, będziesz musiał wykonać dużo pracy - tj. przejdź do wywoływania funkcji systemowych. Nie da się tego jednak uniknąć, więc spójrzmy na zupełnie inny aspekt – co dzieje się z danymi wewnątrz pamięci podręcznej?

Uprośćmy sytuację i załóżmy, że mamy pamięć podręczną mieszczącą tylko 1 obiekt. Oto przykład tego, co się stanie, gdy spróbujemy pracować z woluminem danych 3 razy większym niż pamięć podręczna, będziemy musieli:

1. Umieść blok 1 w pamięci podręcznej
2. Usuń blok 1 z pamięci podręcznej
3. Umieść blok 2 w pamięci podręcznej
4. Usuń blok 2 z pamięci podręcznej
5. Umieść blok 3 w pamięci podręcznej

5 akcji ukończonych! Jednak tej sytuacji nie można nazwać normalną; w rzeczywistości zmuszamy HBase do wykonania szeregu całkowicie bezużytecznej pracy. Stale odczytuje dane z pamięci podręcznej systemu operacyjnego, umieszcza je w BlockCache, by niemal natychmiast wyrzucić, ponieważ przybyła nowa porcja danych. Animacja na początku postu pokazuje istotę problemu - Śmieciarka wypada poza skalę, atmosfera się ociepla, mała Greta w odległej i gorącej Szwecji zaczyna się denerwować. A my, informatycy, naprawdę nie lubimy, gdy dzieci są smutne, więc zaczynamy myśleć o tym, co możemy z tym zrobić.

A co jeśli umieścisz nie wszystkie bloki w pamięci podręcznej, a tylko określony ich procent, aby pamięć podręczna się nie przepełniła? Zacznijmy od prostego dodania kilku linijek kodu na początku funkcji umieszczania danych w BlockCache:

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

Sprawa jest następująca: offset to pozycja bloku w pliku, a jego ostatnie cyfry są losowo i równomiernie rozłożone od 00 do 99. Dlatego pominiemy tylko te, które mieszczą się w potrzebnym nam zakresie.

Na przykład ustaw cacheDataBlockPercent = 20 i zobacz, co się stanie:

Jak zwiększyć prędkość odczytu z HBase do 3 razy i z HDFS do 5 razy

Wynik jest oczywisty. Na poniższych wykresach staje się jasne, dlaczego nastąpiło takie przyspieszenie - oszczędzamy mnóstwo zasobów GC, nie wykonując syzyfowej pracy polegającej na umieszczaniu danych w pamięci podręcznej, tylko po to, aby natychmiast wyrzucić je do ścieków marsjańskich psów:

Jak zwiększyć prędkość odczytu z HBase do 3 razy i z HDFS do 5 razy

Jednocześnie wzrasta wykorzystanie procesora, ale jest ono znacznie mniejsze niż produktywność:

Jak zwiększyć prędkość odczytu z HBase do 3 razy i z HDFS do 5 razy

Warto również zauważyć, że bloki przechowywane w BlockCache są różne. Większość, około 95%, to same dane. A reszta to metadane, takie jak filtry Blooma czy LEAF_INDEX i т.д.. Dane te nie wystarczą, ale są bardzo przydatne, ponieważ przed bezpośrednim dostępem do danych HBase zwraca się do meta, aby zrozumieć, czy trzeba tu szukać dalej, a jeśli tak, to gdzie dokładnie znajduje się interesujący nas blok.

Dlatego w kodzie widzimy warunek sprawdzenia buf.getBlockType().isData() i dzięki tej meta i tak pozostawimy go w pamięci podręcznej.

Teraz zwiększmy obciążenie i lekko dokręćmy funkcję za jednym razem. W pierwszym teście ustawiliśmy procent odcięcia = 20, a BlockCache był nieco niewykorzystany. Teraz ustawmy go na 23% i dodawajmy 100 wątków co 5 minut, aby zobaczyć, w którym momencie następuje nasycenie:

Jak zwiększyć prędkość odczytu z HBase do 3 razy i z HDFS do 5 razy

Tutaj widzimy, że wersja oryginalna niemal natychmiast osiąga pułap około 100 tysięcy żądań na sekundę. Natomiast patch daje przyspieszenie do 300 tys. Jednocześnie jasne jest, że dalsze przyspieszanie nie jest już tak „darmowe”, wzrasta również wykorzystanie procesora.

Nie jest to jednak zbyt eleganckie rozwiązanie, ponieważ nie wiemy z góry, jaki procent bloków należy buforować, zależy to od profilu obciążenia. Dlatego zaimplementowano mechanizm automatycznego dostosowywania tego parametru w zależności od aktywności operacji odczytu.

Dodano trzy opcje umożliwiające kontrolę tego:

hbase.lru.cache.heavy.eviction.count.limit — ustawia, ile razy powinien przebiegać proces eksmisji danych z pamięci podręcznej, zanim zaczniemy stosować optymalizację (czyli pomijanie bloków). Domyślnie jest równa MAX_INT = 2147483647 i w rzeczywistości oznacza, że ​​funkcja nigdy nie zacznie działać z tą wartością. Ponieważ proces eksmisji rozpoczyna się co 5 - 10 sekund (zależy to od obciążenia) i 2147483647 * 10 / 60 / 60 / 24 / 365 = 680 lat. Możemy jednak ustawić ten parametr na 0 i sprawić, że funkcja zacznie działać natychmiast po uruchomieniu.

Jednak w tym parametrze znajduje się również ładunek. Jeśli nasze obciążenie jest takie, że odczyty krótkoterminowe (powiedzmy w ciągu dnia) i odczyty długoterminowe (w nocy) są stale przeplatane, możemy upewnić się, że funkcja jest włączona tylko wtedy, gdy trwają operacje długiego odczytu.

Na przykład wiemy, że odczyty krótkoterminowe trwają zwykle około 1 minuty. Nie ma potrzeby zaczynać wyrzucania bloków, pamięć podręczna nie będzie miała czasu się zestarzeć i wtedy możemy ustawić ten parametr na np. 10. Spowoduje to, że optymalizacja zacznie działać dopiero gdy długo- rozpoczął się semestr aktywnego czytania, tj. za 100 sekund. Zatem jeśli mamy odczyt krótkotrwały, to wszystkie bloki trafią do pamięci podręcznej i będą dostępne (z wyjątkiem tych, które zostaną wyrzucone przez standardowy algorytm). A kiedy wykonujemy odczyty długoterminowe, funkcja jest włączona i mielibyśmy znacznie wyższą wydajność.

hbase.lru.cache.heavy.eviction.mb.size.limit — ustawia, ile megabajtów chcielibyśmy umieścić w pamięci podręcznej (i oczywiście wyrzucić) w ciągu 10 sekund. Funkcja spróbuje osiągnąć tę wartość i ją utrzymać. Rzecz w tym, że jeśli wepchniemy gigabajty do pamięci podręcznej, będziemy musieli eksmitować gigabajty, a to, jak widzieliśmy powyżej, jest bardzo drogie. Nie powinieneś jednak próbować ustawiać go zbyt małego, ponieważ spowoduje to przedwczesne wyjście z trybu pomijania bloku. W przypadku wydajnych serwerów (około 20-40 rdzeni fizycznych) optymalnie jest ustawić około 300-400 MB. Dla klasy średniej (~10 rdzeni) 200-300 MB. W przypadku słabych systemów (2-5 rdzeni) 50-100 MB może być normalne (nie testowano na nich).

Przyjrzyjmy się, jak to działa: powiedzmy, że ustawiamy hbase.lru.cache.heavy.eviction.mb.size.limit = 500, następuje pewne obciążenie (odczyt) i następnie co ~10 sekund obliczamy, ile bajtów było eksmitowany za pomocą wzoru:

Narzut = Suma zwolnionych bajtów (MB) * 100 / Limit (MB) - 100;

Jeśli rzeczywiście wyeksmitowano 2000 MB, to Narzut jest równy:

2000 * 100 / 500 - 100 = 300%

Algorytmy starają się zachować nie więcej niż kilkadziesiąt procent, więc funkcja zmniejszy procent bloków buforowanych, wdrażając w ten sposób mechanizm automatycznego dostrajania.

Jeśli jednak obciążenie spadnie, powiedzmy, że tylko 200 MB zostanie eksmitowanych, a obciążenie będzie ujemne (tzw. przekroczenie):

200 * 100 / 500 - 100 = -60%

Wręcz przeciwnie, funkcja ta zwiększy procent bloków w pamięci podręcznej, dopóki Overhead nie stanie się dodatni.

Poniżej znajduje się przykład tego, jak to wygląda na rzeczywistych danych. Nie ma potrzeby próbować osiągnąć 0%, jest to niemożliwe. Bardzo dobrze jest gdy wynosi około 30 - 100%, pozwala to uniknąć przedwczesnego wyjścia z trybu optymalizacji podczas krótkotrwałych wzrostów.

hbase.lru.cache.heavy.eviction.overhead.współczynnik — określa, jak szybko chcielibyśmy uzyskać wynik. Jeśli wiemy na pewno, że nasze odczyty są przeważnie długie i nie chcemy czekać, możemy zwiększyć ten współczynnik i szybciej uzyskać wysoką wydajność.

Na przykład ustawiamy ten współczynnik = 0.01. Oznacza to, że Narzut (patrz wyżej) zostanie pomnożony przez tę liczbę i wynikowy wynik, a procent bloków w pamięci podręcznej zostanie zmniejszony. Załóżmy, że Overhead = 300% i współczynnik = 0.01, wówczas procent bloków buforowanych zostanie zmniejszony o 3%.

Podobna logika „przeciwciśnienia” jest również zaimplementowana dla ujemnych wartości narzutu (przekroczenia). Ponieważ zawsze możliwe są krótkotrwałe wahania wolumenu odczytów i eksmisji, mechanizm ten pozwala uniknąć przedwczesnego wyjścia z trybu optymalizacji. Przeciwciśnienie ma odwróconą logikę: im silniejsze przekroczenie, tym więcej bloków jest buforowanych.

Jak zwiększyć prędkość odczytu z HBase do 3 razy i z HDFS do 5 razy

Kod implementacyjny

        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;
       }

Spójrzmy teraz na to wszystko na prawdziwym przykładzie. Mamy następujący skrypt testowy:

  1. Zacznijmy skanować (25 wątków, partia = 100)
  2. Po 5 minutach dodaj multi-gets (25 wątków, partia = 100)
  3. Po 5 minutach wyłącz multi-get (znowu pozostaje tylko skanowanie)

Wykonujemy dwa uruchomienia, najpierw hbase.lru.cache.heavy.eviction.count.limit = 10000 (co faktycznie wyłącza tę funkcję), a następnie ustawiamy limit = 0 (włącza ją).

W poniższych logach widzimy, jak funkcja jest włączona i resetuje Overshooting do 14-71%. Od czasu do czasu obciążenie maleje, co powoduje włączenie Back Pressure i HBase ponownie buforuje więcej bloków.

Zaloguj serwer regionu
eksmitowany (MB): 0, współczynnik 0.0, narzut (%): -100, licznik ciężkich eksmisji: 0, bieżący blok danych buforowania DataBlock (%): 100
eksmitowany (MB): 0, współczynnik 0.0, narzut (%): -100, licznik ciężkich eksmisji: 0, bieżący blok danych buforowania DataBlock (%): 100
eksmitowany (MB): 2170, współczynnik 1.09, narzut (%): 985, licznik ciężkich eksmisji: 1, bieżące buforowanie DataBlock (%): 91 < start
eksmitowany (MB): 3763, współczynnik 1.08, narzut (%): 1781, licznik ciężkich eksmisji: 2, bieżące buforowanie DataBlock (%): 76
eksmitowany (MB): 3306, współczynnik 1.07, narzut (%): 1553, licznik ciężkich eksmisji: 3, bieżące buforowanie DataBlock (%): 61
eksmitowany (MB): 2508, współczynnik 1.06, narzut (%): 1154, licznik ciężkich eksmisji: 4, bieżące buforowanie DataBlock (%): 50
eksmitowany (MB): 1824, współczynnik 1.04, narzut (%): 812, licznik ciężkich eksmisji: 5, bieżące buforowanie DataBlock (%): 42
eksmitowany (MB): 1482, współczynnik 1.03, narzut (%): 641, licznik ciężkich eksmisji: 6, bieżące buforowanie DataBlock (%): 36
eksmitowany (MB): 1140, współczynnik 1.01, narzut (%): 470, licznik ciężkich eksmisji: 7, bieżące buforowanie DataBlock (%): 32
eksmitowany (MB): 913, współczynnik 1.0, narzut (%): 356, licznik ciężkich eksmisji: 8, bieżące buforowanie DataBlock (%): 29
eksmitowany (MB): 912, współczynnik 0.89, narzut (%): 356, licznik ciężkich eksmisji: 9, bieżące buforowanie DataBlock (%): 26
eksmitowany (MB): 684, współczynnik 0.76, narzut (%): 242, licznik ciężkich eksmisji: 10, bieżące buforowanie DataBlock (%): 24
eksmitowany (MB): 684, współczynnik 0.61, narzut (%): 242, licznik ciężkich eksmisji: 11, bieżące buforowanie DataBlock (%): 22
eksmitowany (MB): 456, współczynnik 0.51, narzut (%): 128, licznik ciężkich eksmisji: 12, bieżące buforowanie DataBlock (%): 21
eksmitowany (MB): 456, współczynnik 0.42, narzut (%): 128, licznik ciężkich eksmisji: 13, bieżące buforowanie DataBlock (%): 20
eksmitowany (MB): 456, współczynnik 0.33, narzut (%): 128, licznik ciężkich eksmisji: 14, bieżące buforowanie DataBlock (%): 19
eksmitowany (MB): 342, współczynnik 0.33, narzut (%): 71, licznik ciężkich eksmisji: 15, bieżące buforowanie DataBlock (%): 19
eksmitowany (MB): 342, współczynnik 0.32, narzut (%): 71, licznik ciężkich eksmisji: 16, bieżące buforowanie DataBlock (%): 19
eksmitowany (MB): 342, współczynnik 0.31, narzut (%): 71, licznik ciężkich eksmisji: 17, bieżące buforowanie DataBlock (%): 19
eksmitowany (MB): 228, współczynnik 0.3, narzut (%): 14, licznik ciężkich eksmisji: 18, bieżące buforowanie DataBlock (%): 19
eksmitowany (MB): 228, współczynnik 0.29, narzut (%): 14, licznik ciężkich eksmisji: 19, bieżące buforowanie DataBlock (%): 19
eksmitowany (MB): 228, współczynnik 0.27, narzut (%): 14, licznik ciężkich eksmisji: 20, bieżące buforowanie DataBlock (%): 19
eksmitowany (MB): 228, współczynnik 0.25, narzut (%): 14, licznik ciężkich eksmisji: 21, bieżące buforowanie DataBlock (%): 19
eksmitowany (MB): 228, współczynnik 0.24, narzut (%): 14, licznik ciężkich eksmisji: 22, bieżące buforowanie DataBlock (%): 19
eksmitowany (MB): 228, współczynnik 0.22, narzut (%): 14, licznik ciężkich eksmisji: 23, bieżące buforowanie DataBlock (%): 19
eksmitowany (MB): 228, współczynnik 0.21, narzut (%): 14, licznik ciężkich eksmisji: 24, bieżące buforowanie DataBlock (%): 19
eksmitowany (MB): 228, współczynnik 0.2, narzut (%): 14, licznik ciężkich eksmisji: 25, bieżące buforowanie DataBlock (%): 19
eksmitowany (MB): 228, współczynnik 0.17, narzut (%): 14, licznik ciężkich eksmisji: 26, bieżące buforowanie DataBlock (%): 19
eksmitowany (MB): 456, współczynnik 0.17, narzut (%): 128, licznik ciężkich eksmisji: 27, bieżące buforowanie DataBlock (%): 18 < dodane pobiera (ale tabela jest taka sama)
eksmitowany (MB): 456, współczynnik 0.15, narzut (%): 128, licznik ciężkich eksmisji: 28, bieżące buforowanie DataBlock (%): 17
eksmitowany (MB): 342, współczynnik 0.13, narzut (%): 71, licznik ciężkich eksmisji: 29, bieżące buforowanie DataBlock (%): 17
eksmitowany (MB): 342, współczynnik 0.11, narzut (%): 71, licznik ciężkich eksmisji: 30, bieżące buforowanie DataBlock (%): 17
eksmitowany (MB): 342, współczynnik 0.09, narzut (%): 71, licznik ciężkich eksmisji: 31, bieżące buforowanie DataBlock (%): 17
eksmitowany (MB): 228, współczynnik 0.08, narzut (%): 14, licznik ciężkich eksmisji: 32, bieżące buforowanie DataBlock (%): 17
eksmitowany (MB): 228, współczynnik 0.07, narzut (%): 14, licznik ciężkich eksmisji: 33, bieżące buforowanie DataBlock (%): 17
eksmitowany (MB): 228, współczynnik 0.06, narzut (%): 14, licznik ciężkich eksmisji: 34, bieżące buforowanie DataBlock (%): 17
eksmitowany (MB): 228, współczynnik 0.05, narzut (%): 14, licznik ciężkich eksmisji: 35, bieżące buforowanie DataBlock (%): 17
eksmitowany (MB): 228, współczynnik 0.05, narzut (%): 14, licznik ciężkich eksmisji: 36, bieżące buforowanie DataBlock (%): 17
eksmitowany (MB): 228, współczynnik 0.04, narzut (%): 14, licznik ciężkich eksmisji: 37, bieżące buforowanie DataBlock (%): 17
eksmitowany (MB): 109, współczynnik 0.04, narzut (%): -46, licznik ciężkich eksmisji: 37, bieżące buforowanie DataBlock (%): 22 < przeciwciśnienie
eksmitowany (MB): 798, współczynnik 0.24, narzut (%): 299, licznik ciężkich eksmisji: 38, bieżące buforowanie DataBlock (%): 20
eksmitowany (MB): 798, współczynnik 0.29, narzut (%): 299, licznik ciężkich eksmisji: 39, bieżące buforowanie DataBlock (%): 18
eksmitowany (MB): 570, współczynnik 0.27, narzut (%): 185, licznik ciężkich eksmisji: 40, bieżące buforowanie DataBlock (%): 17
eksmitowany (MB): 456, współczynnik 0.22, narzut (%): 128, licznik ciężkich eksmisji: 41, bieżące buforowanie DataBlock (%): 16
eksmitowany (MB): 342, współczynnik 0.16, narzut (%): 71, licznik ciężkich eksmisji: 42, bieżące buforowanie DataBlock (%): 16
eksmitowany (MB): 342, współczynnik 0.11, narzut (%): 71, licznik ciężkich eksmisji: 43, bieżące buforowanie DataBlock (%): 16
eksmitowany (MB): 228, współczynnik 0.09, narzut (%): 14, licznik ciężkich eksmisji: 44, bieżące buforowanie DataBlock (%): 16
eksmitowany (MB): 228, współczynnik 0.07, narzut (%): 14, licznik ciężkich eksmisji: 45, bieżące buforowanie DataBlock (%): 16
eksmitowany (MB): 228, współczynnik 0.05, narzut (%): 14, licznik ciężkich eksmisji: 46, bieżące buforowanie DataBlock (%): 16
eksmitowany (MB): 222, współczynnik 0.04, narzut (%): 11, licznik ciężkich eksmisji: 47, bieżące buforowanie DataBlock (%): 16
eksmitowany (MB): 104, współczynnik 0.03, narzut (%): -48, licznik ciężkich eksmisji: 47, bieżące buforowanie DataBlock (%): 21 < otrzymanie przerwania
eksmitowany (MB): 684, współczynnik 0.2, narzut (%): 242, licznik ciężkich eksmisji: 48, bieżące buforowanie DataBlock (%): 19
eksmitowany (MB): 570, współczynnik 0.23, narzut (%): 185, licznik ciężkich eksmisji: 49, bieżące buforowanie DataBlock (%): 18
eksmitowany (MB): 342, współczynnik 0.22, narzut (%): 71, licznik ciężkich eksmisji: 50, bieżące buforowanie DataBlock (%): 18
eksmitowany (MB): 228, współczynnik 0.21, narzut (%): 14, licznik ciężkich eksmisji: 51, bieżące buforowanie DataBlock (%): 18
eksmitowany (MB): 228, współczynnik 0.2, narzut (%): 14, licznik ciężkich eksmisji: 52, bieżące buforowanie DataBlock (%): 18
eksmitowany (MB): 228, współczynnik 0.18, narzut (%): 14, licznik ciężkich eksmisji: 53, bieżące buforowanie DataBlock (%): 18
eksmitowany (MB): 228, współczynnik 0.16, narzut (%): 14, licznik ciężkich eksmisji: 54, bieżące buforowanie DataBlock (%): 18
eksmitowany (MB): 228, współczynnik 0.14, narzut (%): 14, licznik ciężkich eksmisji: 55, bieżące buforowanie DataBlock (%): 18
eksmitowany (MB): 112, współczynnik 0.14, narzut (%): -44, licznik ciężkich eksmisji: 55, bieżące buforowanie DataBlock (%): 23 < przeciwciśnienie
eksmitowany (MB): 456, współczynnik 0.26, narzut (%): 128, licznik ciężkich eksmisji: 56, bieżące buforowanie DataBlock (%): 22
eksmitowany (MB): 342, współczynnik 0.31, narzut (%): 71, licznik ciężkich eksmisji: 57, bieżące buforowanie DataBlock (%): 22
eksmitowany (MB): 342, współczynnik 0.33, narzut (%): 71, licznik ciężkich eksmisji: 58, bieżące buforowanie DataBlock (%): 22
eksmitowany (MB): 342, współczynnik 0.33, narzut (%): 71, licznik ciężkich eksmisji: 59, bieżące buforowanie DataBlock (%): 22
eksmitowany (MB): 342, współczynnik 0.33, narzut (%): 71, licznik ciężkich eksmisji: 60, bieżące buforowanie DataBlock (%): 22
eksmitowany (MB): 342, współczynnik 0.33, narzut (%): 71, licznik ciężkich eksmisji: 61, bieżące buforowanie DataBlock (%): 22
eksmitowany (MB): 342, współczynnik 0.33, narzut (%): 71, licznik ciężkich eksmisji: 62, bieżące buforowanie DataBlock (%): 22
eksmitowany (MB): 342, współczynnik 0.33, narzut (%): 71, licznik ciężkich eksmisji: 63, bieżące buforowanie DataBlock (%): 22
eksmitowany (MB): 342, współczynnik 0.32, narzut (%): 71, licznik ciężkich eksmisji: 64, bieżące buforowanie DataBlock (%): 22
eksmitowany (MB): 342, współczynnik 0.33, narzut (%): 71, licznik ciężkich eksmisji: 65, bieżące buforowanie DataBlock (%): 22
eksmitowany (MB): 342, współczynnik 0.33, narzut (%): 71, licznik ciężkich eksmisji: 66, bieżące buforowanie DataBlock (%): 22
eksmitowany (MB): 342, współczynnik 0.32, narzut (%): 71, licznik ciężkich eksmisji: 67, bieżące buforowanie DataBlock (%): 22
eksmitowany (MB): 342, współczynnik 0.33, narzut (%): 71, licznik ciężkich eksmisji: 68, bieżące buforowanie DataBlock (%): 22
eksmitowany (MB): 342, współczynnik 0.32, narzut (%): 71, licznik ciężkich eksmisji: 69, bieżące buforowanie DataBlock (%): 22
eksmitowany (MB): 342, współczynnik 0.32, narzut (%): 71, licznik ciężkich eksmisji: 70, bieżące buforowanie DataBlock (%): 22
eksmitowany (MB): 342, współczynnik 0.33, narzut (%): 71, licznik ciężkich eksmisji: 71, bieżące buforowanie DataBlock (%): 22
eksmitowany (MB): 342, współczynnik 0.33, narzut (%): 71, licznik ciężkich eksmisji: 72, bieżące buforowanie DataBlock (%): 22
eksmitowany (MB): 342, współczynnik 0.33, narzut (%): 71, licznik ciężkich eksmisji: 73, bieżące buforowanie DataBlock (%): 22
eksmitowany (MB): 342, współczynnik 0.33, narzut (%): 71, licznik ciężkich eksmisji: 74, bieżące buforowanie DataBlock (%): 22
eksmitowany (MB): 342, współczynnik 0.33, narzut (%): 71, licznik ciężkich eksmisji: 75, bieżące buforowanie DataBlock (%): 22
eksmitowany (MB): 342, współczynnik 0.33, narzut (%): 71, licznik ciężkich eksmisji: 76, bieżące buforowanie DataBlock (%): 22
eksmitowany (MB): 21, współczynnik 0.33, narzut (%): -90, licznik ciężkich eksmisji: 76, bieżący blok danych buforowania DataBlock (%): 32
eksmitowany (MB): 0, współczynnik 0.0, narzut (%): -100, licznik ciężkich eksmisji: 0, bieżący blok danych buforowania DataBlock (%): 100
eksmitowany (MB): 0, współczynnik 0.0, narzut (%): -100, licznik ciężkich eksmisji: 0, bieżący blok danych buforowania DataBlock (%): 100

Skany były potrzebne, aby pokazać ten sam proces w formie wykresu relacji pomiędzy dwiema sekcjami pamięci podręcznej - pojedynczą (gdzie bloki, o które nigdy wcześniej nie prosino) i wieloma (tutaj przechowywane są dane „żądane” przynajmniej raz):

Jak zwiększyć prędkość odczytu z HBase do 3 razy i z HDFS do 5 razy

I na koniec jak wygląda działanie parametrów w formie wykresu. Dla porównania na początku całkowicie wyłączono cache, następnie uruchomiono HBase z buforowaniem i opóźnieniem rozpoczęcia prac optymalizacyjnych o 5 minut (30 cykli eksmisji).

Pełny kod można znaleźć w żądaniu ściągnięcia HBASE 23887 na githubie.

Jednak 300 tysięcy odczytów na sekundę to nie wszystko, co można osiągnąć na tym sprzęcie w tych warunkach. Faktem jest, że gdy potrzebny jest dostęp do danych poprzez HDFS, wykorzystywany jest mechanizm ShortCircuitCache (zwany dalej SSC), który pozwala na bezpośredni dostęp do danych, unikając interakcji sieciowych.

Profilowanie pokazało, że choć mechanizm ten daje duże korzyści, to w pewnym momencie staje się też wąskim gardłem, gdyż prawie wszystkie ciężkie operacje zachodzą wewnątrz zamka, co w większości przypadków prowadzi do zablokowania.

Jak zwiększyć prędkość odczytu z HBase do 3 razy i z HDFS do 5 razy

Uświadomiwszy sobie to, zdaliśmy sobie sprawę, że problem można obejść, tworząc szereg niezależnych SSC:

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

A następnie pracuj z nimi, wykluczając przecięcia także przy ostatniej cyfrze przesunięcia:

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

Teraz możesz rozpocząć testowanie. W tym celu będziemy czytać pliki z HDFS za pomocą prostej aplikacji wielowątkowej. Ustaw parametry:

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

I po prostu przeczytaj pliki:

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);
}

Kod ten jest wykonywany w oddzielnych wątkach i zwiększamy liczbę jednocześnie czytanych plików (z 10 do 200 - oś pozioma) oraz liczbę pamięci podręcznych (od 1 do 10 - grafika). Oś pionowa pokazuje przyspieszenie wynikające ze wzrostu SSC w stosunku do przypadku, gdy jest tylko jedna pamięć podręczna.

Jak zwiększyć prędkość odczytu z HBase do 3 razy i z HDFS do 5 razy

Jak czytać wykres: Czas wykonania 100 tysięcy odczytów w blokach 64 KB z jedną pamięcią podręczną wynosi 78 sekund. Natomiast przy 5 skrytkach zajmuje to 16 sekund. Te. następuje przyspieszenie ~5 razy. Jak widać na wykresie, efekt jest mało zauważalny przy małej liczbie odczytów równoległych, zaczyna odgrywać zauważalną rolę, gdy odczytów jest więcej niż 50. Zauważalne jest również, że zwiększenie liczby SSC z 6 i powyżej daje znacznie mniejszy wzrost wydajności.

Uwaga 1: ponieważ wyniki testu są dość zmienne (patrz poniżej), przeprowadzono 3 serie, a uzyskane wartości uśredniono.

Uwaga 2: Wzrost wydajności wynikający z konfiguracji dostępu losowego jest taki sam, chociaż sam dostęp jest nieco wolniejszy.

Należy jednak wyjaśnić, że w przeciwieństwie do HBase, przyspieszenie to nie zawsze jest bezpłatne. Tutaj „odblokowujemy” zdolność procesora do wykonywania większej pracy, zamiast wisieć na zamkach.

Jak zwiększyć prędkość odczytu z HBase do 3 razy i z HDFS do 5 razy

Tutaj można zaobserwować, że ogólnie wzrost liczby pamięci podręcznych powoduje w przybliżeniu proporcjonalny wzrost wykorzystania procesora. Jednak zwycięskich kombinacji jest nieco więcej.

Na przykład przyjrzyjmy się bliżej ustawieniu SSC = 3. Wzrost wydajności w zakresie jest około 3.3 razy. Poniżej znajdują się wyniki wszystkich trzech oddzielnych serii.

Jak zwiększyć prędkość odczytu z HBase do 3 razy i z HDFS do 5 razy

Podczas gdy zużycie procesora wzrasta około 2.8 razy. Różnica nie jest zbyt duża, ale mała Greta jest już szczęśliwa i może mieć czas na chodzenie do szkoły i lekcje.

Będzie to więc miało pozytywny wpływ na każde narzędzie korzystające z masowego dostępu do HDFS (na przykład Spark itp.), pod warunkiem, że kod aplikacji jest lekki (tj. wtyczka jest po stronie klienta HDFS) i jest wolna moc procesora . Aby to sprawdzić, przetestujmy, jaki wpływ będzie miało połączone wykorzystanie optymalizacji BlockCache i strojenia SSC do odczytu z HBase.

Jak zwiększyć prędkość odczytu z HBase do 3 razy i z HDFS do 5 razy

Widać, że w takich warunkach efekt nie jest tak duży, jak w wyrafinowanych testach (odczyt bez jakiejkolwiek obróbki), ale całkiem możliwe jest wyciśnięcie tutaj dodatkowych 80K. Obie optymalizacje łącznie zapewniają do 4-krotnego przyspieszenia.

Opracowano również PR dotyczący tej optymalizacji [HDFS-15202], który został połączony i ta funkcja będzie dostępna w przyszłych wersjach.

Na koniec interesujące było porównanie wydajności odczytu podobnej szerokokolumnowej bazy danych, Cassandry i HBase.

Aby to zrobić, uruchomiliśmy instancje standardowego narzędzia do testowania obciążenia YCSB z dwóch hostów (w sumie 800 wątków). Po stronie serwera - 4 instancje RegionServer i Cassandra na 4 hostach (nie tych, na których działają klienci, aby uniknąć ich wpływu). Odczyty pochodzą z tabel wielkości:

HBase – 300 GB na HDFS (100 GB czystych danych)

Cassandra – 250 GB (współczynnik replikacji = 3)

Te. objętość była w przybliżeniu taka sama (w HBase trochę więcej).

Parametry HBase:

dfs.client.short.circuit.num = 5 (optymalizacja klienta HDFS)

hbase.lru.cache.heavy.eviction.count.limit = 30 - oznacza to, że patch zacznie działać po 30 eksmisjach (~5 minut)

hbase.lru.cache.heavy.eviction.mb.size.limit = 300 — docelowa wielkość buforowania i eksmisji

Dzienniki YCSB zostały przeanalizowane i skompilowane w wykresy Excel:

Jak zwiększyć prędkość odczytu z HBase do 3 razy i z HDFS do 5 razy

Jak widać, te optymalizacje pozwalają porównać wydajność tych baz danych w tych warunkach i osiągnąć 450 tysięcy odczytów na sekundę.

Mamy nadzieję, że te informacje będą komuś przydatne podczas ekscytującej walki o produktywność.

Źródło: www.habr.com

Dodaj komentarz