Entenda leituras e gravações em escala

Leia este documento para tomar decisões bem embasadas sobre a arquitetura dos seus aplicativos para alto desempenho e confiabilidade. Este documento inclui tópicos avançados do Firestore. Se você está começando a usar o Firestore, consulte o guia de início rápido.

O Firestore é um banco de dados flexível e escalonável para desenvolvimento focado em dispositivos móveis, Web e servidores pelo Firebase e o Google Cloud. É muito fácil começar a usar o Firestore e programar aplicativos avançados e eficientes.

É importante entender a mecânica das leituras e gravações no back-end do Firestore para garantir que seus aplicativos continuem apresentando bom desempenho à medida que o tamanho do banco de dados cresce e o tráfego aumenta. É preciso entender também a interação de suas leituras e gravações com a camada de armazenamento e as restrições subjacentes que podem afetar o desempenho.

Consulte as seções a seguir para conhecer as práticas recomendadas antes de criar a arquitetura do seu aplicativo.

Entenda os componentes de alto nível

O diagrama a seguir mostra os componentes de alto nível envolvidos em uma solicitação da API Firestore.

Componentes de alto nível

SDK do Firestore e bibliotecas de cliente

O Firestore oferece suporte a SDKs e bibliotecas de cliente para diferentes plataformas. Embora um app possa realizar chamadas HTTP e RPC diretas para a API Firestore, as bibliotecas de cliente fornecem uma camada de abstração para simplificar o uso da API e implementar as práticas recomendadas. Elas também podem fornecer recursos adicionais, como acesso off-line, caches etc.

Google Front End (GFE)

Este é um serviço de infraestrutura comum a todos os serviços do Google Cloud. O GFE aceita solicitações de entrada e as encaminha para o serviço do Google relevante (serviço Firestore neste contexto). Ele também oferece outras funcionalidades importantes, incluindo proteção contra ataques de negação de serviço.

Serviço do Firestore

O serviço do Firestore realiza verificações na solicitação da API, o que inclui autenticação, autorização, verificações de cota e regras de segurança, além de gerenciar transações. Esse serviço do Firestore inclui um cliente de armazenamento que interage com a camada de armazenamento para as leituras e gravações de dados.

Camada de armazenamento do Firestore

A camada de armazenamento do Firestore é responsável por armazenar dados e metadados, além dos recursos de banco de dados associados fornecidos pelo Firestore. As seções a seguir descrevem como os dados são organizados na camada de armazenamento do Firestore e como o sistema é escalonado. Saber como os dados são organizados pode ajudar você a projetar um modelo de dados escalonável e entender melhor as práticas recomendadas no Firestore.

Intervalos de chave e divisões

O Firestore é um banco de dados NoSQL orientado a documentos. Os dados são armazenados em documentos, que são organizados em hierarquias de coleções. A hierarquia da coleção e o ID do documento são convertidos em uma única chave para cada documento. Os documentos são logicamente armazenados e ordenados lexicograficamente por essa chave única. Usamos o termo "intervalo de chaves" para nos referir a um intervalo de chaves lexicograficamente contíguo.

Um banco de dados do Firestore típico é grande demais para caber em uma única máquina física. Há também cenários em que a carga de trabalho nos dados é muito pesada para ser processada por uma única máquina. Para trabalhar com grandes cargas de trabalho, o Firestore particiona os dados em partes separadas que podem ser armazenadas e exibidas em várias máquinas ou em servidores de armazenamento. Essas partições são feitas nas tabelas de banco de dados em blocos de intervalos de chaves chamados de divisões.

Replicação síncrona

É importante observar que o banco de dados é sempre replicado de forma automática e síncrona. As divisões de dados têm réplicas em diferentes zonas para que elas fiquem disponíveis mesmo quando uma zona se torna inacessível. A replicação consistente das diferentes cópias da divisão é gerenciada pelo algoritmo Paxos por um consenso. Uma réplica de cada divisão é eleita para atuar como líder do Paxos, responsável por lidar com as gravações nessa divisão. A replicação síncrona permite que você leia sempre a versão mais recente dos dados do Firestore.

O resultado geral é um sistema escalonável e altamente disponível que fornece latências baixas para leituras e gravações, independentemente de cargas de trabalho pesadas e em grande escala.

Layout de dados

O Firestore é um banco de dados de documentos sem esquema. No entanto, internamente, os dados são dispostos principalmente em duas tabelas no estilo de banco de dados relacional na camada de armazenamento, da seguinte forma:

  • Tabela Documentos: os documentos são armazenados nesta tabela.
  • Tabela Índices: são armazenadas nessa tabela entradas de índice que possibilitam receber os resultados de maneira eficiente, classificados por valor de índice

O diagrama a seguir mostra como ficariam as tabelas em um banco de dados do Firestore com as divisões. As divisões são replicadas em três zonas diferentes, e cada divisão tem um líder do Paxos atribuído.

