Práticas recomendadas do Cloud Datastore

É possível usar as práticas recomendadas listadas aqui como uma referência rápida do que deve ser lembrado ao criar um aplicativo que usa o Datastore. Se você está apenas começando no Datastore, essa página pode não ser o melhor ponto de partida, visto que não ensina os fundamentos de como usar o Datastore. Se você for um usuário novo, sugerimos que comece com Primeiros passos no Datastore.

Aspectos gerais

  • Sempre use caracteres UTF-8 em nomes de namespace, tipo, propriedade e chave personalizada. O uso de caracteres não UTF-8 nesses nomes pode interferir na funcionalidade do Datastore. Por exemplo, um caractere não UTF-8 em um nome de propriedade pode impedir a criação de índices que usem essa propriedade.
  • Não use barras (/) nos nomes de tipo ou chave personalizada. O uso de barras nesses nomes pode prejudicar a funcionalidade futura.
  • Evite armazenar informações confidenciais no ID do projeto do Cloud. Há a possibilidade de esse ID persistir após o fim do projeto.

Chamadas de API

  • Em vez de operações individuais, use operações em lote para realizar leituras, gravações e exclusões. As operações em lote são mais eficientes, porque realizam várias operações com a mesma sobrecarga de uma única operação.
  • Se uma transação falhar, faça a reversão. A reversão reduz a latência da repetição de uma outra solicitação disputando os mesmos recursos em uma transação. Há ainda a chance de falha na reversão, portanto, ela deve ser encarada apenas como uma tentativa.
  • Quando possível, use chamadas assíncronas, em vez de chamadas síncronas. As chamadas assíncronas reduzem o impacto da latência. Por exemplo, considere um aplicativo que precisa do resultado de um lookup() síncrono e os resultados de uma consulta para poder renderizar uma resposta. Se lookup() e a consulta não tiverem dependência de dados, não haverá necessidade de aguardar a conclusão de lookup() de maneira síncrona antes de iniciar a consulta.

Entidades

  • Agrupe os dados altamente relacionados em grupos de entidades. Com os grupos de entidades, é possível realizar consultas de ancestral, que retornam resultados de consistência forte. Além disso, as consultas de ancestral verificam rapidamente os grupos de entidades com o mínimo de E/S, porque as entidades contidas no mesmo grupo são armazenadas em locais fisicamente próximos nos servidores do Datastore.
  • Evite realizar mais de uma gravação por segundo no mesmo grupo de entidades. As gravações em uma taxa constante acima desse limite torna as leituras consistentes mais lentas, ocasiona erros de tempo limite nas leituras de consistência forte e reduz o desempenho do aplicativo. Gravações em lote ou transacionais em um grupo de entidades são consideradas como uma única gravação dentro desse limite.
  • Não inclua a mesma entidade (por chave) várias vezes na mesma confirmação. Isso pode afetar a latência do Datastore.

Chaves

  • Se não forem fornecidos na criação da entidade, os nomes de chave serão gerados automaticamente. Eles são alocados de maneira a serem distribuídos uniformemente no keyspace.
  • Para uma chave que usa um nome personalizado, sempre use caracteres UTF-8, exceto uma barra (/). Os caracteres não UTF-8 interferem em vários processos, como a importação de um backup do Datastore para o Google BigQuery. O uso de barras pode prejudicar a funcionalidade futura.
  • No caso de chave com ID numérico:
    • Não use um número negativo como ID. Um ID negativo pode prejudicar a classificação.
    • Não use o valor zero (0) como ID. Caso contrário, o ID será alocado automaticamente.
    • Se você quer atribuir manualmente seus próprios IDs numéricos às entidades criadas, o aplicativo deverá receber um bloco de IDs com o método allocateIds(). Isso impedirá que o Datastore atribua um dos IDs numéricos manuais a outra entidade.
  • Se você atribuir IDs numéricos manuais ou nomes personalizados às entidades que você mesmo criou, não use valores monotonicamente crescentes, por exemplo:

    1, 2, 3, …,
        "Customer1", "Customer2", "Customer3", ….
        "Product 1", "Product 2", "Product 3", ….
        

    No caso de aplicativos que geram muito tráfego, esse tipo de numeração sequencial pode levar a um uso excessivo de pontos de acesso, afetando a latência do Datastore. Para evitar a questão de IDs numéricos sequenciais, consiga IDs numéricos usando o método allocateIds(). O método allocateIds() gera sequências bem distribuídas de IDs numéricos.

  • Com a especificação de uma chave ou o armazenamento do nome gerado, é possível realizar, posteriormente, um lookup() consistente nessa entidade sem precisar emitir uma consulta para encontrar a entidade.

