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, porque 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.

Geral

  • Sempre use caracteres UTF-8 nos nomes de namespaces, tipos, propriedades e chaves personalizadas. O uso de caracteres não UTF-8 nesses nomes pode interferir na funcionalidade do Datastore. Por exemplo, um caractere não codificado em UTF-8 em um nome de propriedade pode impedir a criação de índices que usam essa propriedade.
  • Não use barras (/) nos nomes de tipo ou de chave personalizada. Usar barras nesses nomes pode prejudicar a funcionalidade futura.
  • Evite armazenar informações confidenciais no código do projeto do Cloud. Há a possibilidade de esse código persistir após o fim da duração do projeto.
  • Como prática recomendada de compliance de dados, recomendamos não armazenar informações sensíveis nos nomes de entidades ou propriedades de entidades do Datastore.

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, pois realizam várias operações gerando a mesma sobrecarga que uma única operação.
  • Se uma transação falhar, faça o rollback. A operação de rollback reduz a latência da repetição de solicitações diferentes que disputam os mesmos recursos de uma transação. Observe que a operação de rollback também pode falhar e por isso precisa ser tentada somente uma vez.
  • 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 antes que ela renderize 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.

Entities

  • 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 ancestrais 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. Realizar gravações em uma taxa constante acima desse limite torna as leituras consistentes mais lentas, ocasiona erros de tempo limite nas leituras com forte consistência 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 das chaves 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. Usar 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 0 (zero) como ID. Caso contrário, o ID será atribuído automaticamente.
    • Se quiser atribuir manualmente seus próprios IDs numéricos às entidades criadas, o aplicativo deverá coletar 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 crescentes de maneira uniforme, como:

    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 o problema de IDs numéricos sequenciais, colete os IDs numéricos do 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 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 diferentes de UTF-8 em propriedades do tipo string pode prejudicar as consultas. Se precisar salvar dados que contenham caracteres não codificados em UTF-8, use uma string de bytes.
  • Não use pontos nos nomes das propriedades. Eles interferem na indexação de propriedades de entidade incorporadas.

Consultas

  • Se você precisar acessar somente a chave dos resultados de consultas, use uma consulta apenas de chaves. Esse tipo de consulta retorna resultados com menor latência e custo do que a recuperação de entidades inteiras.
  • Se você precisar acessar somente propriedades específicas de uma entidade, use uma consulta de projeção. Esse tipo de consulta retorna resultados com menor latência e custo do que a recuperação de entidades inteiras.
  • Da mesma forma, se for preciso acessar apenas as propriedades incluídas no filtro de 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. Fazer o uso de deslocamentos apenas evita o retorno das entidades ignoradas para o aplicativo. No entanto, essas entidades ainda são recuperadas 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 uma consistência forte nas suas consultas, use uma consulta de ancestral. Para isso, primeiro é necessário estruturar os dados para uma forte consistência. Esse tipo de consulta retorna resultados com forte consistência. 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 elaborar um projeto 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 próprio. 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 de os 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 espaço da chave 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 passar por leituras ou gravações 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:

  • Na criação de entidades novas a uma taxa muito alta usando a política legada de alocação de códigos sequenciais.

  • Na criação de entidades novas a uma taxa muito alta com alocação de códigos próprios monotonicamente crescentes.

  • Na criação de 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 propriedades indexadas de forma monotonicamente crescente, como o carimbo de data/hora. O motivo é que essas propriedades são as chaves das colunas 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. É possível encontrar um hotspot se começar uma gravação em um novo namespace ou tipo sem intensificar o tráfego gradualmente.

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

Da mesma forma, se você precisa consultar uma propriedade monotonicamente crescente (ou decrescente) usando um tipo ou filtro, pode, em vez disso, indexar uma nova propriedade em que prefixa o valor monotônico com um valor de alta cardinalidade em todo o conjunto de dados, mas que seja comum a todas as entidades no escopo da consulta que quer executar. Por exemplo, para consultar entradas pelo carimbo de data/hora para um único usuário por vez, prefixe o carimbo de data/hora com o código do usuário e indexe essa nova propriedade. Isso ainda permite consultas e resultados ordenados para esse usuário, mas a presença do código 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 5 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 relativamente de forma 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 é eficaz para os seus dados. Um ponto importante a ser levado em consideração é 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 o hotspotting. Portanto, é preciso ter certeza para aumentar a carga gradualmente. Uma estratégia melhor é copiar a entidade antiga para a nova e excluir a antiga em seguida. Aumente gradualmente as leituras paralelas para que o novo keyspace seja bem dividido.

Uma estratégia possível para aumentar gradualmente as leituras ou gravações em um tipo novo é usar um hash determinista do código do usuário e, assim, conseguir uma porcentagem aleatória de usuários que realizam gravações em entidades novas. Verifique se o resultado do hash de código do usuário não foi distorcido pela função aleatória ou 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 prevenir a ocorrência de hotspots 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 é fazer a migração em lotes pequenos de usuários de uma só vez. Adicione um campo à entidade de usuário que rastreia a migração de cada usuário. Selecione um lote de usuários para migrar com base em um hash de 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 não é fácil fazer o rollback, a menos que você realize gravações duplas das entidades antigas e novas durante a fase de migração. Isso aumentaria os custos do Datastore.

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. Assim, as leituras e gravações ficam 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 atingir o tempo limite antes de retornar os resultados.

Usar um valor de carimbo de data/hora de um campo indexado para representar a data de expiração 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 do índice das entidades excluídas mais recentemente.

É possível melhorar o desempenho com "consultas fragmentadas" que colocam uma string de comprimento fixo antes do carimbo de data/hora de expiração. 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", 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. Todas as entidades que não foram excluídas precisam ter o número de geração incrementado. 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 um desempenho ruim até que a compactação esteja 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 à consistência eventual.

Fragmentação e replicação

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 com uma taxa maior do que a 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