A nagy teljesítmény az egyik kulcsfontosságú követelmény a nagy adatokkal végzett munka során. A Sberbank adatbetöltési osztályán szinte minden tranzakciót a Hadoop-alapú adatfelhőnkbe pumpálunk, és ezért igazán nagy információáramlással foglalkozunk. Természetesen mindig keressük a lehetőségeket a teljesítmény javítására, most pedig szeretnénk elmondani, hogyan sikerült a RegionServer HBase és a HDFS kliens foltozása, aminek köszönhetően jelentősen meg tudtuk növelni az olvasási műveletek sebességét.
Mielőtt azonban rátérnénk a fejlesztések lényegére, érdemes beszélni azokról a korlátozásokról, amelyeket elvileg nem lehet megkerülni, ha HDD-n ülünk.
Miért nem kompatibilis a HDD és a gyors Random Access olvasás?
Mint tudják, a HBase és sok más adatbázis több tíz kilobájt méretű blokkokban tárolja az adatokat. Alapértelmezés szerint körülbelül 64 KB. Most képzeljük el, hogy csak 100 bájtot kell kapnunk, és megkérjük a HBase-t, hogy adja meg ezeket az adatokat egy bizonyos kulccsal. Mivel a HFiles blokkmérete 64 KB, a kérés 640-szer nagyobb lesz (csak egy perccel!) a szükségesnél.
Következő, mivel a kérés a HDFS-en és a metaadat-gyorsítótárazási mechanizmuson keresztül megy át ShortCircuitCache (ami lehetővé teszi a fájlok közvetlen elérését), ez már 1 MB kiolvasásához vezet a lemezről. Ez azonban a paraméterrel állítható dfs.client.read.shortcircuit.buffer.size és sok esetben van értelme ezt az értéket csökkenteni, például 126 KB-ra.
Tegyük fel, hogy ezt tesszük, de ezen felül, amikor elkezdünk adatokat olvasni a java API-n keresztül, például a FileChannel.read függvényeket, és megkérjük az operációs rendszert, hogy olvassa be a megadott mennyiségű adatot, akkor kétszer többet olvas „csak abban az esetben” , azaz esetünkben 2 KB. Ennek az az oka, hogy a java nem rendelkezik egyszerű módszerrel a FADV_RANDOM jelző beállítására, hogy megakadályozza ezt a viselkedést.
Ennek eredményeként, hogy megkapjuk a 100 bájtunkat, 2600-szor több olvasható a motorháztető alatt. Úgy tűnik, a megoldás kézenfekvő, csökkentsük a blokk méretét kilobyte-ra, állítsuk be az említett zászlót, és kapjunk nagy megvilágosodási gyorsulást. De az a baj, hogy a blokk méretének 2-szeres csökkentésével az időegységenként kiolvasott bájtok számát is 2-szeresére csökkentjük.
Némi haszon a FADV_RANDOM jelző beállításával érhető el, de csak magas többszálú és 128 KB-os blokkmérettel, de ez legfeljebb néhány tíz százalék:
A teszteket 100 fájlon végezték el, mindegyik 1 GB méretű és 10 HDD-n található.
Számítsuk ki, mire számíthatunk elvileg ennél a sebességnél:
Tegyük fel, hogy 10 lemezről olvasunk 280 MB/sec sebességgel, pl. 3 milliószor 100 bájt. De mint emlékszünk, a szükséges adat 2600-szor kevesebb, mint amit olvasunk. Így 3 milliót elosztunk 2600-zal, és megkapjuk 1100 rekord másodpercenként.
Nyomasztó, nem? Ilyen a természet Véletlen hozzáférés hozzáférés a HDD-n lévő adatokhoz – a blokk méretétől függetlenül. Ez a véletlen hozzáférés fizikai határa, és ilyen feltételek mellett egyetlen adatbázis sem tud többet kipréselni.
Hogyan érhetnek el sokkal nagyobb sebességet az adatbázisok? A kérdés megválaszolásához nézzük meg, mi történik a következő képen:
Itt azt látjuk, hogy az első percekben a sebesség valóban körülbelül ezer rekord/másodperc. Azonban, mivel sokkal többet olvasnak, mint amennyit kértek, az adatok az operációs rendszer (linux) buffjában/cache-jében végzik, és a sebesség tisztességesebb másodpercenként 60 ezerre nő.
Így a továbbiakban csak az operációs rendszer gyorsítótárában vagy a hasonló sebességű SSD/NVMe tárolóeszközökön található adatokhoz való hozzáférés felgyorsításával fogunk foglalkozni.
Esetünkben 4 szerverből álló padon végezzük el a teszteket, amelyek mindegyike az alábbiak szerint kerül felszámolásra:
CPU: Xeon E5-2680 v4 @ 2.40 GHz, 64 szál.
Memória: 730 GB.
java verzió: 1.8.0_111
És itt a kulcsfontosságú pont a táblázatokban található adatok mennyisége, amelyet el kell olvasni. A helyzet az, hogy ha egy olyan táblából olvassa be az adatokat, amely teljes egészében a HBase gyorsítótárban van, akkor az nem is fog kiolvasni az operációs rendszer buff/cache-éből. Mivel a HBase alapértelmezés szerint a memória 40%-át a BlockCache nevű struktúrához rendeli. Lényegében ez egy ConcurrentHashMap, ahol a kulcs a fájlnév + a blokk eltolása, az érték pedig a tényleges adat ennél az eltolásnál.
Így ha csak ebből a szerkezetből olvasunk, mi
Például esetünkben a BlockCache mennyisége egy RS-en körülbelül 12 GB. Egy csomóponton két RS-t landoltunk, i.e. 96 GB van lefoglalva a BlockCache számára az összes csomóponton. És sokszor több adat van, például legyen 4 tábla, egyenként 130 régió, amiben a fájlok 800 MB méretűek, FAST_DIFF-el tömörítve, pl. összesen 410 GB (ez tiszta adat, vagyis a replikációs tényező figyelembevétele nélkül).
Így a BlockCache a teljes adatmennyiségnek csak körülbelül 23%-át teszi ki, és ez sokkal közelebb áll az úgynevezett BigData valós feltételeihez. És itt kezdődik a móka – mert nyilvánvalóan minél kevesebb a gyorsítótár találata, annál rosszabb a teljesítmény. Hiszen ha hiányzik, rengeteget kell dolgoznia – pl. menjen le a rendszerfunkciók hívásához. Ezt azonban nem lehet elkerülni, ezért nézzünk egy teljesen más szempontot – mi történik a gyorsítótáron belüli adatokkal?
Egyszerűsítsük le a helyzetet, és tegyük fel, hogy van egy gyorsítótárunk, amely csak 1 objektumra fér el. Íme egy példa arra, hogy mi fog történni, ha a gyorsítótárnál háromszor nagyobb adatmennyiséggel próbálunk dolgozni, a következőket kell tenni:
1. Helyezze az 1. blokkot a gyorsítótárba
2. Távolítsa el az 1. blokkot a gyorsítótárból
3. Helyezze az 2. blokkot a gyorsítótárba
4. Távolítsa el az 2. blokkot a gyorsítótárból
5. Helyezze az 3. blokkot a gyorsítótárba
5 akció befejeződött! Ez a helyzet azonban nem nevezhető normálisnak, sőt, egy csomó teljesen haszontalan munkára kényszerítjük a HBase-t. Folyamatosan beolvassa az adatokat az operációs rendszer gyorsítótárából, elhelyezi a BlockCache-ben, de szinte azonnal kidobja, mert új adatrész érkezett. A poszt elején található animáció megmutatja a probléma lényegét - a Garbage Collector leáll a méretéről, a légkör felforrósodik, a kis Gréta a távoli és forró Svédországban ideges lesz. És mi informatikusok nagyon nem szeretjük, ha a gyerekek szomorúak, ezért elkezdünk gondolkodni azon, hogy mit tehetünk ellene.
Mi van, ha nem az összes blokkot helyezi a gyorsítótárba, hanem csak egy bizonyos százalékát, hogy a gyorsítótár ne csorduljon túl? Kezdjük azzal, hogy egyszerűen adjunk hozzá néhány kódsort az adatok BlockCache-be helyezéséhez szükséges függvény elejéhez:
public void cacheBlock(BlockCacheKey cacheKey, Cacheable buf, boolean inMemory) {
if (cacheDataBlockPercent != 100 && buf.getBlockType().isData()) {
if (cacheKey.getOffset() % 100 >= cacheDataBlockPercent) {
return;
}
}
...
A lényeg itt a következő: az offset a blokk helyzete a fájlban, és utolsó számjegyei véletlenszerűen és egyenletesen vannak elosztva 00 és 99 között. Ezért csak azokat hagyjuk ki, amelyek a szükséges tartományba esnek.
Például állítsa be a cacheDataBlockPercent = 20 értéket, és nézze meg, mi történik:
Az eredmény nyilvánvaló. Az alábbi grafikonokon világossá válik, hogy miért történt ilyen gyorsulás – rengeteg GC-erőforrást takarítunk meg anélkül, hogy elvégeznénk a sziszifuszi munkát, hogy a gyorsítótárba helyezzük az adatokat, csak hogy azonnal a marsi kutyák csatornájába dobjuk:
Ugyanakkor a CPU kihasználtsága nő, de jóval kisebb, mint a termelékenység:
Azt is érdemes megjegyezni, hogy a BlockCache-ben tárolt blokkok eltérőek. A legtöbb, körülbelül 95%-a maga az adat. A többi pedig metaadatok, például Bloom-szűrők vagy LEAF_INDEX és
Ezért a kódban egy ellenőrzési feltételt látunk buf.getBlockType().isData() és ennek a metának köszönhetően mindenképpen a gyorsítótárban hagyjuk.
Most növeljük a terhelést, és enyhén erősítsük meg a funkciót egy mozdulattal. Az első tesztben 20-at tettünk ki, és a BlockCache kissé alul volt kihasználva. Most állítsuk 23%-ra, és 100 percenként adjunk hozzá 5 szálat, hogy lássuk, mikor következik be a telítettség:
Itt azt látjuk, hogy az eredeti verzió szinte azonnal eléri a plafont körülbelül 100 ezer kérés/másodpercnél. Míg a patch akár 300 ezres gyorsulást ad. Ugyanakkor jól látható, hogy a további gyorsítás már nem annyira „ingyen”, a CPU kihasználtsága is növekszik.
Ez azonban nem túl elegáns megoldás, hiszen nem tudjuk előre, hogy a blokkok hány százalékát kell gyorsítótárazni, ez a terhelési profiltól függ. Ezért egy olyan mechanizmust vezettek be, amely automatikusan beállítja ezt a paramétert az olvasási műveletek aktivitásától függően.
Három lehetőség került hozzáadásra ennek szabályozására:
hbase.lru.cache.heavy.eviction.count.limit — beállítja, hogy az adatok gyorsítótárból való kiürítésének folyamata hányszor fusson le, mielőtt elkezdjük az optimalizálást (vagyis a blokkok átugrását). Alapértelmezés szerint ez egyenlő a MAX_INT = 2147483647 értékkel, és valójában azt jelenti, hogy a szolgáltatás soha nem fog működni ezzel az értékkel. Mivel a kilakoltatási folyamat 5-10 másodpercenként indul (a terheléstől függ), és 2147483647 * 10 / 60 / 60 / 24 / 365 = 680 év. Ezt a paramétert azonban 0-ra állíthatjuk, és azonnal működésbe hozhatjuk a funkciót az indítás után.
Ebben a paraméterben azonban van egy hasznos terhelés is. Ha a terhelésünk olyan, hogy a rövid távú (mondjuk nappal) és a hosszú távú (éjszakai) olvasások állandóan szóba kerülnek, akkor biztos lehetünk benne, hogy a funkció csak hosszú olvasási műveletek esetén kapcsol be.
Például tudjuk, hogy a rövid távú leolvasások általában körülbelül 1 percig tartanak. Nem kell elkezdeni a blokkokat kidobni, a gyorsítótárnak nem lesz ideje elavulni, és akkor ezt a paramétert beállíthatjuk például 10-re. Ez azt eredményezi, hogy az optimalizálás csak akkor fog működni, ha hosszú terminus aktív olvasás megkezdődött, i.e. 100 másodperc alatt. Így, ha van egy rövid távú olvasásunk, akkor minden blokk a gyorsítótárba kerül, és elérhető lesz (kivéve azokat, amelyeket a szabványos algoritmus kiürít). És amikor hosszú távú olvasást végzünk, a funkció be van kapcsolva, és sokkal nagyobb teljesítményt érnénk el.
hbase.lru.cache.heavy.eviction.mb.size.limit — beállítja, hogy 10 másodperc alatt hány megabájtot szeretnénk elhelyezni a gyorsítótárban (és természetesen kidobni). A funkció megpróbálja elérni ezt az értéket és fenntartani. A lényeg a következő: ha gigabájtokat tolunk a gyorsítótárba, akkor gigabájtokat kell kilakoltatnunk, és ez, mint fentebb láttuk, nagyon drága. Azonban ne próbálja meg túl kicsire állítani, mert ez a blokkhagyás mód idő előtti kilépését okozza. Erőteljes szervereknél (kb. 20-40 fizikai mag) az optimális körülbelül 300-400 MB beállítása. Középosztálynak (~10 mag) 200-300 MB. Gyenge rendszereknél (2-5 mag) 50-100 MB lehet normális (ezeken nem tesztelték).
Nézzük meg, hogyan működik ez: tegyük fel, hogy beállítjuk a hbase.lru.cache.heavy.eviction.mb.size.limit = 500-at, van valami terhelés (olvasás), majd ~10 másodpercenként kiszámoljuk, hogy hány bájt volt kilakoltatás a következő képlet alapján:
Overhead = felszabadított bájtok összege (MB) * 100 / korlát (MB) - 100;
Ha valóban 2000 MB-ot kilakoltattak volna, akkor a rezsi egyenlő:
2000 * 100 / 500 - 100 = 300%
Az algoritmusok legfeljebb néhány tíz százalékot próbálnak fenntartani, így a funkció csökkenti a gyorsítótárazott blokkok százalékos arányát, ezáltal egy automatikus hangolási mechanizmust valósít meg.
Ha azonban csökken a terhelés, mondjuk csak 200 MB kerül kiürítésre, és az Overhead negatív lesz (ún. túllövés):
200 * 100 / 500 - 100 = -60%
Éppen ellenkezőleg, a szolgáltatás növeli a gyorsítótárazott blokkok százalékos arányát, amíg az Overhead pozitív lesz.
Az alábbiakban egy példa látható, hogyan néz ki ez valós adatokon. Nem kell megpróbálni elérni a 0%-ot, ez lehetetlen. Nagyon jó, ha ez körülbelül 30-100%, ez segít elkerülni az optimalizálási módból való idő előtti kilépést rövid távú túlfeszültség esetén.
hbase.lru.cache.heavy.eviction.overhead.coefficient — beállítja, hogy milyen gyorsan szeretnénk elérni az eredményt. Ha biztosan tudjuk, hogy olvasmányaink többnyire hosszúak, és nem akarunk várni, akkor ezt az arányt növelhetjük, és gyorsabban érhetjük el a nagy teljesítményt.
Például ezt az együtthatót 0.01-re állítjuk. Ez azt jelenti, hogy az általános költségeket (lásd fent) megszorozzuk ezzel a számmal a kapott eredménnyel, és a gyorsítótárazott blokkok százalékos aránya csökken. Tegyük fel, hogy Overhead = 300% és együttható = 0.01, akkor a gyorsítótárazott blokkok százalékos aránya 3%-kal csökken.
Hasonló „Hátnyomás” logika van megvalósítva a negatív Overhead (túllövés) értékek esetében is. Mivel a leolvasások és a kilakoltatások mennyiségének rövid távú ingadozása mindig lehetséges, ez a mechanizmus lehetővé teszi az optimalizálási módból való idő előtti kilépés elkerülését. Az ellennyomásnak fordított logikája van: minél erősebb a túllépés, annál több blokk kerül gyorsítótárba.
Megvalósítási kód
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;
}
Nézzük most mindezt egy valós példa segítségével. A következő tesztszkriptünk van:
- Kezdjük el a beolvasást (25 szál, köteg = 100)
- 5 perc elteltével adjon hozzá multi-geteket (25 szál, köteg = 100)
- 5 perc elteltével kapcsolja ki a multi-geteket (csak a szkennelés marad újra)
Két futtatást végzünk, először a hbase.lru.cache.heavy.eviction.count.limit = 10000 (ami valójában letiltja a szolgáltatást), majd a limit = 0 értéket állítja be (engedélyezi).
Az alábbi naplókban láthatjuk, hogy a funkció hogyan van bekapcsolva, és visszaállítja a túllövést 14-71%-ra. A terhelés időről időre csökken, ami bekapcsolja a Backpressure-t, és a HBase ismét több blokkot gyorsítótáraz.
Log RegionServer
kilakoltatás (MB): 0, arány 0.0, rezsi (%): -100, nehéz kilakoltatási számláló: 0, aktuális gyorsítótár DataBlock (%): 100
kilakoltatás (MB): 0, arány 0.0, rezsi (%): -100, nehéz kilakoltatási számláló: 0, aktuális gyorsítótár DataBlock (%): 100
kilakoltatás (MB): 2170, arány 1.09, rezsi (%): 985, nehéz kilakoltatási számláló: 1, aktuális gyorsítótár DataBlock (%): 91 < start
kilakoltatás (MB): 3763, arány 1.08, rezsi (%): 1781, nehéz kilakoltatási számláló: 2, aktuális gyorsítótár DataBlock (%): 76
kilakoltatás (MB): 3306, arány 1.07, rezsi (%): 1553, nehéz kilakoltatási számláló: 3, aktuális gyorsítótár DataBlock (%): 61
kilakoltatás (MB): 2508, arány 1.06, rezsi (%): 1154, nehéz kilakoltatási számláló: 4, aktuális gyorsítótár DataBlock (%): 50
kilakoltatás (MB): 1824, arány 1.04, rezsi (%): 812, nehéz kilakoltatási számláló: 5, aktuális gyorsítótár DataBlock (%): 42
kilakoltatás (MB): 1482, arány 1.03, rezsi (%): 641, nehéz kilakoltatási számláló: 6, aktuális gyorsítótár DataBlock (%): 36
kilakoltatás (MB): 1140, arány 1.01, rezsi (%): 470, nehéz kilakoltatási számláló: 7, aktuális gyorsítótár DataBlock (%): 32
kilakoltatás (MB): 913, arány 1.0, rezsi (%): 356, nehéz kilakoltatási számláló: 8, aktuális gyorsítótár DataBlock (%): 29
kilakoltatás (MB): 912, arány 0.89, rezsi (%): 356, nehéz kilakoltatási számláló: 9, aktuális gyorsítótár DataBlock (%): 26
kilakoltatás (MB): 684, arány 0.76, rezsi (%): 242, nehéz kilakoltatási számláló: 10, aktuális gyorsítótár DataBlock (%): 24
kilakoltatás (MB): 684, arány 0.61, rezsi (%): 242, nehéz kilakoltatási számláló: 11, aktuális gyorsítótár DataBlock (%): 22
kilakoltatás (MB): 456, arány 0.51, rezsi (%): 128, nehéz kilakoltatási számláló: 12, aktuális gyorsítótár DataBlock (%): 21
kilakoltatás (MB): 456, arány 0.42, rezsi (%): 128, nehéz kilakoltatási számláló: 13, aktuális gyorsítótár DataBlock (%): 20
kilakoltatás (MB): 456, arány 0.33, rezsi (%): 128, nehéz kilakoltatási számláló: 14, aktuális gyorsítótár DataBlock (%): 19
kilakoltatás (MB): 342, arány 0.33, rezsi (%): 71, nehéz kilakoltatási számláló: 15, aktuális gyorsítótár DataBlock (%): 19
kilakoltatás (MB): 342, arány 0.32, rezsi (%): 71, nehéz kilakoltatási számláló: 16, aktuális gyorsítótár DataBlock (%): 19
kilakoltatás (MB): 342, arány 0.31, rezsi (%): 71, nehéz kilakoltatási számláló: 17, aktuális gyorsítótár DataBlock (%): 19
kilakoltatás (MB): 228, arány 0.3, rezsi (%): 14, nehéz kilakoltatási számláló: 18, aktuális gyorsítótár DataBlock (%): 19
kilakoltatás (MB): 228, arány 0.29, rezsi (%): 14, nehéz kilakoltatási számláló: 19, aktuális gyorsítótár DataBlock (%): 19
kilakoltatás (MB): 228, arány 0.27, rezsi (%): 14, nehéz kilakoltatási számláló: 20, aktuális gyorsítótár DataBlock (%): 19
kilakoltatás (MB): 228, arány 0.25, rezsi (%): 14, nehéz kilakoltatási számláló: 21, aktuális gyorsítótár DataBlock (%): 19
kilakoltatás (MB): 228, arány 0.24, rezsi (%): 14, nehéz kilakoltatási számláló: 22, aktuális gyorsítótár DataBlock (%): 19
kilakoltatás (MB): 228, arány 0.22, rezsi (%): 14, nehéz kilakoltatási számláló: 23, aktuális gyorsítótár DataBlock (%): 19
kilakoltatás (MB): 228, arány 0.21, rezsi (%): 14, nehéz kilakoltatási számláló: 24, aktuális gyorsítótár DataBlock (%): 19
kilakoltatás (MB): 228, arány 0.2, rezsi (%): 14, nehéz kilakoltatási számláló: 25, aktuális gyorsítótár DataBlock (%): 19
kilakoltatás (MB): 228, arány 0.17, rezsi (%): 14, nehéz kilakoltatási számláló: 26, aktuális gyorsítótár DataBlock (%): 19
kilakoltatás (MB): 456, arány 0.17, rezsi (%): 128, nehéz kilakoltatási számláló: 27, aktuális gyorsítótár DataBlock (%): 18 < hozzáadott kap (de ugyanaz a táblázat)
kilakoltatás (MB): 456, arány 0.15, rezsi (%): 128, nehéz kilakoltatási számláló: 28, aktuális gyorsítótár DataBlock (%): 17
kilakoltatás (MB): 342, arány 0.13, rezsi (%): 71, nehéz kilakoltatási számláló: 29, aktuális gyorsítótár DataBlock (%): 17
kilakoltatás (MB): 342, arány 0.11, rezsi (%): 71, nehéz kilakoltatási számláló: 30, aktuális gyorsítótár DataBlock (%): 17
kilakoltatás (MB): 342, arány 0.09, rezsi (%): 71, nehéz kilakoltatási számláló: 31, aktuális gyorsítótár DataBlock (%): 17
kilakoltatás (MB): 228, arány 0.08, rezsi (%): 14, nehéz kilakoltatási számláló: 32, aktuális gyorsítótár DataBlock (%): 17
kilakoltatás (MB): 228, arány 0.07, rezsi (%): 14, nehéz kilakoltatási számláló: 33, aktuális gyorsítótár DataBlock (%): 17
kilakoltatás (MB): 228, arány 0.06, rezsi (%): 14, nehéz kilakoltatási számláló: 34, aktuális gyorsítótár DataBlock (%): 17
kilakoltatás (MB): 228, arány 0.05, rezsi (%): 14, nehéz kilakoltatási számláló: 35, aktuális gyorsítótár DataBlock (%): 17
kilakoltatás (MB): 228, arány 0.05, rezsi (%): 14, nehéz kilakoltatási számláló: 36, aktuális gyorsítótár DataBlock (%): 17
kilakoltatás (MB): 228, arány 0.04, rezsi (%): 14, nehéz kilakoltatási számláló: 37, aktuális gyorsítótár DataBlock (%): 17
kilakoltatás (MB): 109, arány 0.04, rezsi (%): -46, nehéz kilakoltatási számláló: 37, aktuális gyorsítótár DataBlock (%): 22 < ellennyomás
kilakoltatás (MB): 798, arány 0.24, rezsi (%): 299, nehéz kilakoltatási számláló: 38, aktuális gyorsítótár DataBlock (%): 20
kilakoltatás (MB): 798, arány 0.29, rezsi (%): 299, nehéz kilakoltatási számláló: 39, aktuális gyorsítótár DataBlock (%): 18
kilakoltatás (MB): 570, arány 0.27, rezsi (%): 185, nehéz kilakoltatási számláló: 40, aktuális gyorsítótár DataBlock (%): 17
kilakoltatás (MB): 456, arány 0.22, rezsi (%): 128, nehéz kilakoltatási számláló: 41, aktuális gyorsítótár DataBlock (%): 16
kilakoltatás (MB): 342, arány 0.16, rezsi (%): 71, nehéz kilakoltatási számláló: 42, aktuális gyorsítótár DataBlock (%): 16
kilakoltatás (MB): 342, arány 0.11, rezsi (%): 71, nehéz kilakoltatási számláló: 43, aktuális gyorsítótár DataBlock (%): 16
kilakoltatás (MB): 228, arány 0.09, rezsi (%): 14, nehéz kilakoltatási számláló: 44, aktuális gyorsítótár DataBlock (%): 16
kilakoltatás (MB): 228, arány 0.07, rezsi (%): 14, nehéz kilakoltatási számláló: 45, aktuális gyorsítótár DataBlock (%): 16
kilakoltatás (MB): 228, arány 0.05, rezsi (%): 14, nehéz kilakoltatási számláló: 46, aktuális gyorsítótár DataBlock (%): 16
kilakoltatás (MB): 222, arány 0.04, rezsi (%): 11, nehéz kilakoltatási számláló: 47, aktuális gyorsítótár DataBlock (%): 16
kilakoltatás (MB): 104, arány 0.03, általános költségek (%): -48, nehéz kilakoltatási számláló: 47, aktuális gyorsítótár DataBlock (%): 21 < megszakítások
kilakoltatás (MB): 684, arány 0.2, rezsi (%): 242, nehéz kilakoltatási számláló: 48, aktuális gyorsítótár DataBlock (%): 19
kilakoltatás (MB): 570, arány 0.23, rezsi (%): 185, nehéz kilakoltatási számláló: 49, aktuális gyorsítótár DataBlock (%): 18
kilakoltatás (MB): 342, arány 0.22, rezsi (%): 71, nehéz kilakoltatási számláló: 50, aktuális gyorsítótár DataBlock (%): 18
kilakoltatás (MB): 228, arány 0.21, rezsi (%): 14, nehéz kilakoltatási számláló: 51, aktuális gyorsítótár DataBlock (%): 18
kilakoltatás (MB): 228, arány 0.2, rezsi (%): 14, nehéz kilakoltatási számláló: 52, aktuális gyorsítótár DataBlock (%): 18
kilakoltatás (MB): 228, arány 0.18, rezsi (%): 14, nehéz kilakoltatási számláló: 53, aktuális gyorsítótár DataBlock (%): 18
kilakoltatás (MB): 228, arány 0.16, rezsi (%): 14, nehéz kilakoltatási számláló: 54, aktuális gyorsítótár DataBlock (%): 18
kilakoltatás (MB): 228, arány 0.14, rezsi (%): 14, nehéz kilakoltatási számláló: 55, aktuális gyorsítótár DataBlock (%): 18
kilakoltatás (MB): 112, arány 0.14, rezsi (%): -44, nehéz kilakoltatási számláló: 55, aktuális gyorsítótár DataBlock (%): 23 < ellennyomás
kilakoltatás (MB): 456, arány 0.26, rezsi (%): 128, nehéz kilakoltatási számláló: 56, aktuális gyorsítótár DataBlock (%): 22
kilakoltatás (MB): 342, arány 0.31, rezsi (%): 71, nehéz kilakoltatási számláló: 57, aktuális gyorsítótár DataBlock (%): 22
kilakoltatás (MB): 342, arány 0.33, rezsi (%): 71, nehéz kilakoltatási számláló: 58, aktuális gyorsítótár DataBlock (%): 22
kilakoltatás (MB): 342, arány 0.33, rezsi (%): 71, nehéz kilakoltatási számláló: 59, aktuális gyorsítótár DataBlock (%): 22
kilakoltatás (MB): 342, arány 0.33, rezsi (%): 71, nehéz kilakoltatási számláló: 60, aktuális gyorsítótár DataBlock (%): 22
kilakoltatás (MB): 342, arány 0.33, rezsi (%): 71, nehéz kilakoltatási számláló: 61, aktuális gyorsítótár DataBlock (%): 22
kilakoltatás (MB): 342, arány 0.33, rezsi (%): 71, nehéz kilakoltatási számláló: 62, aktuális gyorsítótár DataBlock (%): 22
kilakoltatás (MB): 342, arány 0.33, rezsi (%): 71, nehéz kilakoltatási számláló: 63, aktuális gyorsítótár DataBlock (%): 22
kilakoltatás (MB): 342, arány 0.32, rezsi (%): 71, nehéz kilakoltatási számláló: 64, aktuális gyorsítótár DataBlock (%): 22
kilakoltatás (MB): 342, arány 0.33, rezsi (%): 71, nehéz kilakoltatási számláló: 65, aktuális gyorsítótár DataBlock (%): 22
kilakoltatás (MB): 342, arány 0.33, rezsi (%): 71, nehéz kilakoltatási számláló: 66, aktuális gyorsítótár DataBlock (%): 22
kilakoltatás (MB): 342, arány 0.32, rezsi (%): 71, nehéz kilakoltatási számláló: 67, aktuális gyorsítótár DataBlock (%): 22
kilakoltatás (MB): 342, arány 0.33, rezsi (%): 71, nehéz kilakoltatási számláló: 68, aktuális gyorsítótár DataBlock (%): 22
kilakoltatás (MB): 342, arány 0.32, rezsi (%): 71, nehéz kilakoltatási számláló: 69, aktuális gyorsítótár DataBlock (%): 22
kilakoltatás (MB): 342, arány 0.32, rezsi (%): 71, nehéz kilakoltatási számláló: 70, aktuális gyorsítótár DataBlock (%): 22
kilakoltatás (MB): 342, arány 0.33, rezsi (%): 71, nehéz kilakoltatási számláló: 71, aktuális gyorsítótár DataBlock (%): 22
kilakoltatás (MB): 342, arány 0.33, rezsi (%): 71, nehéz kilakoltatási számláló: 72, aktuális gyorsítótár DataBlock (%): 22
kilakoltatás (MB): 342, arány 0.33, rezsi (%): 71, nehéz kilakoltatási számláló: 73, aktuális gyorsítótár DataBlock (%): 22
kilakoltatás (MB): 342, arány 0.33, rezsi (%): 71, nehéz kilakoltatási számláló: 74, aktuális gyorsítótár DataBlock (%): 22
kilakoltatás (MB): 342, arány 0.33, rezsi (%): 71, nehéz kilakoltatási számláló: 75, aktuális gyorsítótár DataBlock (%): 22
kilakoltatás (MB): 342, arány 0.33, rezsi (%): 71, nehéz kilakoltatási számláló: 76, aktuális gyorsítótár DataBlock (%): 22
kilakoltatás (MB): 21, arány 0.33, rezsi (%): -90, nehéz kilakoltatási számláló: 76, aktuális gyorsítótár DataBlock (%): 32
kilakoltatás (MB): 0, arány 0.0, rezsi (%): -100, nehéz kilakoltatási számláló: 0, aktuális gyorsítótár DataBlock (%): 100
kilakoltatás (MB): 0, arány 0.0, rezsi (%): -100, nehéz kilakoltatási számláló: 0, aktuális gyorsítótár DataBlock (%): 100
A vizsgálatokra azért volt szükség, hogy ugyanazt a folyamatot grafikonon mutassák be két gyorsítótár-szakasz közötti kapcsolatról - egy (ahol korábban soha nem kért blokkok) és több (a legalább egyszer „lekért” adatok itt tárolódnak):
És végül, hogyan néz ki a paraméterek működése grafikon formájában. Összehasonlításképpen a gyorsítótárat az elején teljesen kikapcsolták, majd a HBase gyorsítótárazással és 5 perccel késlelteti az optimalizálási munka megkezdését (30 kilakoltatási ciklus).
A teljes kód a Pull Request-ben található
A másodpercenkénti 300 ezer olvasás azonban nem minden, amit ezen a hardveren ilyen körülmények között el lehet érni. A helyzet az, hogy amikor HDFS-en keresztül kell hozzáférnie az adatokhoz, akkor a ShortCircuitCache (a továbbiakban: SSC) mechanizmus kerül alkalmazásra, amely lehetővé teszi az adatok közvetlen elérését, elkerülve a hálózati interakciókat.
A profilozás azt mutatta, hogy bár ez a mechanizmus nagy nyereséget ad, egy ponton szűk keresztmetszetté is válik, mert szinte minden nehéz művelet a záron belül történik, ami legtöbbször blokkoláshoz vezet.
Ennek felismerése után rájöttünk, hogy a probléma megkerülhető független SSC-k tömbjének létrehozásával:
private final ShortCircuitCache[] shortCircuitCache;
...
shortCircuitCache = new ShortCircuitCache[this.clientShortCircuitNum];
for (int i = 0; i < this.clientShortCircuitNum; i++)
this.shortCircuitCache[i] = new ShortCircuitCache(…);
Ezután dolgozzon velük, kivéve a kereszteződéseket az utolsó eltolási számjegynél is:
public ShortCircuitCache getShortCircuitCache(long idx) {
return shortCircuitCache[(int) (idx % clientShortCircuitNum)];
}
Most elkezdheti a tesztelést. Ehhez egy egyszerű többszálas alkalmazással HDFS-ből olvasunk be fájlokat. Állítsa be a paramétereket:
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
És csak olvassa el a fájlokat:
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);
}
Ez a kód külön szálban fut, és növelni fogjuk az egyidejűleg olvasott fájlok számát (10-ről 200-ra - vízszintes tengely) és a gyorsítótárak számát (1-ről 10-re - grafika). A függőleges tengely azt a gyorsulást mutatja, amely az SSC növekedéséből adódik ahhoz az esethez képest, amikor csak egy gyorsítótár van.
A grafikon olvasása: A 100 ezer olvasás végrehajtása 64 KB-os blokkokban egy gyorsítótárral 78 másodpercet vesz igénybe. Míg 5 gyorsítótárral ez 16 másodpercet vesz igénybe. Azok. ~5-szörös gyorsulás van. Amint az a grafikonon is látható, a hatás kevés párhuzamos olvasásnál nem túl észrevehető, 50-nél több szál olvasásakor kezd észrevehető szerepet játszani. Az is észrevehető, hogy az SSC-k számát 6-ról növelik. és a fentiek lényegesen kisebb teljesítménynövekedést adnak.
1. megjegyzés: mivel a teszteredmények meglehetősen ingadozóak (lásd alább), 3 futtatást végeztünk, és a kapott értékeket átlagoltuk.
2. megjegyzés: A véletlen hozzáférés konfigurálásából származó teljesítménynövekedés ugyanaz, bár maga a hozzáférés valamivel lassabb.
Azonban tisztázni kell, hogy a HBase esetével ellentétben ez a gyorsulás nem mindig ingyenes. Itt „feloldjuk” a CPU azon képességét, hogy több munkát végezzen, ahelyett, hogy a zárakon lógna.
Itt megfigyelhető, hogy általában a gyorsítótárak számának növekedése hozzávetőlegesen arányosan növeli a CPU kihasználtságot. Vannak azonban valamivel több nyerő kombináció is.
Például nézzük meg közelebbről az SSC = 3 beállítást. A teljesítmény növekedése a tartományon körülbelül 3.3-szoros. Az alábbiakban mindhárom külön futam eredményeit közöljük.
Miközben a CPU-fogyasztás körülbelül 2.8-szorosára nő. A különbség nem túl nagy, de a kis Gréta már boldog, és lehet, hogy lesz ideje iskolába járni és leckéket venni.
Így ez pozitív hatással lesz minden olyan eszközre, amely tömeges hozzáférést használ a HDFS-hez (például Spark stb.), feltéve, hogy az alkalmazás kódja könnyű (azaz a csatlakozó a HDFS kliens oldalon van), és van szabad CPU-táp. . Az ellenőrzéshez teszteljük, milyen hatással lesz a BlockCache optimalizálás és az SSC hangolás együttes használata a HBase-ről történő olvasáshoz.
Látható, hogy ilyen körülmények között nem akkora a hatás, mint a finomított teszteknél (mindenféle feldolgozás nélkül leolvasva), de itt teljesen ki lehet préselni további 80K-t. A két optimalizálás együtt akár 4-szeres gyorsulást biztosít.
Ehhez az optimalizáláshoz PR is készült
És végül érdekes volt összehasonlítani egy hasonló széles oszlopú adatbázis, a Cassandra és a HBase olvasási teljesítményét.
Ennek érdekében elindítottuk a szabványos YCSB terheléstesztelő segédprogram példányait két gazdagépről (összesen 800 szál). Szerver oldalon – a RegionServer és a Cassandra 4 példánya 4 gazdagépen (nem azokon, ahol a kliensek futnak, hogy elkerüljük a befolyásukat). Az adatok a következő méretű táblázatokból származnak:
HBase – 300 GB HDFS-en (100 GB tiszta adat)
Cassandra - 250 GB (replikációs tényező = 3)
Azok. a hangerő megközelítőleg azonos volt (HBase-ben kicsit több).
HBase paraméterek:
dfs.client.short.circuit.num = 5 (HDFS kliens optimalizálás)
hbase.lru.cache.heavy.eviction.count.limit = 30 - ez azt jelenti, hogy a tapasz 30 kilakoltatás után (~5 perc) kezd működni.
hbase.lru.cache.heavy.eviction.mb.size.limit = 300 — a gyorsítótárazás és a kilakoltatás célmennyisége
Az YCSB naplókat elemezte és Excel grafikonokká fordította:
Amint látja, ezek az optimalizálások lehetővé teszik ezen adatbázisok teljesítményének összehasonlítását ilyen körülmények között, és 450 ezer olvasást érünk el másodpercenként.
Reméljük, hogy ez az információ hasznos lehet valakinek a termelékenységért folytatott izgalmas küzdelem során.
Forrás: will.com