Práticas recomendadas de desempenho do Apache Phoenix

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

Design do esquema de tabela

Ao criar uma tabela no Phoenix, ela é armazenada em uma tabela do HBase. A tabela do HBase contém grupos de colunas (famílias de colunas) que são acessados em conjunto. Uma linha na tabela do Phoenix é uma linha na tabela do HBase em que cada linha consiste em células com versão 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 rowkey. 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 do Phoenix inclui design de chave primária, design de família de colunas, design de coluna individual e como os dados são particionados.

Design de chave primária

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

Por exemplo, uma tabela para contatos tem o 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:

rowkey address phone firstName lastName
1000 1111 San Gabriel Dr. 1-425-000-0002 John Dole
8396 5415 San Gabriel Dr. 1-230-555-0191 Calvin Raji

No entanto, se você consultar com frequência por lastName, esta chave primária pode não ser bem executada, 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 número do seguro social. Esta última coluna é para evitar a ambiguidade de dois residentes no mesmo endereço com o mesmo nome, como pai e filho.

rowkey address phone firstName lastName socialSecurityNum
1000 1111 San Gabriel Dr. 1-425-000-0002 John Dole 111
8396 5415 San Gabriel Dr. 1-230-555-0191 Calvin Raji 222

Com essa nova chave primária, chaves de linhas geradas pelo Phoenix seriam:

rowkey address phone firstName lastName socialSecurityNum
Dole-John-111 1111 San Gabriel Dr. 1-425-000-0002 John Dole 111
Raji-Calvin-222 5415 San Gabriel Dr. 1-230-555-0191 Calvin Raji 222

Na primeira linha de determinada tabela, os dados da chave de linha são representados como mostrados:

rowkey chave value
Dole-John-111 address 1111 San Gabriel Dr.
Dole-John-111 phone 1-425-000-0002
Dole-John-111 firstName John
Dole-John-111 lastName Dole
Dole-John-111 socialSecurityNum 111

Agora, este rowkey armazena uma cópia duplicada dos dados. Considere o tamanho e o número de colunas que você inclui em sua chave primária, pois esse valor é incluído com todas as células na tabela subjacente do HBase.

Além disso, se a chave primária tiver valores que estão crescendo de forma monotônica, você deve criar a tabela com salt buckets para ajudar a evitar a criação de pontos de acesso de gravação – consulte Particionar dados.

Design de família de colunas

Se algumas colunas são acessadas com mais frequência do que outras, você deve criar várias famílias de colunas para separar as que são acessadas frequentemente das que são raramente acessadas.

Além disso, se determinadas colunas tendem a ser acessadas em conjunto, coloque-as na mesma família de colunas.

Design da coluna

  • Mantenha as colunas VARCHAR abaixo de aproximadamente 1 MB devido a custos de E/S de colunas grandes. Ao processar consultas, o HBase materializa células por completo antes de enviá-las para o cliente, que as recebe por completo antes de repassá-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 os dados antes de armazenar para reduzir a latência e custos de E/S.

Dados de partição

O Phoenix permite que você controle o número de regiões em que seus dados são distribuídos, o que pode aumentar significativamente o desempenho de leitura/gravação. Ao criar uma tabela do Phoenix, você pode incrementar ou dividir previamente os dados.

Para incrementar uma tabela durante a criação, especifique o número de salt buckets:

CREATE TABLE CONTACTS (...) SALT_BUCKETS = 16

Este incremento divide a tabela entre os valores de chaves primárias, escolhendo os valores automaticamente.

Para controlar onde as divisões de tabela ocorrerem, você pode dividir previamente a tabela fornecendo os valores de 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 de índice

Um índice do Phoenix é uma tabela do 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ê tiver vários índices definidos e, em seguida, consultar uma tabela, o Phoenix selecionará automaticamente o melhor índice para a consulta. O índice primário é criado automaticamente com base nas chaves primárias que você seleciona.

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

Durante a criação dos índices:

  • Crie somente os índices que você precisa.
  • Limite o número de índices em tabelas atualizadas com frequência. Atualizações em uma tabela são convertidas em gravações para a tabela principal e as tabelas de índice.

Criar índice secundário

Índices secundários podem melhorar o desempenho de leitura ativando o que seria uma verificação completa de tabela em uma pesquisa de ponto, às custas de espaço de armazenamento e velocidade de gravação. Índices secundários podem ser adicionados ou removidos após a criação de tabela e não requerem alterações para as consultas existentes – as consultas apenas são executadas mais rapidamente. Dependendo de suas necessidades, considere a criação de índices abrangidos, índices funcionais ou ambos.