Índices

Propriedades

  • Sempre use caracteres UTF-8 nas propriedades do tipo string. O uso de caracteres não UTF-8 em propriedades do tipo string pode prejudicar as consultas. Se você precisar salvar dados que contenham caracteres não UTF-8, use uma string de bytes.
  • Não use pontos nos nomes de propriedade. Eles interferem na indexação de propriedades de entidade incorporadas.

Consultas

  • Se você só precisar acessar a chave dos resultados da consulta, use uma consulta apenas de chave. Esse tipo de consulta retorna resultados com latência e custo menores do que a recuperação de entidades inteiras.
  • Se você só precisar acessar propriedades específicas de uma entidade, use uma consulta de projeção. Esse tipo de consulta retorna resultados com latência e custo menores do que a recuperação de entidades inteiras.
  • Da mesma forma, se você só precisar acessar as propriedades incluídas no filtro da consulta (por exemplo, aquelas listadas em uma cláusula order by), use uma consulta de projeção.
  • Não use deslocamentos. Em vez disso, use cursores. O uso de um deslocamento só evita o retorno de entidades ignoradas ao aplicativo, mas é possível recuperá-las internamente. As entidades ignoradas afetam a latência da consulta. Além disso, as operações de leitura necessárias para recuperar essas entidades serão cobradas do seu aplicativo.
  • Se você precisar de consistência forte nas suas consultas, use uma consulta de ancestral. Para tanto, primeiro é necessário estruturar os dados para consistência forte. Esse tipo de consulta retorna resultados de consistência forte. Observe que uma consulta não ancestral apenas de chave , seguida por um lookup(), não retorna resultados fortes, porque a consulta não ancestral apenas de chave pode conseguir resultados de um índice não consistente no momento da consulta.

Como desenvolver um design para escalonar

Atualizações em um único grupo de entidades

Não atualize um único grupo de entidades no Datastore muito rapidamente.

Se você estiver usando o Datastore, o Google recomenda que você projete seu aplicativo para que ele não precise atualizar um grupo de entidades mais de uma vez por segundo. Lembre-se de que uma entidade sem pai nem filhos constitui um grupo de entidades em si. Se você atualizar um grupo de entidades muito rapidamente, suas gravações do Datastore terão maior latência, tempos limite e outros tipos de erro. Trata-se de uma contenção.

As taxas de gravação do Datastore em um único grupo de entidades, às vezes, excedem o limite de uma gravação por segundo. Portanto, há a possibilidade dos testes de carga não mostrarem esse problema.

Taxas de leitura/gravação altas em um intervalo de chaves restrito

Evite a ocorrência de taxas altas de leitura ou gravação nas chaves do Datastore que sejam próximas alfabeticamente.

O Datastore é construído sobre o banco de dados NoSQL do Google, o Bigtable, portanto, está sujeito às características de desempenho do Bigtable. O Bigtable escalona por meio da fragmentação de linhas em blocos separados. Essas linhas seguem a ordem alfabética por chave.

No Datastore, um aumento repentino na taxa de gravação em um intervalo pequeno de chaves, que excede a capacidade de um único servidor de blocos, ocasiona gravações mais lentas devido à sobrecarga no bloco. Mais cedo ou mais tarde, o Bigtable dividirá o keyspace para aguentar a alta carga.

Normalmente, o limite de leituras é muito maior do que o de gravações, a menos que as leituras sejam realizadas em uma única chave a uma alta taxa. O Bigtable não pode dividir uma chave única em mais de um bloco.

Os blocos mais usados são aplicáveis a intervalos de chaves usados tanto por chaves de entidades quanto por índices.

Em alguns casos, um ponto de acesso do Datastore pode afetar muito um aplicativo, não apenas impedir leituras ou gravações em um intervalo pequeno de chaves. Por exemplo, as chaves mais acessadas podem ser lidas ou gravadas durante a inicialização da instância, causando falha nas solicitações de carregamento.

