Esta postagem de convidado do arquiteto de desempenho Intel Java Eric Kaczmarek (originalmente publicada aqui) explora como ajustar a coleta de lixo (GC) Java para Apache HBase com foco em leituras 100% YCSB.
Apache HBase é um projeto de código aberto Apache que oferece armazenamento de dados NoSQL. Frequentemente usado em conjunto com o HDFS, o HBase é amplamente utilizado em todo o mundo. Usuários conhecidos incluem Facebook, Twitter, Yahoo e muito mais. Do ponto de vista do desenvolvedor, o HBase é um “banco de dados distribuído, versionado e não relacional, modelado após o Bigtable do Google, um sistema de armazenamento distribuído para dados estruturados”. O HBase pode lidar facilmente com uma taxa de transferência muito alta por meio de expansão (ou seja, implantação em um servidor maior) ou expansão (ou seja, implantação em mais servidores).
Do ponto de vista do usuário, a latência para cada consulta é muito importante. À medida que trabalhamos com os usuários para testar, ajustar e otimizar as cargas de trabalho do HBase, encontramos um número significativo agora que realmente deseja latências de operação de 99º percentil. Isso significa uma viagem de ida e volta, da solicitação do cliente à resposta de volta ao cliente, tudo em 100 milissegundos.
Vários fatores contribuem para a variação na latência. Um dos invasores de latência mais devastadores e imprevisíveis são as pausas de “parar o mundo” da Java Virtual Machine (JVM) para coleta de lixo (limpeza de memória).
Para resolver isso, tentamos alguns experimentos usando Oracle jdk7u21 e jdk7u60 G1 (Garbage 1st). O sistema de servidor que usamos foi baseado em processadores Intel Xeon Ivy-bridge EP com Hyper-threading (40 processadores lógicos). Ele tinha 256 GB de RAM DDR3-1600 e três SSDs de 400 GB como armazenamento local. Essa pequena configuração continha um mestre e um escravo, configurados em um único nó com a carga dimensionada adequadamente. Usamos HBase versão 0.98.1 e sistema de arquivos local para armazenamento HFile. A tabela de teste do HBase foi configurada como 400 milhões de linhas e tinha 580 GB de tamanho. Usamos a estratégia de heap padrão do HBase:40% para blockcache, 40% para memstore. O YCSB foi usado para direcionar 600 threads de trabalho enviando solicitações para o servidor HBase.
Os gráficos a seguir mostram jdk7u21 executando 100% de leitura por uma hora usando
-XX:+UseG1GC -Xms100g -Xmx100g -XX:MaxGCPauseMillis=100
. Especificamos o coletor de lixo a ser usado, o tamanho do heap e o tempo de pausa da coleta de lixo (GC) desejado para “parar o mundo”.
Figura 1:oscilações bruscas no tempo de pausa do GC
Neste caso, tivemos pausas de GC descontroladamente oscilantes. A pausa do GC teve um intervalo de 7 milissegundos a 5 segundos completos após um pico inicial que atingiu 17,5 segundos.
O gráfico a seguir mostra mais detalhes, durante o estado estacionário:
Figura 2:detalhes da pausa do GC, durante o estado estável
A Figura 2 nos diz que as pausas do GC, na verdade, vêm em três grupos diferentes:(1) entre 1 a 1,5 segundos; (2) entre 0,007 segundos a 0,5 segundos; (3) picos entre 1,5 segundos a 5 segundos. Isso foi muito estranho, então testamos o jdk7u60 lançado mais recentemente para ver se os dados seriam diferentes:
Executamos os mesmos testes de leitura de 100% usando exatamente os mesmos parâmetros da JVM:-XX:+UseG1GC -Xms100g -Xmx100g -XX:MaxGCPauseMillis=100
.
Figura 3:manipulação bastante aprimorada de picos de tempo de pausa
O Jdk7u60 melhorou muito a capacidade do G1 de lidar com picos de tempo de pausa após o pico inicial durante o estágio de estabilização. Jdk7u60 fez 1029 GCs jovens e mistos durante uma hora de corrida. GC aconteceu a cada 3,5 segundos. Jdk7u21 fez 286 GCs com cada GC acontecendo a cada 12,6 segundos. Jdk7u60 foi capaz de gerenciar o tempo de pausa entre 0,302 a 1 segundo sem grandes picos.
A Figura 4, abaixo, nos dá uma visão mais detalhada das pausas de 150 GC durante o estado estacionário:
Figura 4:melhor, mas não o suficiente
Durante o estado estacionário, o jdk7u60 conseguiu manter o tempo médio de pausa em torno de 369 milissegundos. Era muito melhor que jdk7u21, mas ainda não atendia ao nosso requisito de 100 milissegundos fornecido por –Xx:MaxGCPauseMillis=100
.
Para determinar o que mais poderíamos fazer para obter nosso tempo de pausa de 100 milhões de segundos, precisávamos entender mais sobre o comportamento do gerenciamento de memória da JVM e do coletor de lixo G1 (Garbage First). As figuras a seguir mostram como o G1 funciona na coleção Young Gen.
Figura 5:Slide da apresentação JavaOne de 2012 de Charlie Hunt e Monica Beckwith:“G1 Garbage Collector Performance Tuning”
Quando a JVM é iniciada, com base nos parâmetros de inicialização da JVM, ela solicita que o sistema operacional aloque um grande bloco de memória contínuo para hospedar o heap da JVM. Esse fragmento de memória é particionado pela JVM em regiões.
Figura 6:Slide da apresentação JavaOne de 2012 de Charlie Hunt e Monica Beckwith:“G1 Garbage Collector Performance Tuning”
Como mostra a Figura 6, cada objeto que o programa Java aloca usando a API Java chega primeiro ao espaço Eden na geração Young à esquerda. Depois de um tempo, o Éden fica cheio e um GC da geração jovem é acionado. Objetos que ainda são referenciados (ou seja, “vivos”) são copiados para o espaço Survivor. Quando os objetos sobrevivem a vários GCs na geração Young, eles são promovidos ao espaço da geração Old.
Quando o Young GC acontece, os encadeamentos do aplicativo Java são interrompidos para marcar e copiar objetos ativos com segurança. Essas paradas são as notórias pausas do GC “stop-the-world”, que fazem com que os aplicativos não respondam até que as pausas terminem.
Figura 7:Slide da apresentação JavaOne de 2012 de Charlie Hunt e Monica Beckwith:“G1 Garbage Collector Performance Tuning”
A velha geração também pode ficar lotada. Em um determinado nível, controlado por -XX:InitiatingHeapOccupancyPercent=?
onde o padrão é 45% do heap total—um GC misto é acionado. Ele coleta tanto a geração jovem quanto a geração antiga. As pausas de GC mistas são controladas por quanto tempo o Young gen leva para limpar quando ocorre GC mista.
Assim, podemos ver em G1, as pausas de GC “pare o mundo” são dominadas pela rapidez com que G1 pode marcar e copiar objetos vivos do espaço do Éden. Com isso em mente, analisaremos como o padrão de alocação de memória do HBase nos ajudará a ajustar o G1 GC para obter a pausa desejada de 100 milissegundos.
No HBase, existem duas estruturas na memória que consomem a maior parte de seu heap:O BlockCache
, armazenar em cache blocos de arquivo HBase para operações de leitura e o Memstore armazenando em cache as atualizações mais recentes.
Figura 8:no HBase, duas estruturas na memória consomem a maior parte de seu heap.
A implementação padrão do BlockCache
do HBase é o LruBlockCache
, que simplesmente usa uma grande matriz de bytes para hospedar todos os blocos HBase. Quando os blocos são “despejados”, a referência ao objeto Java daquele bloco é removida, permitindo que o GC realoque a memória.
Novos objetos formando o LruBlockCache
e Memstore
vá primeiro para o espaço do Éden da geração jovem. Se eles viverem o suficiente (ou seja, se não forem despejados de LruBlockCache
ou liberados do Memstore), depois de várias gerações jovens de GCs, eles chegam à geração Antiga do heap Java. Quando o espaço livre da geração antiga é menor que um determinado threshOld
(InitiatingHeapOccupancyPercent
para começar), o GC misto entra em ação e limpa alguns objetos mortos na geração antiga, copia objetos vivos da geração jovem e recalcula o Éden da geração jovem e o HeapOccupancyPercent
da geração antiga . Eventualmente, quando HeapOccupancyPercent
atinge um certo nível, um FULL GC
acontece, o que faz grandes pausas de GC “pare o mundo” para limpar todos os objetos mortos dentro da geração antiga.
Depois de estudar o log do GC produzido por “-XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:+PrintAdaptiveSizePolicy
“, notamos HeapOccupancyPercent
nunca cresceu o suficiente para induzir um GC completo durante a leitura do HBase 100%. As pausas de GC que vimos foram dominadas pelas pausas “pare o mundo” da Young gen e o crescente processamento de referência ao longo do tempo.
Ao concluir essa análise, fizemos três grupos de alterações na configuração padrão do G1 GC:
- Use
-XX:+ParallelRefProcEnabled
Quando esse sinalizador está ativado, o GC usa vários encadeamentos para processar as referências crescentes durante o GC jovem e misto. Com esse sinalizador para HBase, o tempo de remarcação do GC é reduzido em 75% e o tempo geral de pausa do GC é reduzido em 30%. Set -XX:-ResizePLAB and -XX:ParallelGCThreads=8+(logical processors-8)(5/8)
Os Tampões de Alocação Local de Promoção (PLABs) são usados durante a coleta Young. Vários segmentos são usados. Cada thread pode precisar alocar espaço para objetos que estão sendo copiados no espaço Survivor ou Old. As PLABs são necessárias para evitar a competição de threads por estruturas de dados compartilhadas que gerenciam a memória livre. Cada thread GC tem um PLAB para o espaço de sobrevivência e um para o espaço antigo. Gostaríamos de parar de redimensionar PLABs para evitar o grande custo de comunicação entre os threads do GC, bem como variações durante cada GC. Gostaríamos de corrigir o número de threads do GC para ser o tamanho calculado por 8+(processadores lógicos-8)( 5/8). Essa fórmula foi recentemente recomendada pela Oracle. Com ambas as configurações, podemos ver pausas de GC mais suaves durante a execução.- Alterar
-XX:G1NewSizePercent
padrão de 5 a 1 para heap de 100 GBBaseado na saída de-XX:+PrintGCDetails and -XX:+PrintAdaptiveSizePolicy
, notamos que o motivo da falha do G1 em atender ao tempo de pausa desejado de 100GC foi o tempo que levou para processar o Eden. Em outras palavras, o G1 levou em média 369 milissegundos para esvaziar 5 GB do Eden durante nossos testes. Em seguida, alteramos o tamanho do Éden usando-XX:G1NewSizePercent=
sinalizador de 5 para 1. Com essa alteração, vimos o tempo de pausa do GC reduzido para 100 milissegundos.
A partir desse experimento, descobrimos que a velocidade do G1 para limpar o Eden é de cerca de 1 GB por 100 milissegundos, ou 10 GB por segundo para a configuração do HBase que usamos.
Com base nessa velocidade, podemos definir
-XX:G1NewSizePercent=
então o tamanho do Éden pode ser mantido em torno de 1 GB. Por exemplo:- heap de 32 GB,
-XX:G1NewSizePercent=3
- Heap de 64 GB, –
XX:G1NewSizePercent=2
- Heap de 100 GB e acima,
-XX:G1NewSizePercent=1
- As nossas opções finais de linha de comando para o HRegionserver são:
-XX:+UseG1GC
-Xms100g -Xmx100g
(Tamanho do heap usado em nossos testes)-XX:MaxGCPauseMillis=100
(Tempo de pausa do GC desejado nos testes)- –
XX:+ParallelRefProcEnabled
-XX:-ResizePLAB
-XX:ParallelGCThreads= 8+(40-8)(5/8)=28
-XX:G1NewSizePercent=1
Aqui está o gráfico de tempo de pausa do GC para executar a operação de leitura de 100% por 1 hora:
Figura 9:os maiores picos iniciais de acomodação foram reduzidos em mais da metade.
Neste gráfico, mesmo os maiores picos iniciais de acomodação foram reduzidos de 3,792 segundos para 1,684 segundos. Os picos mais iniciais foram inferiores a 1 segundo. Após a liquidação, o GC conseguiu manter o tempo de pausa em torno de 100 milissegundos.
O gráfico abaixo compara execuções do jdk7u60 com e sem ajuste, durante o estado estável:
Figura 10:jdk7u60 é executado com e sem ajuste, durante o estado estável.
O ajuste simples do GC que descrevemos acima fornece tempos ideais de pausa do GC, em torno de 100 milissegundos, com média de 106 milissegundos e desvio padrão de 7 milissegundos.
Resumo
O HBase é um aplicativo de tempo de resposta crítico que exige que o tempo de pausa do GC seja previsível e gerenciável. Com Oracle jdk7u60, com base nas informações de GC relatadas por
-XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:+PrintAdaptiveSizePolicy
, podemos ajustar o tempo de pausa do GC para os 100 milissegundos desejados. Eric Kaczmarek é arquiteto de desempenho Java no Grupo de Soluções de Software da Intel. Ele lidera o esforço na Intel para habilitar e otimizar estruturas de Big Data (Hadoop, HBase, Spark, Cassandra) para plataformas Intel.
O software e as cargas de trabalho usados nos testes de desempenho podem ter sido otimizados para desempenho apenas em microprocessadores Intel. Testes de desempenho, como SYSmark e MobileMark, são medidos usando sistemas de computador, componentes, software, operações e funções específicos. Qualquer alteração em qualquer um desses fatores pode fazer com que os resultados variem. Você deve consultar outras informações e testes de desempenho para auxiliá-lo na avaliação completa de suas compras contempladas, incluindo o desempenho desse produto quando combinado com outros produtos.
Os números dos processadores Intel não são uma medida de desempenho. Os números do processador diferenciam os recursos de cada família de processadores. Não em diferentes famílias de processadores. Acesse:http://www.intel.com/products/processor_number.
Copyright 2014 Intel Corp. Intel, o logotipo Intel e Xeon são marcas registradas da Intel Corporation nos EUA e/ou em outros países.