Usar índices abrangidos

Índices abrangidos são índices que incluem dados da linha além os valores que são indexados. Após localizar a entrada de índice desejada, não será necessário acessar a tabela primária.

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

rowkey address phone firstName lastName socialSecurityNum
Dole-John-111 1111 San Gabriel Dr. 1-425-000-0002 John Dole 111
Raji-Calvin-222 5415 San Gabriel Dr. 1-230-555-0191 Calvin Raji 222

No entanto, se você geralmente deseja procurar o FirstName e o lastName dado o socialSecurityNum, pode criar um índice abrangido que inclui o FirstName e o lastName como dados reais na tabela de índice:

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

Esse índice abrangido permite que a consulta a seguir obtenha todos os dados apenas com a leitura da tabela que contém o índice secundário:

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

Usar índices funcionais

Índices funcionais permitem que se crie um índice em uma expressão arbitrária que você espera que seja usada em consultas. Após ter um índice funcional em vigor e uma consulta usar essa expressão, o índice pode ser usado para recuperar os resultados em vez da tabela de dados.

Por exemplo, você pode criar um índice para que possa fazer pesquisas que façam distinção entre maiúsculas e minúsculas no conjunto de nome e sobrenome de uma pessoa:

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

Design de consulta

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

  • Entender o plano de consulta e verifique o comportamento esperado.
  • Unir com eficiência.

Entender 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 a chave primária quando apropriado.
  • Usa índices secundários apropriados, em vez da tabela de dados.
  • Usa RANGE SCAN ou RANGE SCAN sempre que possível, em vez de TABLE SCAN.

Exemplos de planos

Por exemplo, digamos que você tenha uma tabela chamada VOOS que armazena informações de atrasos 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'

Nesse plano, observe a expressão FULL SCAN OVER FLIGHTS. Essa expressão indica que a execução faz uma VERIFICAÇÃO DE TABELA em todas as linhas na tabela, em vez de usar a opção mais eficiente de VERIFICAÇÃO DE INTERVALO ou IGNORAR VERIFICAÇÃO.

Agora, digamos que você deseja consultar voos em 2 de janeiro de 2014 para a operadora AA onde seu flightnum era maior que 1. Vamos supor que as colunas ano, mês, dia do mês, operadora e número do voo existem na tabela de exemplo e fazem parte da chave primária composta. A consulta deve ser semelhante ao seguinte:

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

Vamos examinar o plano para essa 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. Nesse caso, os valores do intervalo são corrigidos com o ano 2014, mês 1 e dia do mês 2, mas permitem valores de número de voo iniciando com 2 e aumentando (*). Esse plano de consulta confirma que a chave primária está sendo usada como o esperado.

Em seguida, crie um índice na tabela VOOS denominada carrier2_idx que esteja apenas no campo da operadora. Esse índice também inclui flightdate, , origintailnume 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 de explicação de planos, consulte a seção explicam Explicar Planos no Guia de Ajuste do Apache Phoenix.

Unir com eficiência

Em geral, convém evitar junções, a menos que um lado seja pequeno, especialmente em consultas frequentes.

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

Cenários

As diretrizes a seguir descrevem alguns padrões comuns.

Cargas de trabalho com uso intenso de leitura

Para casos de uso intenso de leitura, verifique se está usando índices. Além disso, para economizar em sobrecarga de tempo de leitura, considere a criação de índices abrangidos.

Cargas de trabalho com uso intenso de gravação

Para cargas de trabalho com uso intenso de gravação em que a chave primária aumenta de forma monotônica, crie buckets de sal para ajudar a evitar pontos de acesso de gravação, às custas da taxa de transferência de leitura geral por causa das verificações adicionais necessárias. Além disso, ao usar UPSERT para gravar um grande número de registros, desative a confirmação automática e armazene os registros em lote.

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 para todas as linhas excluídas. A confirmação automática impede que o cliente armazene as linhas afetadas buffer com DELETE, de forma que o Phoenix possa excluí-las diretamente nos servidores regionais sem a despesa de retorná-los para o cliente.

Imutável e somente acréscimo

Se seu cenário favorece a velocidade de gravação na integridade dos dados, considere desabilitar o log write-ahead durante a criação de tabelas:

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

Para obter mais detalhes sobre esta e outras opções, consulte Gramática do Apache Phoenix.

Próximas etapas