Por padrão, o Datastore aloca chaves usando um algoritmo disperso. Portanto, não será comum encontrar uso excessivo do ponto de acesso em gravações do Datastore se você criar novas entidades com uma alta taxa de gravação usando a política de alocação de ID padrão. Há alguns casos especiais que podem ocasionar esse problema:

  • Se você criar entidades novas a uma taxa muito alta usando a política legada de alocação de IDs sequenciais.

  • Se você criar entidades novas a uma taxa muito alta com alocação de IDs próprios monotonicamente crescentes.

  • Se você criar entidades novas a uma taxa muito alta para um tipo que tinha pouquíssimas entidades anteriormente. O Bigtable iniciará com todas as entidades no mesmo servidor de blocos e demorará um pouco para dividir o intervalo de chaves em servidores de blocos diferentes.

  • Esse problema também ocorre na criação de entidades novas a uma taxa alta com uma propriedade indexada de monotonicamente crescente, como o carimbo de data/hora. O motivo é que essas propriedades são chaves de linhas nas tabelas de índice do Bigtable.

  • O Datastore insere o namespace e o tipo do grupo de entidades raiz antes da chave de linha do Bigtable. Você encontrará um ponto de acesso se começar uma gravação em um novo namespace ou tipo sem intensificar o tráfego gradualmente.

Se você tiver uma chave ou propriedade indexada que será monotonicamente crescente, terá possibilidade de antecedê-la com um hash aleatório para que as chaves sejam fragmentadas em vários blocos.

Da mesma forma, se for preciso consultar uma propriedade monotonicamente crescente (ou decrescente) usando um tipo ou filtro, também será possível indexar uma nova propriedade com valor constante antecedido por um valor de alta cardinalidade em todo o conjunto de dados, mas comum a todas as entidades no escopo da consulta a ser realizada. Por exemplo, para consultar entradas pelo carimbo de data/hora para um único usuário por vez, anteceda o carimbo de data/hora com o ID do usuário e indexe essa nova propriedade. Isso ainda permite consultas e resultados ordenados para esse usuário, mas a presença do ID do usuário garante que o índice em si seja bem fragmentado.

Para uma explicação mais detalhada sobre esse problema, consulte a postagem do blog de Ikai Lan sobre como salvar valores monotonicamente crescentes no Datastore.

Como intensificar o tráfego

Intensifique o tráfego gradualmente para os novos tipos ou partes do keyspace no Datastore.

Intensifique gradualmente o tráfego para os novos tipos do Datastore para que o Bigtable tenha tempo suficiente de dividir os blocos à medida que o tráfego aumentar. Recomendamos realizar até 500 operações por segundo em um tipo novo do Datastore e, depois, aumentar o tráfego em 50% a cada cinco minutos. Teoricamente, seguindo essa programação de intensificação do tráfego, é possível aumentar para até 740 mil operações por segundo após 90 minutos. Confira se as gravações estão distribuídas de forma relativamente uniforme por todo o intervalo de chaves. Nossos SREs chamam isso de regra dos "500/50/5".

Esse padrão de intensificação gradual é muito importante quando você altera seu código para deixar de usar o tipo A e usar o tipo B. Uma forma simples de processar essa migração é alterar o código para ler o tipo B e, caso ele não exista, ler o tipo A. No entanto, isso pode causar um aumento repentino no tráfego para um novo tipo com uma parte muito pequena do keyspace. É possível que o Bigtable não consiga dividir os blocos com eficiência se o keyspace for esparso.

O mesmo problema poderá ocorrer ao migrar as entidades para usar um intervalo de chaves diferente dentro do mesmo tipo.

A estratégia usada para migrar as entidades para um novo tipo ou chave dependerá do modelo de dados. Veja abaixo um exemplo de estratégia de migração, conhecida como "leituras paralelas". Você precisará determinar se essa estratégia é ou não eficaz para os seus dados. Um ponto importante a ser considerado é o impacto no custo das operações paralelas durante a migração.

Primeiro, faça a leitura de uma entidade ou chave antiga. Se não houver uma, faça a leitura de uma entidade ou chave nova. Uma taxa alta de leituras de entidades inexistentes pode provocar uso excessivo do ponto de acesso. Portanto, é preciso aumentar a carga gradualmente. Uma estratégia melhor é copiar a entidade antiga para a nova e excluir a antiga em seguida. Intensifique as leituras paralelas para que o novo keyspace seja bem dividido.