Layout de dados

Região única versus multirregião

Ao criar um banco de dados, você precisa selecionar uma região ou uma multirregião.

Um único local regional é uma localização geográfica específica, como us-west1. As divisões de dados de um banco de dados do Firestore têm réplicas em diferentes zonas dentro da região selecionada, conforme explicado anteriormente.

Um local multirregional consiste em um conjunto definido de regiões com réplicas do banco de dados armazenadas. Em uma implantação multirregional do Firestore, duas das regiões têm réplicas completas de todos os dados no banco de dados. Uma terceira região tem uma réplica testemunha que não mantém um conjunto completo de dados, mas participa da replicação. Ao replicar os dados entre várias regiões, esses dados ficam disponíveis para gravação e leitura mesmo com a perda de uma região inteira.

Para mais informações sobre os locais de uma região, consulte Locais do Firestore.

Região única versus multirregião

Entenda a vida útil de uma gravação no Firestore

Um cliente do Firestore pode gravar dados criando, atualizando ou excluindo um único documento. Uma gravação em um único documento requer a atualização atômica do documento e de suas entradas de índice associadas na camada de armazenamento. O Firestore também é compatível com operações atômicas que consistem em várias leituras e/ou gravações em um ou mais documentos.

Para todos os tipos de gravação, o Firestore fornece as propriedades ACID (atomicidade, consistência, isolamento e durabilidade) de bancos de dados relacionais. O Firestore também oferece capacidade de serialização, o que significa que todas as transações aparecem como se tivessem sido executadas em uma ordem serial.

Etapas avançadas em uma transação de gravação

Quando o cliente do Firestore emite uma gravação ou confirma uma transação usando qualquer um dos métodos mencionados anteriormente, isso é executado internamente como uma transação de leitura e gravação de banco de dados na camada de armazenamento. A transação permite que o Firestore forneça as propriedades ACID mencionadas anteriormente.

Como primeira etapa de uma transação, o Firestore lê o documento existente e determina as mutações feitas nos dados da tabela "Documentos".

Isso também inclui fazer as atualizações necessárias na tabela índices, da seguinte maneira:

  • Os campos que estão sendo adicionados aos documentos precisam de inserções correspondentes na tabela Índices.
  • Os campos que estão sendo removidos dos documentos precisam de exclusões correspondentes na tabela Índices.
  • Os campos que estão sendo modificados nos documentos precisam de exclusões (para valores antigos) e inserções (para novos valores) na tabela Índices.

Para calcular as mutações mencionadas anteriormente, o Firestore lê a configuração de indexação do projeto. A configuração de indexação armazena informações sobre os índices de um projeto. O Firestore usa dois tipos de índices, de campo único e compostos. Para uma compreensão detalhada dos índices criados no Firestore, consulte Tipos de índice no Firestore.

Depois que as mutações são calculadas, o Firestore as coleta dentro de uma transação e, em seguida, as confirma.

Entenda uma transação de gravação na camada de armazenamento

Como discutido anteriormente, uma gravação no Firestore envolve uma transação de leitura/gravação na camada de armazenamento. Dependendo do layout dos dados, uma gravação pode envolver uma ou mais divisões, como visto no layout de dados.

No diagrama a seguir, o banco de dados do Firestore tem oito divisões (marcadas de 1 a 8) hospedadas em três servidores de armazenamento diferentes em uma única zona, e cada divisão é replicada em três ou mais zonas diferentes. Cada divisão tem um líder do Paxos, que pode estar em uma zona diferente no caso de divisões diferentes.

Divisão do banco de dados do Firestore

Considere um banco de dados do Firestore que tenha a coleção Restaurants da seguinte maneira:

Coleção de restaurantes

O cliente do Firestore solicita a seguinte alteração em um documento na coleção Restaurant atualizando o valor do campo priceCategory.

Alterar para um documento na coleção

As etapas avançadas a seguir descrevem o que acontece como parte da gravação:

  1. Crie uma transação de leitura/gravação.
  2. Leia o documento restaurant1 na coleção Restaurants da tabela Documentos na camada de armazenamento.
  3. Leia os índices do documento na tabela Índices.
  4. Calcule as mutações a serem feitas nos dados. Nesse caso, há cinco mutações:
    • M1: atualize a linha de restaurant1 na tabela Documentos para refletir a alteração no valor do campo priceCategory.
    • M2 e M3: exclua as linhas do valor antigo de priceCategory na tabela Índices para índices decrescentes e crescentes.
    • M4 e M5: insira as linhas do novo valor de priceCategory na tabela Índices para índices decrescentes e crescentes.
  5. Confirme essas mutações.

