Práticas recomendadas

É possível usar as práticas recomendadas listadas aqui como uma rápida referência sobre o que precisa ser lembrado ao criar um aplicativo que usa o Firestore no modo Datastore. Se estiver apenas começando no modo Datastore, essa página pode não ser o melhor ponto de partida, já que não ensinamos os fundamentos de como usar o modo Datastore aqui. Se você for um novo usuário, recomendamos começar pelos Primeiros passos com o Firestore no modo 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 modo 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 em um ID de projeto do Cloud. Um ID de projeto do Cloud pode ser mantido após a vida útil 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, 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.

Entidades

  • Não inclua a mesma entidade (por chave) várias vezes na mesma confirmação. Isso pode afetar a latência.

  • Consulte a seção sobre atualizações em uma entidade.

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 chaves que usam um nome personalizado, sempre use caracteres UTF-8, exceto uma barra (/). Os caracteres não codificados em UTF-8 interferem em vários processos, como na importação de um arquivo de exportação do modo Datastore para o BigQuery. Usar barras pode prejudicar a funcionalidade futura.
  • Para uma chave que usa um ID numérico:
    • Não use um número negativo como ID. Um ID negativo pode interferir na classificação.
    • Não use o valor 0(zero) como ID. Caso contrário, você receberá um ID alocado 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(). Assim, o modo Datastore não atribuirá os IDs numéricos que você criou manualmente a outras entidades.
  • Se você atribuir códigos 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 do ponto de acesso, afetando a latência do modo 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.

  • Ao especificar uma chave ou armazenar o nome gerado, é possível executar posteriormente um lookup() 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 cursors. 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.

Como elaborar um design para escalonar

As práticas recomendadas a seguir descrevem como evitar situações que criam problemas de contenção.

Atualizações em uma entidade

Ao projetar seu app, considere a velocidade com que ele atualiza entidades únicas. A melhor maneira de caracterizar o desempenho da carga de trabalho é realizar testes de carga. A taxa máxima exata em que um app pode atualizar uma única entidade depende muito da carga de trabalho. Os fatores incluem a taxa de gravação, a disputa entre solicitações e o número de índices afetados.

Uma operação de gravação de entidade atualiza a entidade e todos os índices associados, e o Firestore no modo Datastore aplica de maneira síncrona a operação de gravação em um quórum de réplicas. Com taxas de gravação altas o suficiente, o banco de dados vai começar a passar por contenções, aumento de latência ou outros erros.

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

Evite altas taxas de leitura ou gravação para fechar documentos lexicograficamente, ou seu aplicativo sofrerá erros de contenção. Esse problema é conhecido como uso excessivo do ponto de acesso, e seu aplicativo pode sofrer isso ao fazer o seguinte:

  • Criar novas entidades a uma taxa muito alta e alocar os próprios IDs constantemente crescentes.

    O modo Datastore aloca chaves usando um algoritmo de dispersão. Se você criar novas entidades usando alocação automática de ID de entidade, não haverá uso excessivo de ponto de acesso em gravações.

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

  • Criar novas entidades a uma taxa alta para um tipo com poucas entidades.

  • Criar novas entidades com um valor de propriedade indexado e constantemente crescente, como um carimbo de data/hora, a uma taxa muito alta.

  • Excluir entidades de um tipo a uma taxa alta.

  • Gravar no banco de dados a uma taxa muito alta sem aumentar gradualmente o tráfego.

Se você tiver um aumento repentino na taxa de gravação em um pequeno intervalo de chaves, pode haver gravações lentas devido a um ponto de acesso. No futuro, o modo Datastore 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.

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

Em alguns casos, um ponto de acesso pode causar um impacto maior em um aplicativo do que somente 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.

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

Da mesma forma, se você precisar consultar uma propriedade crescente (ou decrescente) de maneira constante usando um tipo ou filtro, também é possível indexar uma nova propriedade em que o valor constante seja prefixado 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.

Como intensificar o tráfego

Gradualmente, aumente o tráfego para novos tipos ou partes do keyspace.

Intensifique gradualmente o tráfego para novos tipos para dar tempo suficiente ao Firestore no modo Datastore para se preparar para o aumento do tráfego. Recomendamos realizar no máximo 500 operações por segundo em um tipo novo de Cloud 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 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.

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. Seu trabalho em lote deve evitar gravações em chaves sequenciais para evitar pontos de acesso. 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 incorridos do modo Datastore.

Exclusões

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

Periodicamente, o Firestore no modo Datastore regrava suas tabelas para remover entradas excluídas e reorganizar os dados, tornando as leituras e gravações mais eficientes. Esse processo é conhecido como compactação.

Se você excluir um grande número de entidades do modo 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 para lidar com o uso excessivo de pontos de acesso.

É possível usar a replicação se precisar ler uma parte do intervalo de chaves a uma taxa maior do que o permitido pelo Firestore no modo Datastore. Ao usar essa estratégia, “n” cópias da mesma entidade serão armazenadas, permitindo uma taxa de leituras “n” vezes mais elevada do que a compatível para uma única entidade.

É possível usar a fragmentação se precisar gravar uma parte do intervalo de chaves a uma taxa maior do que o permitido pelo Firestore no modo Datastore. 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.

E agora?