Melhores práticas de desempenho do Apache Phoenix

O aspeto mais importante do desempenho do Apache Phoenix é otimizar o Apache HBase subjacente. Phoenix cria um modelo de dados relacional sobre o HBase que converte consultas SQL em operações do HBase, como verificações. O design do esquema da tabela, a seleção e a ordenação dos campos na chave primária e o uso de índices afetam o desempenho do Phoenix.

Design de esquema de tabela

Quando você cria uma tabela no Phoenix, essa tabela é armazenada em uma tabela do HBase. A tabela HBase contém grupos de colunas (famílias de colunas) que são acessadas juntas. Uma linha na tabela Phoenix é uma linha na tabela HBase, onde cada linha consiste em células versionadas associadas a uma ou mais colunas. Logicamente, uma única linha do HBase é uma coleção de pares chave-valor, cada um com o mesmo valor de chave de linha. Ou seja, cada par chave-valor tem um atributo rowkey, e o valor desse atributo rowkey é o mesmo para uma linha específica.

O design de esquema de uma tabela Phoenix inclui o design de chave primária, o design da família de colunas, o design de coluna individual e como os dados são particionados.

Design de chave primária

A chave primária definida em uma tabela em Phoenix determina como os dados são armazenados na chave de linha da tabela HBase subjacente. No HBase, a única maneira de acessar uma linha específica é com a chave de linha. Além disso, os dados armazenados em uma tabela do HBase são classificados pela chave de linha. Phoenix cria o valor da chave de linha concatenando os valores de cada uma das colunas na linha, na ordem em que são definidas na chave primária.

Por exemplo, uma tabela para contatos tem nome, sobrenome, número de telefone e endereço, todos na mesma família de colunas. Você pode definir uma chave primária com base em um número de sequência crescente:

Tecla de linha Endereço telefone nomePróprio apelido
1000 1111 São Gabriel Dr. 1-425-000-0002 John Dole
8396 5415 São Gabriel Dr. 1-230-555-0191 Calvino Raji

No entanto, se você consultar frequentemente por lastName, essa chave primária pode não ter um bom desempenho, porque cada consulta requer uma verificação de tabela completa para ler o valor de cada lastName. Em vez disso, você pode definir uma chave primária nas colunas lastName, firstName e social security number. Esta última coluna é para desambiguar dois moradores no mesmo endereço com o mesmo nome, como pai e filho.

Tecla de linha Endereço telefone nomePróprio apelido socialSecurityNum
1000 1111 São Gabriel Dr. 1-425-000-0002 John Dole 111
8396 5415 São Gabriel Dr. 1-230-555-0191 Calvino Raji 222

Com esta nova chave primária, as chaves de linha geradas pelo Phoenix seriam:

Tecla de linha Endereço telefone nomePróprio apelido socialSecurityNum
Dole-João-111 1111 São Gabriel Dr. 1-425-000-0002 John Dole 111
Raji-Calvino-222 5415 São Gabriel Dr. 1-230-555-0191 Calvino Raji 222

Na primeira linha de uma determinada tabela, os dados para a chave de linha são representados como mostrado:

Tecla de linha key valor
Dole-João-111 Endereço 1111 São Gabriel Dr.
Dole-João-111 telefone 1-425-000-0002
Dole-João-111 nomePróprio John
Dole-João-111 apelido Dole
Dole-João-111 socialSecurityNum 111

Essa chave de linha agora armazena uma cópia duplicada dos dados. Considere o tamanho e o número de colunas incluídas na chave primária, pois esse valor é incluído em todas as células da tabela HBase subjacente.

Além disso, se a chave primária tiver valores que estão aumentando monotonicamente, você deve criar a tabela com baldes de sal para ajudar a evitar a criação de pontos de acesso de gravação - consulte Dados de partição.

Design da família de colunas

Se algumas colunas forem acessadas com mais frequência do que outras, você deverá criar várias famílias de colunas para separar as colunas acessadas com freqüência das colunas raramente acessadas.

Além disso, se certas colunas tendem a ser acessadas juntas, coloque essas colunas na mesma família de colunas.

Design da coluna

  • Mantenha as colunas VARCHAR abaixo de cerca de 1 MB devido aos custos de E/S de colunas grandes. Ao processar consultas, o HBase materializa as células na íntegra antes de enviá-las ao cliente, e o cliente as recebe na íntegra antes de entregá-las ao código do aplicativo.
  • Armazene valores de coluna usando um formato compacto, como protobuf, Avro, msgpack ou BSON. JSON não é recomendado, pois é maior.
  • Considere compactar dados antes do armazenamento para reduzir a latência e os custos de E/S.

Dados de partição

