Det här gästinlägget från Intels Java-prestandaarkitekt Eric Kaczmarek (ursprungligen publicerad här) utforskar hur man ställer in Java garbage collection (GC) för Apache HBase med fokus på 100 % YCSB-läsning.
Apache HBase är ett Apache-projekt med öppen källkod som erbjuder NoSQL-datalagring. HBase används ofta tillsammans med HDFS och används flitigt över hela världen. Välkända användare inkluderar Facebook, Twitter, Yahoo och mer. Ur utvecklarens perspektiv är HBase en "distribuerad, versionerad, icke-relationell databas modellerad efter Googles Bigtable, ett distribuerat lagringssystem för strukturerad data". HBase kan enkelt hantera mycket hög genomströmning genom att antingen skala upp (dvs distribution på en större server) eller skala ut (dvs driftsättning på fler servrar).
Ur en användares synvinkel spelar latensen för varje enskild fråga väldigt stor roll. När vi arbetar med användare för att testa, ställa in och optimera HBase-arbetsbelastningar, möter vi ett betydande antal nu som verkligen vill ha 99:e percentilens driftfördröjningar. Det innebär en tur och retur, från klientens begäran till svaret tillbaka till klienten, allt inom 100 millisekunder.
Flera faktorer bidrar till variation i latens. En av de mest förödande och oförutsägbara latensintrångarna är Java Virtual Machines (JVM:s) "stoppa världen"-pauser för sophämtning (minnesrensning).
För att ta itu med det försökte vi några experiment med Oracle jdk7u21 och jdk7u60 G1 (Garbage 1st) samlare. Serversystemet vi använde var baserat på Intel Xeon Ivy-bridge EP-processorer med Hyper-threading (40 logiska processorer). Den hade 256 GB DDR3-1600 RAM och tre 400 GB SSD:er som lokal lagring. Denna lilla installation innehöll en master och en slav, konfigurerade på en enda nod med belastningen skalad på lämpligt sätt. Vi använde HBase version 0.98.1 och lokalt filsystem för HFile-lagring. HBase-testtabellen konfigurerades som 400 miljoner rader och den var 580 GB stor. Vi använde standard HBase-högstrategin:40% för blockcache, 40% för memstore. YCSB användes för att driva 600 arbetstrådar som skickade förfrågningar till HBase-servern.
Följande diagram visar att jdk7u21 körs 100 % avläsning i en timme med -XX:+UseG1GC -Xms100g -Xmx100g -XX:MaxGCPauseMillis=100
. Vi specificerade sopsamlaren som skulle användas, högstorleken och önskad sopsamling (GC) "stoppa världen" paustid.
Figur 1:Vilda svängningar i GC-paustid
I det här fallet fick vi vilt svängande GC-pauser. GC-pausen hade ett intervall från 7 millisekunder till 5 hela sekunder efter en första topp som nådde så hög som 17,5 sekunder.
Följande tabell visar mer detaljer under steady state:
Figur 2:GC-pausdetaljer, under steady state
Figur 2 visar oss att GC-pauserna faktiskt finns i tre olika grupper:(1) mellan 1 till 1,5 sekunder; (2) mellan 0,007 sekunder till 0,5 sekunder; (3) toppar mellan 1,5 sekunder till 5 sekunder. Detta var väldigt konstigt, så vi testade den senast släppta jdk7u60 för att se om data skulle vara annorlunda:
Vi körde samma 100 % lästester med exakt samma JVM-parametrar:-XX:+UseG1GC -Xms100g -Xmx100g -XX:MaxGCPauseMillis=100
.
Figur 3:Avsevärt förbättrad hantering av paustidstoppar
Jdk7u60 förbättrade avsevärt G1:s förmåga att hantera paustidstoppar efter initial spike under avvecklingsstadiet. Jdk7u60 gjorde 1029 unga och blandade GC under en timmes körning. GC inträffade ungefär var 3,5:e sekund. Jdk7u21 gjorde 286 GC med varje GC som inträffade ungefär var 12,6:e sekund. Jdk7u60 kunde hantera paustiden mellan 0,302 och 1 sekund utan större toppar.
Figur 4 nedan ger oss en närmare titt på 150 GC-pauser under steady state:
Figur 4:Bättre, men inte tillräckligt bra
Under steady state kunde jdk7u60 hålla den genomsnittliga paustiden runt 369 millisekunder. Det var mycket bättre än jdk7u21, men det uppfyllde fortfarande inte vårt krav på 100 millisekunder från –Xx:MaxGCPauseMillis=100
.
För att avgöra vad vi mer kunde göra för att få vår paustid på 100 miljoner sekunder, behövde vi förstå mer om beteendet hos JVM:s minneshantering och G1 (Garbage First) sophämtare. Följande figurer visar hur G1 fungerar på Young Gen-kollektionen.
Figur 5:Bild från JavaOne-presentationen 2012 av Charlie Hunt och Monica Beckwith:"G1 Garbage Collector Performance Tuning"
När JVM startar, baserat på JVM-startparametrarna, ber den operativsystemet att allokera en stor kontinuerlig minnesbit för att vara värd för JVM:s hög. Den minnesbiten är uppdelad av JVM i regioner.
Figur 6:Bild från JavaOne-presentationen 2012 av Charlie Hunt och Monica Beckwith:"G1 Garbage Collector Performance Tuning"
Som figur 6 visar kommer varje objekt som Java-programmet allokerar med Java API först till Eden-utrymmet i den unga generationen till vänster. Efter ett tag blir Eden fullt, och en Young generation GC utlöses. Objekt som fortfarande refereras till (dvs "levande") kopieras till Survivor-utrymmet. När föremål överlever flera GCs i den unga generationen, befordras de till den gamla generationens utrymme.
När Young GC inträffar stoppas Java-applikationens trådar för att säkert markera och kopiera levande objekt. Dessa stopp är de ökända "stoppa-världen" GC-pauserna, som gör att applikationerna inte svarar tills pauserna är över.
Figur 7:Bild från JavaOne-presentationen 2012 av Charlie Hunt och Monica Beckwith:"G1 Garbage Collector Performance Tuning"
Den gamla generationen kan också bli trångt. På en viss nivå – kontrollerad av -XX:InitiatingHeapOccupancyPercent=?
där standard är 45 % av den totala högen – en blandad GC utlöses. Den samlar både Young gen och Old gen. De blandade GC-pauserna styrs av hur lång tid det tar för Young gen att städa upp när blandad GC inträffar.
Så vi kan se i G1, "stoppa världen" GC-pauserna domineras av hur snabbt G1 kan markera och kopiera levande objekt ut ur Eden-rymden. Med detta i åtanke kommer vi att analysera hur HBase-minnestilldelningsmönstret kommer att hjälpa oss att ställa in G1 GC för att få våra 100 millisekunders önskade paus.
I HBase finns det två minnesstrukturer som förbrukar det mesta av dess hög:BlockCache
, cachning av HBase-filblock för läsoperationer och Memstore cachar de senaste uppdateringarna.
Figur 8:I HBase förbrukar två minnesstrukturer det mesta av dess hög.
Standardimplementeringen av HBases BlockCache
är LruBlockCache
, som helt enkelt använder en stor byte-array för att vara värd för alla HBase-block. När block "vräknas" tas hänvisningen till det blockets Java-objekt bort, vilket gör att GC kan flytta minnet.
Nya objekt som bildar LruBlockCache
och Memstore
gå först till den unga generationens Eden-utrymme. Om de lever tillräckligt länge (dvs. om de inte vräkts från LruBlockCache
eller spolas ut ur Memstore), och efter flera unga generationer av GC:er tar de sig till den gamla generationen av Java-högen. När den gamla generationens lediga utrymme är mindre än en given threshOld
(InitiatingHeapOccupancyPercent
till att börja med), blandade GC kickar in och rensar ut några döda föremål i den gamla generationen, kopierar levande föremål från den unga genen och räknar om den unga generationens Eden och den gamla generationens HeapOccupancyPercent
. Så småningom, när HeapOccupancyPercent
når en viss nivå, en FULL GC
händer, vilket gör att enorma "stoppa världen" GC pausar för att rensa upp alla döda föremål inuti den gamla generationen.
Efter att ha studerat GC-loggen producerad av "-XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:+PrintAdaptiveSizePolicy
", märkte vi HeapOccupancyPercent
växte sig aldrig tillräckligt stor för att inducera en fullständig GC under HBase 100 % avläsning. GC-pauserna vi såg dominerades av den unga generationens "stoppa världen"-pauser och den ökande referensbearbetningen över tiden.
Efter att ha slutfört den analysen gjorde vi tre grupper av ändringar i standardinställningen för G1 GC:
- Använd
-XX:+ParallelRefProcEnabled
När denna flagga är aktiverad använder GC flera trådar för att bearbeta de ökande referenserna under Young och mixed GC. Med denna flagga för HBase reduceras GC-anmärkningstiden med 75 % och den totala GC-paustiden minskas med 30 %. Set -XX:-ResizePLAB and -XX:ParallelGCThreads=8+(logical processors-8)(5/8)
Promotion Local Allocation Buffers (PLAB) används under Young Collection. Flera trådar används. Varje tråd kan behöva allokera utrymme för objekt som kopieras antingen i Survivor eller Old space. PLAB krävs för att undvika konkurrens av trådar för delade datastrukturer som hanterar ledigt minne. Varje GC-tråd har en PLAB för överlevnadsutrymme och en för gammalt utrymme. Vi skulle vilja sluta ändra storlek på PLAB för att undvika den stora kommunikationskostnaden mellan GC-trådar, såväl som variationer under varje GC. Vi skulle vilja fixa antalet GC-trådar till storleken som beräknas av 8+ (logiska processorer-8)( 5/8). Denna formel rekommenderades nyligen av Oracle. Med båda inställningarna kan vi se mjukare GC-pauser under löpningen.- Ändra
-XX:G1NewSizePercent
standard från 5 till 1 för 100 GB heapBaserat på utdata från-XX:+PrintGCDetails and -XX:+PrintAdaptiveSizePolicy
märkte vi att orsaken till att G1 inte lyckades uppfylla vår önskade paustid på 100GC var den tid det tog att bearbeta Eden. Med andra ord tog G1 i genomsnitt 369 millisekunder att tömma 5 GB Eden under våra tester. Vi ändrade sedan Eden-storleken med-XX:G1NewSizePercent=
flagga från 5 ner till 1. Med denna ändring såg vi GC-paustiden minskad till 100 millisekunder.
Från det här experimentet fick vi reda på att G1:s hastighet för att rengöra Eden är cirka 1 GB per 100 millisekunder, eller 10 GB per sekund för HBase-installationen som vi använde.
Baserat på den hastigheten kan vi ställa in -XX:G1NewSizePercent=
så Eden-storleken kan hållas runt 1 GB. Till exempel:
- 32 GB hög,
-XX:G1NewSizePercent=3
- 64 GB hög, –
XX:G1NewSizePercent=2
- 100 GB och mer hög,
-XX:G1NewSizePercent=1
- Så våra sista kommandoradsalternativ för HRegionservern är:
-XX:+UseG1GC
-Xms100g -Xmx100g
(Högstorlek används i våra tester)-XX:MaxGCPauseMillis=100
(Önskad GC-paustid i tester)- –
XX:+ParallelRefProcEnabled
-XX:-ResizePLAB
-XX:ParallelGCThreads= 8+(40-8)(5/8)=28
-XX:G1NewSizePercent=1
Här är GC paustidsdiagram för att köra 100 % läsfunktion i 1 timme:
Figur 9:De högsta initiala sedimenteringstopparna reducerades med mer än hälften.
I det här diagrammet reducerades även de högsta initiala sedimenteringstopparna från 3,792 sekunder till 1,684 sekunder. De flesta initiala topparna var mindre än 1 sekund. Efter uppgörelsen kunde GC hålla paustiden runt 100 millisekunder.
Tabellen nedan jämför jdk7u60-körningar med och utan trimning, under steady state:
Figur 10:jdk7u60 körs med och utan trimning, under steady state.
Den enkla GC-inställningen vi beskrev ovan ger idealiska GC-paustider, cirka 100 millisekunder, med i genomsnitt 106 millisekunder och 7 millisekunders standardavvikelse.
Sammanfattning
HBase är en svarstidskritisk applikation som kräver GC-paustid för att vara förutsägbar och hanterbar. Med Oracle jdk7u60, baserat på GC-informationen som rapporteras av -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:+PrintAdaptiveSizePolicy
, vi kan ställa in GC-paustiden till våra önskade 100 millisekunder.
Eric Kaczmarek är en Java-prestandaarkitekt i Intels Software Solution Group. Han leder arbetet på Intel för att möjliggöra och optimera Big Data-ramverk (Hadoop, HBase, Spark, Cassandra) för Intel-plattformar.
Programvara och arbetsbelastningar som används i prestandatester kan ha optimerats för prestanda endast på Intels mikroprocessorer. Prestandatester, såsom SYSmark och MobileMark, mäts med hjälp av specifika datorsystem, komponenter, mjukvara, operationer och funktioner. Varje förändring av någon av dessa faktorer kan göra att resultaten varierar. Du bör konsultera annan information och prestandatester för att hjälpa dig att fullständigt utvärdera dina övervägda köp, inklusive prestandan för den produkten i kombination med andra produkter.
Intel-processornummer är inte ett mått på prestanda. Processornummer skiljer på funktioner inom varje processorfamilj. Inte över olika processorfamiljer. Gå till:http://www.intel.com/products/processor_number.
Copyright 2014 Intel Corp. Intel, Intels logotyp och Xeon är varumärken som tillhör Intel Corporation i USA och/eller andra länder.