Teoria e prática do uso do HBase

Boa tarde Meu nome é Danil Lipova, nossa equipe na Sbertech começou a usar o HBase como repositório de dados operacionais. Durante seu estudo, foi adquirida experiência que eu queria sistematizar e descrever (esperamos que seja útil para muitos). Todas as experiências abaixo foram realizadas com versões do HBase 1.2.0-cdh5.14.2 e 2.0.0-cdh6.0.0-beta1.

  1. Arquitetura geral
  2. Gravando dados no HBASE
  3. Lendo dados do HBASE
  4. Armazenamento em cache de dados
  5. Processamento em lote MultiGet / MultiPut
  6. Estratégia para dividir tabelas em regiões (derramamento)
  7. Tolerância a falhas, compactação e localidade dos dados
  8. Configurações e desempenho
  9. Teste de carga
  10. Conclusões

1. Arquitetura geral



O mestre em espera ouve a pulsação ativa no nó ZooKeeper e, em caso de desaparecimento, assume as funções do mestre.

2. Gravando dados no HBASE


Primeiro, considere o caso mais simples - escrever um objeto de valor-chave em uma determinada tabela usando put (rowkey). O cliente deve primeiro descobrir onde está localizado o servidor da região raiz (RRS) que armazena a tabela hbase: meta. Ele recebe essas informações do ZooKeeper. Em seguida, ele se volta para o RRS e lê a tabela hbase: meta, da qual recupera as informações de que o RegionServer (RS) é responsável por armazenar dados para a chave de linha especificada na tabela de seu interesse. Para uso futuro, a tabela meta é armazenada em cache pelo cliente e, portanto, as chamadas subsequentes são mais rápidas, diretamente para o RS.

Em seguida, o RS, tendo recebido a solicitação, primeiro grava-a no WriteAheadLog (WAL), necessário para a recuperação no caso de uma falha. Em seguida, ele salva os dados no MemStore. Este é um buffer na memória que contém um conjunto classificado de chaves para uma determinada região. A tabela pode ser dividida em regiões (partições), cada uma das quais contém um conjunto de chaves separado. Isso permite a colocação de regiões em diferentes servidores para obter maior desempenho. No entanto, apesar da obviedade dessa afirmação, veremos mais adiante que isso não funciona em todos os casos.

Após colocar o registro no MemStore, o cliente recebe uma resposta de que o registro foi salvo com sucesso. Ao mesmo tempo, ele é realmente armazenado apenas no buffer e chega ao disco somente após um certo período de tempo ou quando é preenchido com novos dados.


Ao executar a operação “Excluir”, a exclusão de dados físicos não ocorre. Eles são simplesmente marcados como excluídos e a própria destruição ocorre quando a principal função compacta é chamada, descrita em mais detalhes na Seção 7.

Os arquivos no formato HFile são acumulados no HDFS e, de tempos em tempos, inicia um pequeno processo compacto, que simplesmente cola arquivos pequenos em arquivos maiores sem excluir nada. Com o tempo, isso se transforma em um problema que se manifesta apenas ao ler dados (retornaremos a isso mais tarde).

Além do processo de inicialização descrito acima, há um procedimento muito mais eficiente, que provavelmente é o lado mais poderoso desse banco de dados - o BulkLoad. Consiste no fato de criarmos HFiles independentemente e colocá-lo no disco, o que nos permite escalar perfeitamente e obter velocidades muito decentes. De fato, a limitação aqui não é o HBase, mas as possibilidades de ferro. Abaixo estão os resultados do carregamento em um cluster composto por 16 RegionServers e 16 NodeManager YARN (CPU Xeon E5-2680 v4 a 2.40GHz * 64 threads), versão HBase 1.2.0-cdh5.14.2.



Pode-se observar que, aumentando o número de partições (regiões) na tabela, bem como os executáveis ​​Spark, obtemos um aumento na velocidade de download. Além disso, a velocidade depende da quantidade de gravação. Blocos grandes proporcionam um aumento na medição de MB / s, pequenos no número de registros inseridos por unidade de tempo, sendo todas as outras coisas iguais.