Phoenix permite que você controle o número de regiões onde seus dados são distribuídos, o que pode aumentar significativamente o desempenho de leitura/gravação. Ao criar uma tabela Phoenix, você pode salgar ou pré-dividir seus dados.

Para salgar uma mesa durante a criação, especifique o número de baldes de sal:

CREATE TABLE CONTACTS (...) SALT_BUCKETS = 16

Esta salga divide a tabela ao longo dos valores das chaves primárias, escolhendo os valores automaticamente.

Para controlar onde ocorrem as divisões da tabela, você pode pré-dividir a tabela fornecendo os valores do intervalo ao longo do qual a divisão ocorre. Por exemplo, para criar uma tabela dividida em três regiões:

CREATE TABLE CONTACTS (...) SPLIT ON ('CS','EU','NA')

Design do índice

Um índice Phoenix é uma tabela HBase que armazena uma cópia de alguns ou todos os dados da tabela indexada. Um índice melhora o desempenho para tipos específicos de consultas.

Quando você tem vários índices definidos e, em seguida, consulta uma tabela, Phoenix seleciona automaticamente o melhor índice para a consulta. O índice primário é criado automaticamente com base nas chaves primárias selecionadas.

Para consultas antecipadas, você também pode criar índices secundários especificando suas colunas.

Ao projetar seus índices:

  • Crie apenas os índices de que precisa.
  • Limite o número de índices em tabelas atualizadas com freqüência. As atualizações de uma tabela se traduzem em gravações na tabela principal e nas tabelas de índice.

Criar índices secundários

Os índices secundários podem melhorar o desempenho de leitura, transformando o que seria uma verificação de tabela completa em uma pesquisa pontual, ao custo de espaço de armazenamento e velocidade de gravação. Os índices secundários podem ser adicionados ou removidos após a criação da tabela e não exigem alterações nas consultas existentes – as consultas são executadas mais rapidamente. Dependendo das suas necessidades, considere a criação de índices cobertos, índices funcionais ou ambos.

Usar índices cobertos

Os índices cobertos são índices que incluem dados da linha, além dos valores indexados. Depois de encontrar a entrada de índice desejada, não há necessidade de acessar a tabela primária.

Por exemplo, na tabela de contatos de exemplo, você pode criar um índice secundário apenas na coluna socialSecurityNum. Esse índice secundário aceleraria consultas que filtram por valores socialSecurityNum, mas a recuperação de outros valores de campo requer outra leitura na tabela principal.

Tecla de linha Endereço telefone nomePróprio apelido socialSecurityNum
Dole-João-111 1111 São Gabriel Dr. 1-425-000-0002 John Dole 111
Raji-Calvino-222 5415 São Gabriel Dr. 1-230-555-0191 Calvino Raji 222

No entanto, se você normalmente quiser procurar o firstName e lastName dado o socialSecurityNum, você pode criar um índice coberto que inclui o firstName e lastName como dados reais na tabela de índice:

CREATE INDEX ssn_idx ON CONTACTS (socialSecurityNum) INCLUDE(firstName, lastName);

Esse índice coberto permite que a seguinte consulta adquira todos os dados apenas lendo a tabela que contém o índice secundário:

SELECT socialSecurityNum, firstName, lastName FROM CONTACTS WHERE socialSecurityNum > 100;

Usar índices funcionais

Os índices funcionais permitem criar um índice em uma expressão arbitrária que você espera que seja usada em consultas. Depois que você tiver um índice funcional instalado e uma consulta usar essa expressão, o índice poderá ser usado para recuperar os resultados em vez da tabela de dados.

Por exemplo, você pode criar um índice para permitir que você faça pesquisas que não diferenciem maiúsculas de minúsculas no nome e sobrenome combinados de uma pessoa:

CREATE INDEX FULLNAME_UPPER_IDX ON "Contacts" (UPPER("firstName"||' '||"lastName"));

Design de consulta

As principais considerações no design da consulta são:

  • Compreenda o plano de consulta e verifique seu comportamento esperado.
  • Junte-se de forma eficiente.

Compreender o plano de consulta

No SQLLine, use EXPLAIN seguido de sua consulta SQL para exibir o plano de operações que o Phoenix executa. Verifique se o plano:

  • Usa sua chave primária quando apropriado.
  • Usa índices secundários apropriados, em vez da tabela de dados.
  • Usa RANGE SCAN ou SKIP SCAN sempre que possível, em vez de TABLE SCAN.

Exemplos de planos

Como exemplo, digamos que você tenha uma tabela chamada VOOS que armazena informações de atraso de voo.

Para selecionar todos os voos com um de 19805, onde airlineid é um airlineid campo que não está na chave primária ou em qualquer índice:

select * from "FLIGHTS" where airlineid = '19805';

Execute o comando explicado da seguinte maneira:

explain select * from "FLIGHTS" where airlineid = '19805';

O plano de consulta tem esta aparência:

CLIENT 1-CHUNK PARALLEL 1-WAY ROUND ROBIN FULL SCAN OVER FLIGHTS
   SERVER FILTER BY AIRLINEID = '19805'

Neste plano, anote a frase FULL SCAN OVER FLIGHTS. Esta frase indica que a execução faz uma TABLE SCAN em todas as linhas da tabela, em vez de usar a opção mais eficiente RANGE SCAN ou SKIP SCAN.

Agora, digamos que você queira consultar voos em 2 de janeiro de 2014 para a companhia aérea AA onde seu flightnum foi maior que 1. Vamos supor que as colunas ano, mês, diademês, transportadora e flightnum existam na tabela de exemplo e façam parte da chave primária composta. A consulta teria a seguinte aparência:

select * from "FLIGHTS" where year = 2014 and month = 1 and dayofmonth = 2 and carrier = 'AA' and flightnum > 1;

Vamos examinar o plano para esta consulta com:

explain select * from "FLIGHTS" where year = 2014 and month = 1 and dayofmonth = 2 and carrier = 'AA' and flightnum > 1;

O plano resultante é:

CLIENT 1-CHUNK PARALLEL 1-WAY ROUND ROBIN RANGE SCAN OVER FLIGHTS [2014,1,2,'AA',2] - [2014,1,2,'AA',*]

Os valores entre colchetes são o intervalo de valores para as chaves primárias. Neste caso, os valores do intervalo são fixados com o ano 2014, mês 1 e dia do mês 2, mas permitem valores para flightnum começando com 2 e para cima (*). Este plano de consulta confirma que a chave primária está sendo usada conforme o esperado.

Em seguida, crie um índice na tabela VOOS nomeado carrier2_idx que está apenas no campo da transportadora. Este índice também inclui flightdate, , , tailnumorigine flightnum como colunas cobertas cujos dados também são armazenados no índice.

CREATE INDEX carrier2_idx ON FLIGHTS (carrier) INCLUDE(FLIGHTDATE,TAILNUM,ORIGIN,FLIGHTNUM);

Digamos que você queira obter a operadora junto com o flightdate e tailnum, como na seguinte consulta:

explain select carrier,flightdate,tailnum from "FLIGHTS" where carrier = 'AA';

Você deve ver este índice sendo usado:

CLIENT 1-CHUNK PARALLEL 1-WAY ROUND ROBIN RANGE SCAN OVER CARRIER2_IDX ['AA']

Para obter uma lista completa dos itens que podem aparecer nos resultados do plano explicativo, consulte a seção Explicar planos no Apache Phoenix Tuning Guide.

Junte-se de forma eficiente

Geralmente, você quer evitar junções, a menos que um lado seja pequeno, especialmente em consultas frequentes.

Se necessário, você pode fazer grandes junções com a dica, mas uma junção grande é uma operação cara /*+ USE_SORT_MERGE_JOIN */ em um grande número de linhas. Se o tamanho geral de todas as tabelas do lado direito exceder a memória disponível, use a /*+ NO_STAR_JOIN */ dica.

Cenários

As diretrizes a seguir descrevem alguns padrões comuns.

Cargas de trabalho de leitura pesada

Para casos de uso com muita leitura, verifique se você está usando índices. Além disso, para economizar sobrecarga de tempo de leitura, considere a criação de índices cobertos.

Cargas de trabalho pesadas de gravação

Para cargas de trabalho de gravação pesadas em que a chave primária está aumentando monotonicamente, crie buckets de sal para ajudar a evitar pontos críticos de gravação, às custas da taxa de transferência de leitura geral devido às verificações adicionais necessárias. Além disso, ao usar o UPSERT para gravar um grande número de registros, desative a confirmação automática e agrupe os registros.

Exclusões em massa

Ao excluir um conjunto de dados grande, ative a confirmação automática antes de emitir a consulta DELETE, para que o cliente não precise se lembrar das chaves de linha de todas as linhas excluídas. A AutoCommit impede que o cliente armazene em buffer as linhas afetadas pelo DELETE, para que Phoenix possa excluí-las diretamente nos servidores da região sem a despesa de devolvê-las ao cliente.

Imutável e somente apêndice

Se o seu cenário favorece a velocidade de gravação em detrimento da integridade dos dados, considere desativar o log write-ahead ao criar suas tabelas:

CREATE TABLE CONTACTS (...) DISABLE_WAL=true;

Para obter detalhes sobre esta e outras opções, consulte Apache Phoenix Grammar.

Próximos passos