Uma estratégia possível de intensificação gradual de leituras ou gravações em um tipo novo é usar um hash determinista do ID do usuário para conseguir uma porcentagem aleatória de usuários que gravam em entidades novas. Verifique se o resultado do hash de ID do usuário não foi distorcido pela função aleatória ou pelo comportamento do usuário.

Enquanto isso, execute um job do Dataflow para copiar todos os dados das entidades ou chaves antigas para as novas. O job em lote precisa evitar gravações em chaves sequenciais para evitar o uso excessivo de pontos de acesso no Bigtable. Quando o job em lote estiver concluído, só será possível realizar leituras no local novo.

Uma maneira de refinar essa estratégia é migrar pequenos lotes de usuários por vez. Adicione um campo à entidade que rastreia o status de migração de cada usuário. Selecione um lote de usuários a migrar com base em um hash do ID do usuário. Um job do Mapreduce ou do Dataflow migrará as chaves para esse lote de usuários. Os usuários que estão em migração usarão leituras paralelas.

Observe que, a menos que você faça gravações duplas das entidades antigas e novas durante a fase de migração (o que aumentaria os custos do Datastore), a reversão não é fácil.

Exclusões

Evite excluir um grande número de entidades do Datastore em um intervalo pequeno de chaves.

O Bigtable periodicamente regrava as tabelas para remover as entradas excluídas e reorganizar os dados, permitindo leituras e gravações mais eficientes. Esse processo é conhecido como compactação.

Se você excluir um grande número de entidades do Datastore em um pequeno intervalo de chaves, as consultas nessa parte do índice serão mais lentas até que a compactação seja concluída. Em casos extremos, as consultas podem expirar antes de retornar os resultados.

O uso de um valor de carimbo de data/hora de um campo indexado para representar o prazo de validade da entidade é um antipadrão. Para recuperar as entidades expiradas, é necessário realizar uma consulta no campo indexado que, provavelmente, está em uma parte sobreposta do keyspace com as entradas de índice das entidades excluídas mais recentemente.

É possível melhorar o desempenho com "consultas fragmentadas", que antecedem o carimbo de data/hora de expiração com uma string de comprimento fixo. O índice é classificado na string completa. Portanto, as entidades no mesmo carimbo de data/hora serão localizadas ao longo do intervalo de chaves do índice. Execute várias consultas em paralelo para buscar os resultados de cada fragmento.

Uma solução mais completa para o problema do carimbo de data/hora de expiração é usar um "número de geração", ou seja, um contador global que é atualizado periodicamente. O número de geração antecede o carimbo de data/hora de expiração para que as consultas sejam classificadas primeiro por esse número, depois pelo fragmento e, por fim, pelo carimbo de data/hora. A exclusão de entidades antigas ocorre em uma geração anterior. Para todas as entidades que não foram excluídas, é preciso incrementar o número de geração. Após a exclusão, você avança para a próxima geração. As consultas realizadas em uma geração mais antiga terão baixo desempenho até que a compactação seja concluída. Talvez seja necessário aguardar até que várias gerações sejam concluídas antes de consultar o índice para conseguir a lista de entidades a serem excluídas. Isso reduz o risco de perder resultados devido a consistência posterior.

Como fragmentar e replicar

Use fragmentação ou replicação nas chaves do Datastore mais usadas.

Use a replicação se precisar realizar leituras em uma parte do intervalo de chaves a uma taxa acima da permitida pelo Bigtable. Ao usar essa estratégia, você armazenará “n” cópias da mesma entidade, permitindo uma taxa de leituras “n” vezes mais elevada do que a compatível para uma única entidade.

Use a fragmentação se precisar gravar em uma parte do intervalo de chaves a uma taxa acima da permitida pelo Bigtable. A fragmentação divide uma entidade em pedaços menores.

Alguns erros comuns ao fragmentar:

  • Fragmentar usando um prefixo de tempo. Quando o tempo passa para o próximo prefixo, a nova parte não dividida se torna um hotspot. Em vez disso, passe gradualmente uma parte das gravações para o prefixo novo.

  • Fragmentar apenas as entidades mais usadas. Se você fragmentar uma pequena proporção do número total de entidades, não haverá linhas suficientes entre as entidades mais usadas para garantir que elas fiquem em partes divididas diferentes.

A seguir