O cliente de armazenamento no serviço do Firestore pesquisa as divisões que têm as chaves das linhas a serem alteradas. Vamos considerar um caso em que a Divisão 3 atende M1 e a Divisão 6 atende M2-M5. Há uma transação distribuída, envolvendo todas essas divisões como participantes. As divisões do participante também podem incluir qualquer outra divisão da qual os dados foram lidos anteriormente como parte da transação de leitura/gravação.

As etapas a seguir descrevem o que acontece como parte da confirmação:

  1. O cliente de armazenamento emite uma confirmação. A confirmação contém as mutações M1-M5.
  2. As divisões 3 e 6 são os participantes dessa transação. Um dos participantes é escolhido como o coordenador, como a Divisão 3, por exemplo. O trabalho dele é garantir que a transação seja confirmada ou cancelada atomicamente em todos os participantes.
    • As réplicas líderes dessas divisões são responsáveis pelo trabalho realizado pelos participantes e coordenadores.
  3. Cada participante e coordenador executa um algoritmo Paxos com as respectivas réplicas.
    • O líder executa um algoritmo Paxos com as réplicas. O quórum é alcançado se a maioria das réplicas responder com uma resposta ok to commit ao líder.
    • Em seguida, cada participante notifica o coordenador quando está preparado (primeira fase da confirmação de duas fases). Se algum participante não puder confirmar a transação, toda a transação aborts.
  4. Depois que o coordenador sabe que todos os participantes, incluindo ele mesmo, estão preparados, comunica o resultado da transação do accept a todos os participantes (segunda fase da confirmação de duas fases). Nesta fase, cada participante registra a decisão de confirmação no armazenamento estável e a transação é confirmada.
  5. O coordenador responde ao cliente de armazenamento no Firestore que a transação foi confirmada. Paralelamente, o coordenador e todos os participantes aplicam as mutações aos dados.

Ciclo de vida da confirmação

Quando o banco de dados do Firestore é pequeno, pode acontecer de uma única divisão ter todas as chaves nas mutações M1-M5. Nesse caso, há apenas um participante na transação e a confirmação de duas fases mencionada anteriormente não é necessária, tornando as gravações mais rápidas.

Gravações em multirregiões

Em uma implantação multirregional, a distribuição de réplicas entre regiões aumenta a disponibilidade, mas tem um custo de desempenho. A comunicação entre as réplicas em diferentes regiões aumenta o tempo de retorno. Portanto, a latência do valor de referência para as operações do Firestore é um pouco maior em comparação com as implantações de região única.

Configuramos as réplicas de modo que a liderança para divisões sempre permaneça na região principal. A região principal é aquela que recebe o tráfego para o servidor do Firestore. Essa decisão da liderança reduz o atraso do tempo de retorno na comunicação entre o cliente de armazenamento no Firestore e o líder da réplica (ou coordenador de transações com várias divisões).

Cada gravação no Firestore também envolve alguma interação com o mecanismo em tempo real no Firestore. Para mais informações sobre consultas em tempo real, consulte Entender as consultas em tempo real e em grande escala.

Entenda a vida útil de uma leitura no Firestore

Esta seção analisa detalhadamente leituras independentes e não em tempo real no Firestore. Internamente, o servidor do Firestore lida com a maioria dessas consultas em dois estágios principais:

  1. Uma verificação de intervalo único sobre a tabela Índices.
  2. Pesquisas de pontos na tabela Documentos com base no resultado da verificação anterior
Algumas consultas no Firestore podem exigir menos processamento (por exemplo, consultas somente chaves no modo Datastore) ou mais processamento (por exemplo, consultas IN).

As leituras de dados da camada de armazenamento são feitas internamente usando uma transação de banco de dados para garantir leituras consistentes. No entanto, ao contrário das transações usadas para gravações, essas transações não têm bloqueios. Em vez disso, elas escolhem um carimbo de data/hora e executam todas as leituras com esse carimbo. Como elas não adquirem bloqueios, elas não bloqueiam transações simultâneas de leitura/gravação. Para executar essa transação, o cliente de armazenamento no Firestore especifica um limite de carimbo de data/hora, que informa à camada de armazenamento como escolher um carimbo de data/hora de leitura. O tipo de limite de carimbo de data/hora escolhido pelo cliente de armazenamento no Firestore é determinado pelas opções de leitura para a solicitação de leitura.

Entenda uma transação de leitura na camada de armazenamento

Esta seção descreve os tipos de leituras e como elas são processadas na camada de armazenamento no Firestore.

Leituras fortes

Por padrão, as leituras do Firestore são altamente consistentes. Essa consistência forte significa que uma leitura do Firestore retorna a versão mais recente dos dados que refletem todas as gravações que foram confirmadas até o início da leitura.

Leitura de divisão única

