HBase
 sql >> Database >  >> NoSQL >> HBase

Ottimizzazione di Java Garbage Collection per HBase

Questo guest post dell'architetto delle prestazioni Intel Java Eric Kaczmarek (pubblicato originariamente qui) esplora come ottimizzare Java Garbage Collection (GC) per Apache HBase concentrandosi sul 100% di letture YCSB.

Apache HBase è un progetto open source Apache che offre archiviazione dati NoSQL. Spesso utilizzato insieme a HDFS, HBase è ampiamente utilizzato in tutto il mondo. Gli utenti famosi includono Facebook, Twitter, Yahoo e altri. Dal punto di vista dello sviluppatore, HBase è un "database distribuito, versionato e non relazionale modellato su Bigtable di Google, un sistema di archiviazione distribuito per dati strutturati". HBase è in grado di gestire facilmente un throughput molto elevato aumentando (ovvero, distribuzione su un server più grande) o aumentando (ovvero, distribuzione su più server).

Dal punto di vista dell'utente, la latenza per ogni singola query è molto importante. Mentre collaboriamo con gli utenti per testare, ottimizzare e ottimizzare i carichi di lavoro HBase, ora incontriamo un numero significativo di persone che desiderano davvero latenze operative al 99° percentile. Ciò significa un viaggio di andata e ritorno, dalla richiesta del cliente alla risposta al cliente, il tutto entro 100 millisecondi.

Diversi fattori contribuiscono alla variazione della latenza. Uno degli intrusi di latenza più devastanti e imprevedibili sono le pause "stop the world" di Java Virtual Machine (JVM) per la raccolta dei rifiuti (pulizia della memoria).

Per risolvere questo problema, abbiamo provato alcuni esperimenti utilizzando Oracle jdk7u21 e jdk7u60 G1 (Garbage 1st) collector. Il sistema server che abbiamo utilizzato era basato su processori Intel Xeon Ivy-bridge EP con Hyper-threading (40 processori logici). Aveva 256 GB di RAM DDR3-1600 e tre SSD da 400 GB come memoria locale. Questa piccola configurazione conteneva un master e uno slave, configurati su un singolo nodo con il carico adeguatamente ridimensionato. Abbiamo usato HBase versione 0.98.1 e filesystem locale per l'archiviazione HFile. La tabella di test HBase era configurata come 400 milioni di righe e aveva una dimensione di 580 GB. Abbiamo utilizzato la strategia heap HBase predefinita:40% per blockcache, 40% per memstore. YCSB è stato utilizzato per guidare 600 thread di lavoro inviando richieste al server HBase.

I grafici seguenti mostrano jdk7u21 in esecuzione al 100% di lettura per un'ora utilizzando -XX:+UseG1GC -Xms100g -Xmx100g -XX:MaxGCPauseMillis=100 . Abbiamo specificato il Garbage Collector da utilizzare, la dimensione dell'heap e il tempo di pausa desiderato per "fermare il mondo" di Garbage Collection.

Figura 1:oscillazioni selvagge nel tempo di pausa del GC

In questo caso, abbiamo avuto pause GC estremamente oscillanti. La pausa GC aveva un intervallo da 7 millisecondi a 5 secondi interi dopo un picco iniziale che ha raggiunto un massimo di 17,5 secondi.

Il grafico seguente mostra maggiori dettagli, durante lo stato stazionario:

Figura 2:dettagli della pausa GC, durante lo stato stazionario

La Figura 2 ci dice che le pause GC sono effettivamente di tre gruppi diversi:(1) tra 1 e 1,5 secondi; (2) tra 0,007 secondi e 0,5 secondi; (3) picchi tra 1,5 secondi e 5 secondi. Questo è stato molto strano, quindi abbiamo testato l'ultimo jdk7u60 rilasciato per vedere se i dati sarebbero stati diversi:

Abbiamo eseguito gli stessi test di lettura al 100% utilizzando esattamente gli stessi parametri JVM:-XX:+UseG1GC -Xms100g -Xmx100g -XX:MaxGCPauseMillis=100 .

Figura 3:gestione notevolmente migliorata dei picchi di tempo di pausa

Jdk7u60 ha notevolmente migliorato la capacità di G1 di gestire i picchi di tempo di pausa dopo il picco iniziale durante la fase di assestamento. Jdk7u60 ha ottenuto 1029 GC Young e mixed durante un'ora di corsa. GC accadeva circa ogni 3,5 secondi. Jdk7u21 ha realizzato 286 GC con ogni GC che si verifica ogni 12,6 secondi circa. Jdk7u60 è stato in grado di gestire il tempo di pausa compreso tra 0,302 e 1 secondo senza picchi importanti.

La Figura 4, di seguito, ci offre uno sguardo più da vicino a 150 pause GC durante lo stato stazionario:

Figura 4:migliore, ma non abbastanza buona

Durante lo stato stazionario, jdk7u60 è stato in grado di mantenere il tempo di pausa medio di circa 369 millisecondi. Era molto meglio di jdk7u21, ma non soddisfaceva ancora il nostro requisito di 100 millisecondi dato da –Xx:MaxGCPauseMillis=100 .

Per determinare cos'altro potremmo fare per ottenere il nostro tempo di pausa di 100 milioni di secondi, dovevamo capire di più sul comportamento della gestione della memoria della JVM e del Garbage Collector G1 (Garbage First). Le figure seguenti mostrano come funziona G1 sulla collezione Young Gen.

Figura 5:Diapositiva dalla presentazione JavaOne 2012 di Charlie Hunt e Monica Beckwith:"G1 Garbage Collector Performance Tuning"

Quando la JVM si avvia, in base ai parametri di avvio della JVM, chiede al sistema operativo di allocare un grosso blocco di memoria continua per ospitare l'heap della JVM. Quel blocco di memoria è partizionato dalla JVM in regioni.

Figura 6:diapositiva della presentazione JavaOne 2012 di Charlie Hunt e Monica Beckwith:"G1 Garbage Collector Performance Tuning"

Come mostra la Figura 6, ogni oggetto che il programma Java alloca utilizzando l'API Java arriva prima nello spazio Eden nella generazione Young a sinistra. Dopo un po', l'Eden si riempie e viene attivato un GC di giovani generazioni. Gli oggetti a cui si fa ancora riferimento (cioè "vivi") vengono copiati nello spazio dei sopravvissuti. Quando gli oggetti sopravvivono a diversi GC nella generazione dei Giovani, vengono promossi nello spazio della Vecchia generazione.

Quando si verifica Young GC, i thread dell'applicazione Java vengono interrotti per contrassegnare e copiare in modo sicuro oggetti live. Queste interruzioni sono le famigerate pause GC "stop-the-world", che impediscono alle applicazioni di rispondere fino al termine delle pause.

Figura 7:Diapositiva dalla presentazione JavaOne 2012 di Charlie Hunt e Monica Beckwith:"G1 Garbage Collector Performance Tuning"

Anche la vecchia generazione può diventare affollata. A un certo livello, controllato da -XX:InitiatingHeapOccupancyPercent=? dove l'impostazione predefinita è il 45% dell'heap totale:viene attivato un GC misto. Raccoglie sia la Young gen che la Old gen. Le pause del GC misto sono controllate dal tempo impiegato dalla generazione Young per ripulire quando si verifica il GC misto.

Quindi possiamo vedere in G1, le pause GC "ferma il mondo" sono dominate dalla velocità con cui G1 può contrassegnare e copiare oggetti live fuori dallo spazio dell'Eden. Tenendo presente questo, analizzeremo in che modo il modello di allocazione della memoria HBase ci aiuterà a ottimizzare G1 GC per ottenere la pausa desiderata di 100 millisecondi.