Você também pode iniciar o carregamento em duas tabelas ao mesmo tempo e obter uma duplicação de velocidade. Pode-se observar abaixo que blocos de 10 KB são gravados em duas tabelas ao mesmo tempo com uma velocidade de cerca de 600 Mb / s cada (total de 1275 Mb / s), o que coincide com a velocidade de gravação de 623 MB / s em uma tabela (consulte o nº 11 acima)


Mas o segundo lançamento com registros de 50 KB mostra que a velocidade do download já está crescendo um pouco, o que indica uma aproximação aos valores limite. Deve-se ter em mente que praticamente não há carga no HBASE, tudo o que é necessário é fornecer primeiro os dados do hbase: meta e, depois de alinhar os HFiles, liberar os dados do BlockCache e salvar o buffer do MemStore em disco, se não houver. vazio.

3. Lendo dados do HBASE


Se assumirmos que todas as informações do hbase: meta já possuem um cliente (consulte a seção 2), a solicitação será enviada imediatamente para o RS onde a chave desejada está armazenada. Primeiro, a pesquisa é feita no MemCache. Independentemente de haver dados lá ou não, a pesquisa também é realizada no buffer do BlockCache e, se necessário, nos HFiles. Se os dados foram encontrados em um arquivo, eles são colocados no BlockCache e retornados mais rapidamente na próxima solicitação. As pesquisas de arquivos H são relativamente rápidas devido ao uso do filtro Bloom, ou seja, Depois de ler uma pequena quantidade de dados, ele determina imediatamente se esse arquivo contém a chave desejada e, se não, segue para a próxima.


Tendo recebido dados dessas três fontes, o RS forma uma resposta. Em particular, ele pode transferir várias versões do objeto encontradas ao mesmo tempo, se o cliente solicitou o controle de versão.

4. Armazenamento em cache de dados


Os buffers MemStore e BlockCache ocupam até 80% da memória RS alocada na pilha (o restante é reservado para tarefas de serviço RS). Se o modo de uso típico for tal que os processos gravem e leiam imediatamente os mesmos dados, faz sentido reduzir o BlockCache e aumentar o MemStore, porque ao gravar dados no cache de leitura, o uso do BlockCache ocorrerá com menos frequência. O buffer do BlockCache consiste em duas partes: LruBlockCache (sempre na pilha) e BucketCache (geralmente fora da pilha ou no SSD). O BucketCache deve ser usado quando houver muitas solicitações de leitura e elas não se ajustam ao LruBlockCache, o que leva ao trabalho ativo do Garbage Collector. Ao mesmo tempo, você não deve esperar um aumento radical no desempenho usando o cache de leitura, mas retornaremos a isso na Seção 8


O BlockCache é um para todo o RS e o MemStore possui um para cada tabela (um para cada família de colunas).

Conforme descrito na teoria, ao gravar dados não cai no cache e, de fato, esses parâmetros CACHE_DATA_ON_WRITE para a tabela e "Cache DATA na Gravação" para RS são configurados como false. No entanto, na prática, se você gravar dados no MemStore, liberá-los para o disco (limpando-os dessa maneira), excluir o arquivo resultante e, em seguida, executando uma solicitação de obtenção, os dados serão recebidos com êxito. E mesmo que você desative completamente o BlockCache e preencha a tabela com novos dados, coloque o MemStore em disco, exclua-os e solicite a partir de outra sessão, eles ainda serão buscados em algum lugar. Portanto, o HBase armazena não apenas dados, mas também quebra-cabeças misteriosos.