O cliente de armazenamento no Firestore procura as divisões que têm as chaves das linhas a serem lidas. Vamos supor que ele precise fazer uma leitura da Divisão 3 a partir da seção anterior. O cliente envia a solicitação de leitura para a réplica mais próxima para reduzir a latência de ida e volta.

Neste ponto, os seguintes casos podem acontecer, dependendo da réplica escolhida:

  • A solicitação de leitura vai para uma réplica líder (Zona A).
    • Como o líder está sempre atualizado, a leitura pode prosseguir diretamente.
  • A solicitação de leitura vai para uma réplica não líder (como a Zona B)
    • A Divisão 3 pode saber, pelo estado interno, que tem informações suficientes para atender à leitura, e a divisão faz isso.
    • A Divisão 3 não tem certeza se teve acesso aos dados mais recentes. Ela envia uma mensagem ao líder para solicitar o carimbo de data/hora da última transação que precisa ser aplicado para exibir a leitura. Depois que essa transação é aplicada, a leitura pode prosseguir.

O Firestore retorna a resposta para o cliente.

Leitura de várias divisões

Na situação em que as leituras precisam ser feitas de várias divisões, o mesmo mecanismo acontece em todas as divisões. Depois que os dados forem retornados de todas as divisões, o cliente de armazenamento no Firestore vai combinar os resultados. O Firestore responde ao cliente com esses dados.

Leituras desatualizadas

Leituras fortes são o modo padrão no Firestore. No entanto, isso tem um custo de latência potencialmente maior devido à comunicação que pode ser necessária com o líder. Muitas vezes, o aplicativo do Firestore não precisa ler a versão mais recente dos dados, e a funcionalidade tem bom desempenho com dados que podem estar alguns segundos desatualizados.

Nesse caso, o cliente pode optar por receber leituras desatualizadas usando as opções de leitura read_time. Nesse caso, as leituras são feitas porque os dados estavam em read_time, e é muito provável que a réplica mais próxima já tenha confirmado que tem dados no read_time especificado. Para ter um desempenho perceptivelmente melhor, 15 segundos é um valor de inatividade razoável. Mesmo para leituras desatualizadas, as linhas geradas são consistentes entre si.

Evite pontos de acesso

As divisões no Firestore são automaticamente divididas em partes menores para distribuir o trabalho de veiculação de tráfego a mais servidores de armazenamento quando necessário ou quando o espaço da chave é expandido. As divisões criadas para lidar com o excesso de tráfego são retidas por cerca de 24 horas, mesmo que o tráfego desapareça. Portanto, se houver picos de tráfego recorrentes, as divisões serão mantidas e mais divisões serão introduzidas sempre que necessário. Esses mecanismos ajudam o escalonamento automático dos bancos de dados do Firestore para aumentar a carga do tráfego ou o tamanho do banco de dados. No entanto, existem algumas limitações que devem ser consideradas, conforme explicado abaixo.

A divisão do armazenamento e da carga demora, e o aumento rápido do tráfego pode causar erros de alta latência ou de prazo excedido, geralmente chamados de pontos de acesso, enquanto o serviço é ajustado. A prática recomendada é distribuir operações no intervalo de chaves, enquanto se intensifica o tráfego em uma coleção em um banco de dados com 500 operações por segundo. Após esse aumento gradual, intensifique o tráfego em até 50% a cada cinco minutos. Esse processo é chamado de regra 500/50/5 e posiciona o banco de dados com o escalonamento ideal para atender à sua carga de trabalho.

Embora as divisões sejam criadas automaticamente com carga crescente, o Firestore só pode dividir um intervalo de chaves até exibir um único documento usando um conjunto dedicado de servidores de armazenamento replicados. Como resultado, grandes volumes de operações simultâneas em um único documento podem levar a um ponto de acesso nesse documento. Se você encontrar altas latências sustentadas em um único documento, modifique seu modelo de dados para dividir ou replicar os dados em vários documentos.

Os erros de contenção ocorrem quando várias operações tentam ler e/ou gravar o mesmo documento simultaneamente.

Outro caso especial de uso excessivo do ponto de acesso ocorre quando uma chave de aumento/diminuição sequencial é usada como o ID do documento no Firestore e há um número consideravelmente alto de operações por segundo. Criar mais divisões não ajuda neste caso, já que o aumento do tráfego se move para a divisão recém-criada. Como o Firestore indexa automaticamente todos os campos no documento por padrão, esses pontos de acesso em movimento também podem ser criados no espaço de índice de um campo de documento que contém um valor de redução/aumento sequencial como um carimbo de data/hora.

Seguindo as práticas descritas acima, o Firestore pode ser escalonado para atender cargas de trabalho arbitrariamente grandes sem a necessidade de ajustar qualquer configuração.

Solução de problemas

O Firestore oferece o Key Visualizer como uma ferramenta de diagnóstico projetada para analisar padrões de uso e resolver problemas de uso excessivo do ponto de acesso.

A seguir