In HBase, ci sono due strutture in memoria che consumano la maggior parte del suo heap:BlockCache , memorizzando nella cache i blocchi di file HBase per le operazioni di lettura e il Memstore memorizzando nella cache gli ultimi aggiornamenti.

Figura 8:in HBase, due strutture in memoria consumano la maggior parte dell'heap.

L'implementazione predefinita di BlockCache di HBase è il LruBlockCache , che utilizza semplicemente un array di byte di grandi dimensioni per ospitare tutti i blocchi HBase. Quando i blocchi vengono "sfrattati", il riferimento all'oggetto Java di quel blocco viene rimosso, consentendo al GC di riposizionare la memoria.

Nuovi oggetti che formano la LruBlockCache e Memstore vai prima nello spazio dell'Eden di Young generation. Se vivono abbastanza a lungo (cioè, se non vengono sfrattati da LruBlockCache o spazzati via da Memstore), quindi, dopo diverse generazioni di giovani di GC, si fanno strada verso la vecchia generazione del mucchio di Java. Quando lo spazio libero della vecchia generazione è inferiore a un determinato threshOld (InitiatingHeapOccupancyPercent tanto per cominciare), il GC misto entra in azione ed elimina alcuni oggetti morti nella vecchia generazione, copia gli oggetti vivi dalla generazione giovane e ricalcola l'Eden della giovane generazione e il HeapOccupancyPercent della vecchia generazione . Alla fine, quando HeapOccupancyPercent raggiunge un certo livello, un FULL GC succede, il che fa enormi pause GC "ferma il mondo" per ripulire tutti gli oggetti morti all'interno della vecchia generazione.

Dopo aver studiato il log GC prodotto da “-XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:+PrintAdaptiveSizePolicy ", abbiamo notato HeapOccupancyPercent non è mai cresciuto abbastanza da indurre un GC completo durante la lettura al 100% di HBase. Le pause GC che abbiamo visto sono state dominate dalle pause "stop the world" di Young gen e dall'elaborazione dei riferimenti in aumento nel tempo.

Al termine dell'analisi, abbiamo apportato tre gruppi di modifiche all'impostazione predefinita del GC G1:

  1. Usa -XX:+ParallelRefProcEnabled Quando questo flag è attivato, GC utilizza più thread per elaborare i riferimenti crescenti durante Young e mixed GC. Con questo flag per HBase, il tempo di commento del GC viene ridotto del 75% e il tempo di pausa complessivo del GC viene ridotto del 30%.
  2. Set -XX:-ResizePLAB and -XX:ParallelGCThreads=8+(logical processors-8)(5/8) Durante la raccolta Young vengono utilizzati buffer di allocazione locale (PLAB) di promozione. Vengono utilizzati più thread. Ciascun thread potrebbe dover allocare spazio per gli oggetti copiati in Survivor o Old space. I PLAB sono necessari per evitare la concorrenza dei thread per le strutture di dati condivise che gestiscono la memoria libera. Ogni thread GC ha un PLAB per lo spazio di sopravvivenza e uno per lo spazio vecchio. Vorremmo interrompere il ridimensionamento dei PLAB per evitare l'elevato costo di comunicazione tra i thread GC, nonché variazioni durante ogni GC. Vorremmo fissare il numero di thread GC in modo che fosse la dimensione calcolata da 8+(processori logici-8)( 5/8). Questa formula è stata recentemente consigliata da Oracle. Con entrambe le impostazioni, siamo in grado di vedere pause GC più fluide durante la corsa.
  3. Cambia -XX:G1NewSizePercent valore predefinito da 5 a 1 per heap da 100 GB Basato sull'output di -XX:+PrintGCDetails and -XX:+PrintAdaptiveSizePolicy , abbiamo notato che il motivo per cui G1 non ha rispettato il tempo di pausa di 100 GC desiderato era il tempo impiegato per elaborare Eden. In altre parole, G1 ha impiegato in media 369 millisecondi per svuotare 5 GB di Eden durante i nostri test. Abbiamo quindi modificato la dimensione dell'Eden utilizzando -XX:G1NewSizePercent=
    flag da 5 a 1. Con questa modifica, abbiamo visto il tempo di pausa GC ridotto a 100 millisecondi.