hbase(main):001:0> create 'ns:magic', 'cf' Created table ns:magic Took 1.1533 seconds hbase(main):002:0> put 'ns:magic', 'key1', 'cf:c', 'try_to_delete_me' Took 0.2610 seconds hbase(main):003:0> flush 'ns:magic' Took 0.6161 seconds hdfs dfs -mv /data/hbase/data/ns/magic/* /tmp/trash hbase(main):002:0> get 'ns:magic', 'key1' cf:c timestamp=1534440690218, value=try_to_delete_me 

DADOS do cache na leitura está definido como falso. Se você tiver alguma idéia, discuta isso nos comentários.

5. Processamento em lote de dados MultiGet / MultiPut


O processamento de solicitações únicas (Obter / Colocar / Excluir) é uma operação bastante cara, portanto, você deve combiná-las o máximo possível em uma Lista ou Lista, o que permite obter um aumento significativo no desempenho. Isso é especialmente verdade na operação de gravação, mas ao ler, existe a seguinte armadilha. O gráfico abaixo mostra o tempo de leitura de 50.000 registros do MemStore. A leitura foi feita em um fluxo e o eixo horizontal mostra o número de chaves na solicitação. Pode-se observar que, quando você aumenta para mil chaves em uma solicitação, o tempo de execução diminui, ou seja, velocidade aumenta. No entanto, quando o modo MSLAB é ativado por padrão, após esse limite, uma queda drástica no desempenho é iniciada e quanto maior a quantidade de dados no registro, maior o tempo.



Os testes foram realizados em uma máquina virtual, 8 núcleos, versão HBase 2.0.0-cdh6.0.0-beta1.

O modo MSLAB foi projetado para reduzir a fragmentação de heap, que ocorre devido à mistura de dados de nova e velha geração. Como solução para o problema quando o MSLAB está ativado, os dados são colocados em células relativamente pequenas (bloco) e processados ​​em lotes. Como resultado, quando o volume no pacote de dados solicitado excede o tamanho alocado, o desempenho cai acentuadamente. Por outro lado, também não é recomendável desativar esse modo, pois isso leva a paradas devido ao GC durante momentos de trabalho intensivo com dados. Uma boa saída é aumentar o volume da célula, no caso da escrita ativa via put simultaneamente com a leitura. Vale ressaltar que o problema não ocorre se, após a gravação, executar o comando flush que libera o MemStore para o disco ou se o carregamento é realizado usando o BulkLoad. A tabela abaixo mostra que as consultas dos dados do MemStore de um volume maior (e a mesma quantidade) levam a uma desaceleração. No entanto, aumentar o tamanho do chunks retorna ao tempo de processamento normal.


Além de aumentar o tamanho do pedaço, a fragmentação de dados por região ajuda, ou seja, divisão de mesa. Isso leva ao fato de que menos solicitações chegam a cada região e, se forem colocadas em uma célula, a resposta permanece boa.

6. A estratégia de dividir tabelas em regiões (corte)


Como o HBase é um armazenamento de valor-chave e o particionamento é realizado por chave, é extremamente importante compartilhar dados uniformemente em todas as regiões. Por exemplo, particionar essa tabela em três partes resultará na divisão dos dados em três regiões:


Ocorre que isso leva a uma desaceleração acentuada se os dados carregados no futuro parecerem, por exemplo, valores longos, a maioria dos quais começa com o mesmo dígito, por exemplo:

1000001
1000002
...
1100003

Como as chaves são armazenadas como uma matriz de bytes, todas elas serão iniciadas da mesma maneira e pertencerão à mesma região nº 1 que armazena esse intervalo de chaves. Existem várias estratégias de divisão:

HexStringSplit - Transforma a chave em uma string com codificação hexadecimal no intervalo "00000000" => "FFFFFFFF" e a preenche com zeros à esquerda.

UniformSplit - Transforma uma chave em uma matriz de bytes com codificação hexadecimal no intervalo "00" => "FF" e a preenche com zeros à direita.

Além disso, você pode especificar qualquer intervalo ou conjunto de teclas para dividir e configurar a divisão automática. No entanto, uma das abordagens mais simples e eficazes é o UniformSplit e o uso de concatenação de hash, por exemplo, um par alto de bytes executando uma chave através da função CRC32 (rowkey) e a própria keykey:

hash + rowkey

Todos os dados serão distribuídos uniformemente pelas regiões. Ao ler, os dois primeiros bytes são simplesmente descartados e a chave original permanece. O RS também controla a quantidade de dados e chaves na região e, quando os limites são excedidos, os divide automaticamente em pedaços.

7. Tolerância a falhas e localidade dos dados


Como apenas uma região é responsável por cada conjunto de chaves, a solução para os problemas associados a falhas ou descomissionamento do RS é armazenar todos os dados necessários no HDFS. Quando o RS trava, o mestre detecta isso através da ausência de um batimento cardíaco no nó ZooKeeper. Em seguida, ele atribui a região atendida a outro RS e, como os HFiles são armazenados em um sistema de arquivos distribuídos, o novo host os lê e continua a servir os dados. No entanto, como alguns dos dados podem estar no MemStore e não tiveram tempo de entrar nos HFiles, os WALs, que também são armazenados no HDFS, são usados ​​para restaurar o histórico da operação. Após o rollover das alterações, o RS é capaz de responder às solicitações, no entanto, a mudança leva ao fato de que parte dos dados e seus processos estão em nós diferentes, ou seja, localidade diminuída.

A solução para o problema é a compactação principal - esse procedimento move os arquivos para os nós responsáveis ​​por eles (onde suas regiões estão localizadas), como resultado do qual a carga na rede e nos discos aumenta acentuadamente durante esse procedimento. No entanto, no futuro, o acesso aos dados é visivelmente acelerado. Além disso, major_compaction combina todos os HFiles em um arquivo na região e também limpa os dados, dependendo das configurações da tabela. Por exemplo, você pode especificar o número de versões de um objeto que deseja salvar ou sua vida útil, após as quais o objeto é excluído fisicamente.

Este procedimento pode ter um efeito muito positivo no HBase. A imagem abaixo mostra como o desempenho diminuiu como resultado da gravação de dados ativa. Aqui você pode ver como 40 fluxos foram gravados em uma tabela e 40 fluxos leram dados ao mesmo tempo. Os fluxos de gravação formam cada vez mais HFiles, que são lidos por outros fluxos. Como resultado, mais e mais dados precisam ser excluídos da memória e, no final, o GC começa a funcionar, o que praticamente paralisa todo o trabalho. O lançamento da grande compactação levou à limpeza dos bloqueios resultantes e à restauração do desempenho.


O teste foi realizado em 3 DataNode e 4 RS (CPU Xeon E5-2680 v4 @ 2,40GHz * 64 threads). HBase Versão 1.2.0-cdh5.14.2

Vale ressaltar que o lançamento da compactação principal foi realizado em uma tabela "ao vivo", na qual os dados foram ativamente gravados e lidos. Havia uma declaração na rede de que isso poderia levar a uma resposta incorreta ao ler dados. Para verificar, foi iniciado um processo que gerou novos dados e os gravou na tabela. Depois, li e verifiquei imediatamente se o valor obtido coincidia com o que foi registrado. Durante esse processo, a compactação principal foi iniciada cerca de 200 vezes e nenhuma falha foi registrada. Talvez o problema apareça raramente e somente durante alta carga, por isso é mais seguro interromper os processos de gravação e leitura de maneira programada e executar a limpeza sem permitir tais rebaixamentos do GC.

Além disso, a compactação principal não afeta o estado do MemStore. Para liberá-lo para o disco e compactar, é necessário usar flush (connection.getAdmin (). Flush (TableName.valueOf (tblName))).

8. Configurações e desempenho


Como já mencionado, o HBase mostra o maior sucesso onde não precisa fazer nada ao executar o BulkLoad. No entanto, isso se aplica à maioria dos sistemas e pessoas. No entanto, essa ferramenta é mais adequada para o empilhamento em massa de dados em grandes blocos, enquanto que se o processo exigir muitas solicitações concorrentes de leitura e gravação, os comandos Get e Put descritos acima serão usados. Para determinar os parâmetros ideais, foram realizados lançamentos com várias combinações de parâmetros e configurações da tabela:

  • 10 threads foram iniciados ao mesmo tempo 3 vezes seguidas (vamos chamá-lo de um bloco de threads).
  • A média de tempo de operação de todos os fluxos no bloco foi o resultado final da operação do bloco.
  • Todos os threads funcionaram com a mesma tabela.
  • Antes de cada início do bloco de encadeamentos, uma compactação principal era executada.
  • Cada bloco executou apenas uma das seguintes operações:

- Coloque
- obtenha
- Get + Put

  • Cada bloco realizou 50.000 repetições de sua operação.
  • O tamanho do registro no bloco é de 100 bytes, 1000 bytes ou 10000 bytes (aleatório).
  • Os blocos foram lançados com um número diferente de chaves solicitadas (uma chave ou 10).
  • Os blocos foram lançados em várias configurações da tabela. Parâmetros alterados:

- BlockCache = ativado ou desativado
- BlockSize = 65 Kb ou 16 Kb
- Partições = 1, 5 ou 30
- MSLAB = ativado ou desativado

Assim, o bloco fica assim:

a. Modo MSLAB ativado / desativado.
b. Foi criada uma tabela para a qual os seguintes parâmetros foram definidos: BlockCache = true / none, BlockSize = 65/16 Kb, Partições = 1/5/30.
c. Defina a compressão GZ.
d. Foram lançados 10 encadeamentos simultaneamente, executando 1/10 das operações put / get / get + put nesta tabela com registros de 100/1000/10000 bytes, executando 50.000 consultas seguidas (chaves aleatórias).
e O ponto d foi repetido três vezes.
f. O tempo de operação de todos os threads foi medido.

Todas as combinações possíveis foram verificadas. É previsível que, à medida que o tamanho da gravação aumente, a velocidade caia ou a desativação do cache diminua. No entanto, o objetivo foi compreender o grau e a significância da influência de cada parâmetro, portanto, os dados coletados foram alimentados com a entrada da função de regressão linear, o que possibilita avaliar a confiabilidade usando a estatística t. Abaixo estão os resultados dos blocos que executam operações Put. Um conjunto completo de combinações 2 * 2 * 3 * 2 * 3 = 144 opções + 72 desde alguns foram realizados duas vezes. Portanto, um total de 216 lançamentos:


O teste foi realizado em um mini-cluster composto por 3 DataNode e 4 RS (CPU Xeon E5-2680 v4 @ 2.40GHz * 64 fluxos). HBase versão 1.2.0-cdh5.14.2.

A velocidade de inserção mais alta de 3,7 segundos foi obtida quando o modo MSLAB foi desativado, em uma tabela com uma partição, com o BlockCache ativado, BlockSize = 16, registros de 100 bytes de 10 peças por pacote.
A velocidade de inserção mais baixa de 82,8 segundos foi obtida quando o modo MSLAB foi ativado, em uma tabela com uma partição, com o BlockCache ativado, BlockSize = 16, registros de 10.000 bytes cada.

Agora vamos ver o modelo. Vemos um modelo de boa qualidade para o R2, mas é claro que a extrapolação é contra-indicada aqui. O comportamento real do sistema ao alterar os parâmetros não será linear, este modelo não é necessário para previsões, mas para entender o que aconteceu dentro dos parâmetros fornecidos. Por exemplo, aqui vemos pelo critério de Student que, para a operação Put, os parâmetros BlockSize e BlockCache não importam (o que geralmente é previsível):


Mas o fato de um aumento no número de partições levar a uma diminuição no desempenho é algo inesperado (já vimos o efeito positivo de um aumento no número de partições com o BulkLoad), embora seja compreensível. Primeiro, para o processamento, é necessário formar consultas para 30 regiões, em vez de uma, e a quantidade de dados não é tal que gera ganho. Em segundo lugar, o tempo total de operação é determinado pelo RS mais lento e, como o número de DataNode é menor que o número de RS, algumas regiões têm localidade zero. Bem, vamos olhar para os cinco primeiros:


Agora vamos avaliar os resultados da execução dos blocos Get:


O número de partições perdeu significância, o que provavelmente se deve ao fato de os dados estarem bem armazenados em cache e o cache de leitura ser o parâmetro mais significativo (estatisticamente). Naturalmente, aumentar o número de mensagens em uma solicitação também é muito útil para o desempenho. Os melhores resultados:


Bem, finalmente, veja o modelo do bloco que foi executado primeiro e depois coloque:


Aqui todos os parâmetros são significativos. E os resultados dos líderes:


9. Teste de carga


Bem, finalmente, lançaremos uma carga mais ou menos decente, mas é sempre mais interessante quando há algo para comparar. O site do DataStax, um desenvolvedor chave do Cassandra, tem os resultados do NT de vários repositórios NoSQL, incluindo o HBase versão 0.98.6-1. O carregamento foi realizado por 40 fluxos, tamanho de dados 100 bytes, discos SSD. O resultado do teste das operações Read-Modify-Write mostrou esses resultados.


Pelo que entendi, a leitura foi realizada em blocos de 100 registros e, para 16 nós do HBase, o teste DataStax mostrou um desempenho de 10 mil operações por segundo.

É uma sorte que nosso cluster também tenha 16 nós, mas não muito “feliz” que cada um tenha 64 núcleos (threads), enquanto o teste DataStax possui apenas 4. Por outro lado, eles têm discos SSD, e temos HDD e mais a nova versão do HBase e a utilização da CPU durante a carga praticamente não aumentaram significativamente (visualmente em 5 a 10%). No entanto, tentaremos iniciar essa configuração. Por padrão, nas configurações da tabela, a leitura é realizada em um intervalo de teclas de 0 a 50 milhões aleatoriamente (isto é, de fato, cada vez que um novo). Na tabela, 50 milhões de entradas são divididas em 64 partições. As chaves são hash crc32. As configurações da tabela são padrão, o MSLAB está ativado. Iniciando 40 threads, cada thread lê um conjunto de 100 chaves aleatórias e grava imediatamente os 100 bytes gerados nessas chaves novamente.


Suporte: 16 DataNode e 16 RS (CPU Xeon E5-2680 v4 a 2.40GHz * 64 fluxos). HBase versão 1.2.0-cdh5.14.2.

O resultado médio está mais próximo de 40 mil operações por segundo, o que é significativamente melhor do que no teste DataStax. No entanto, para os fins do experimento, as condições podem ser ligeiramente alteradas. É bastante improvável que todo o trabalho seja realizado exclusivamente com uma tabela, bem como apenas com chaves exclusivas. Suponha que exista um determinado conjunto de chaves "quente" que gere a carga principal. Portanto, tentaremos criar uma carga com registros maiores (10 KB), também em pacotes de 100 cada, em 4 tabelas diferentes e limitar o intervalo de chaves solicitadas a 50 mil.O gráfico abaixo mostra o início de 40 threads, cada fluxo lê um conjunto de 100 chaves e grava imediatamente aleatoriamente 10 KB nessas chaves de volta.


Suporte: 16 DataNode e 16 RS (CPU Xeon E5-2680 v4 a 2.40GHz * 64 fluxos). HBase versão 1.2.0-cdh5.14.2.

Durante o carregamento, uma compactação principal foi iniciada várias vezes, conforme mostrado acima, sem esse procedimento; o desempenho será gradualmente degradado; no entanto, um carregamento adicional também ocorrerá durante a execução. Os rebaixamentos são causados ​​por vários motivos. Às vezes, os threads terminavam e, enquanto reiniciavam, havia uma pausa, outras vezes, aplicativos de terceiros criavam uma carga no cluster.

Ler e escrever imediatamente é um dos cenários de trabalho mais difíceis para o HBase. Se você colocar apenas solicitações de colocação de tamanho pequeno, por exemplo, 100 bytes cada, combinando-as em lotes de 10 a 50 mil peças, poderá obter centenas de milhares de operações por segundo e a situação é semelhante às solicitações somente leitura. Vale ressaltar que os resultados são radicalmente melhores do que os obtidos no DataStax, principalmente devido a solicitações em blocos de 50 mil.


Suporte: 16 DataNode e 16 RS (CPU Xeon E5-2680 v4 a 2.40GHz * 64 fluxos). HBase versão 1.2.0-cdh5.14.2.

10. Conclusões


Este sistema é flexível o suficiente para configurar, mas o efeito de um grande número de parâmetros ainda é desconhecido. Alguns deles foram testados, mas não foram incluídos no conjunto de testes resultante. Por exemplo, experimentos preliminares mostraram a insignificância de um parâmetro como DATA_BLOCK_ENCODING, que codifica informações usando valores de células vizinhas, o que é bastante compreensível para dados gerados aleatoriamente. No caso de usar um grande número de objetos repetidos, o ganho pode ser significativo. Em geral, podemos dizer que o HBase dá a impressão de um banco de dados bastante sério e bem pensado, que pode ser bastante produtivo ao lidar com grandes blocos de dados. Especialmente se for possível espalhar os processos de leitura e gravação no tempo.

Se algo na sua opinião não for suficientemente divulgado, estou pronto para contar com mais detalhes. Sugerimos compartilhar sua experiência ou debater se você não concorda com algo.

Source: https://habr.com/ru/post/pt420425/


All Articles