Da questo esperimento, abbiamo scoperto che la velocità di G1 per pulire Eden è di circa 1 GB ogni 100 millisecondi, o 10 GB al secondo per la configurazione HBase che abbiamo utilizzato.

Sulla base di tale velocità, possiamo impostare -XX:G1NewSizePercent=
quindi la dimensione dell'Eden può essere mantenuta intorno a 1 GB. Ad esempio:

  • Heap da 32 GB, -XX:G1NewSizePercent=3
  • 64 GB di heap, –XX:G1NewSizePercent=2
  • 100 GB e oltre heap, -XX:G1NewSizePercent=1
  • Quindi le nostre opzioni finali della riga di comando per HRegionserver sono:
    • -XX:+UseG1GC
    • -Xms100g -Xmx100g (dimensione dell'heap utilizzata nei nostri test)
    • -XX:MaxGCPauseMillis=100 (Tempo di pausa GC desiderato nei test)
    • XX:+ParallelRefProcEnabled
    • -XX:-ResizePLAB
    • -XX:ParallelGCThreads= 8+(40-8)(5/8)=28
    • -XX:G1NewSizePercent=1

Ecco il grafico del tempo di pausa GC per l'esecuzione dell'operazione di lettura al 100% per 1 ora:

Figura 9:i picchi di assestamento iniziali più elevati sono stati ridotti di oltre la metà.

In questo grafico, anche i picchi di assestamento iniziali più alti sono stati ridotti da 3,792 secondi a 1,684 secondi. I picchi più iniziali erano inferiori a 1 secondo. Dopo la transazione, GC è riuscita a mantenere il tempo di pausa di circa 100 millisecondi.

Il grafico seguente confronta jdk7u60 funziona con e senza tuning, durante lo stato stazionario:

Figura 10:jdk7u60 funziona con e senza tuning, durante lo stato stazionario.

La semplice messa a punto del GC che abbiamo descritto sopra offre tempi di pausa GC ideali, circa 100 millisecondi, con una deviazione standard media di 106 millisecondi e 7 millisecondi.

Riepilogo

HBase è un'applicazione critica in termini di tempo di risposta che richiede il tempo di pausa del GC per essere prevedibile e gestibile. Con Oracle jdk7u60, in base alle informazioni GC riportate da -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:+PrintAdaptiveSizePolicy , siamo in grado di regolare il tempo di pausa del GC fino ai 100 millisecondi desiderati.

Eric Kaczmarek è un architetto delle prestazioni Java nel Software Solution Group di Intel. Guida Intel per abilitare e ottimizzare i framework Big Data (Hadoop, HBase, Spark, Cassandra) per le piattaforme Intel.

Il software e i carichi di lavoro utilizzati nei test delle prestazioni potrebbero essere stati ottimizzati per le prestazioni solo sui microprocessori Intel. I test delle prestazioni, come SYSmark e MobileMark, vengono misurati utilizzando sistemi informatici, componenti, software, operazioni e funzioni specifici. Qualsiasi modifica a uno qualsiasi di questi fattori può far variare i risultati. Dovresti consultare altre informazioni e test sulle prestazioni per aiutarti nella valutazione completa dei tuoi acquisti previsti, comprese le prestazioni di quel prodotto quando combinato con altri prodotti.

I numeri dei processori Intel non sono una misura delle prestazioni. I numeri dei processori differenziano le funzionalità all'interno di ciascuna famiglia di processori. Non in diverse famiglie di processori. Vai a:http://www.intel.com/products/processor_number.

Copyright 2014 Intel Corp. Intel, il logo Intel e Xeon sono marchi di Intel Corporation negli Stati Uniti e/o in altri paesi.