Como equilibrar consistência forte e eventual com o Cloud Datastore

Como oferecer uma experiência de usuário consistente e aproveitar o modelo de consistência eventual para fazer o escalonamento para grandes conjuntos de dados

Neste documento, falamos sobre como atingir uma consistência forte para garantir uma experiência do usuário positiva e, ao mesmo tempo, adotar o modelo de consistência eventual do Cloud Datastore para processar grandes quantidades de dados e usuários.

Ele é destinado a engenheiros e arquitetos de software que querem criar soluções no Cloud Datastore. Para ajudar os leitores mais acostumados com bancos de dados relacionais do que com sistemas não relacionais como o Cloud Datastore, este artigo aponta conceitos análogos em bancos de dados relacionais. Consideramos que você está um pouco familiarizado com o Cloud Datastore. A maneira mais fácil de começar a usar o Cloud Datastore é com uma das linguagens compatíveis no Google App Engine. Se você ainda não usou o App Engine, sugerimos que leia antes o Guia de primeiros passos e a seção Como armazenar dados de uma dessas linguagens. O Python é usado nos fragmentos de código de exemplo, mas não é necessário ter experiência com ele para acompanhar este documento.

Observação: os snippets de código deste artigo usam a biblioteca de cliente do Python DB para Cloud Datastore, o que não é mais recomendado. Os desenvolvedores de novos aplicativos são altamente incentivados a usar a biblioteca de cliente do NDB, que oferece diversos benefícios em comparação a esta biblioteca de cliente, como o armazenamento em cache automático de entidades por meio da API Memcache. Se você estiver usando a antiga biblioteca de cliente de DB, leia o Guia de migração de DB para NDB.

Índice

Consistência eventual e NoSQL
Consistência eventual no Cloud Datastore
Consulta de ancestral e grupo de entidades
Limitações do grupo de entidades e da consulta de ancestral
Alternativas às consultas de ancestral
Como minimizar o tempo para atingir a consistência completa
Conclusão
Outros recursos

Consistência eventual e NoSQL

Os bancos de dados não relacionais, também conhecidos como bancos de dados NoSQL, surgiram nos últimos anos como uma alternativa aos bancos de dados relacionais. O Cloud Datastore é um dos bancos de dados não relacionais mais usados no setor. Em 2013, o Cloud Datastore processou 4,5 trilhões de transações por mês (postagem do blog do Google Cloud Platform). Ele oferece aos desenvolvedores uma maneira simplificada de armazenar e acessar dados. O esquema flexível gera naturalmente o mapeamento para linguagens de script e orientadas a objeto. O Cloud Datastore também oferece diversos recursos que os bancos de dados relacionais não são totalmente adequados para oferecer. Isso inclui o alto desempenho em uma escala muito grande e a alta confiabilidade.

Para desenvolvedores mais habituados com os bancos de dados relacionais, talvez seja um desafio projetar um sistema que utilize bancos de dados não relacionais. Isso porque algumas características e práticas deles são relativamente desconhecidas para os desenvolvedores. O modelo de programação do Cloud Datastore é simples, mas é importante conhecer essas características. A consistência eventual é uma dessas características, e a programação dela é o principal assunto deste documento.

O que é a consistência eventual?

Consistência eventual é uma garantia teórica de que, desde que não sejam feitas novas atualizações em uma entidade, todas as leituras da entidade em algum momento retornarão o último valor atualizado. O Sistema de Nome de Domínio (DNS, na sigla em inglês) da Internet é um exemplo bem conhecido de sistema com um modelo de consistência eventual. Os servidores DNS não necessariamente refletem os valores mais recentes, mas os valores são armazenados em cache e replicados em muitos diretórios na Internet. É necessário um certo tempo para replicar os valores modificados em todos os servidores e clientes DNS. No entanto, o sistema DNS é extremamente bem-sucedido e se tornou uma das bases da Internet. Ele é altamente disponível e provou ser extremamente escalável, permitindo pesquisas de nome para mais cem milhões de dispositivos em toda a Internet.

A Figura 1 ilustra o conceito de replicação com consistência eventual. O diagrama ilustra que, embora as réplicas estejam sempre disponíveis para leitura, algumas delas podem estar inconsistentes com a última gravação no nó de origem, em um determinado momento. No diagrama, o nó A é o nó de origem e os nós B e C são as réplicas.

Figura 1: representação conceitual da replicação com consistência eventual

Em contraste, os bancos de dados relacionais tradicionais foram projetados com base no conceito de consistência forte, também chamado de consistência imediata. Isso significa que os dados visualizados imediatamente após a atualização serão consistentes para todos os observadores da entidade. Essa característica tem sido uma pressuposição fundamental para muitos desenvolvedores que usam bancos de dados relacionais. Porém, para atingir a consistência forte, os desenvolvedores precisam comprometer a escalabilidade e o desempenho dos seus aplicativos. Em outras palavras, os dados precisam ser bloqueados durante o período de atualização ou processo de replicação para garantir que nenhum outro processo atualize os mesmos dados.

A Figura 2 mostra uma visualização conceitual da topologia de implantação e processo de replicação com consistência forte. Nesse diagrama, é possível ver como as réplicas sempre têm valores consistentes com o nó de origem, mas não podem ser acessadas até que a atualização seja concluída.

Figura 2: representação conceitual da replicação com consistência forte

Como equilibrar consistência forte e eventual

Os bancos de dados não relacionais se tornaram populares recentemente, em especial para aplicativos da Web que exigem alta escalabilidade e desempenho com alta disponibilidade. Os bancos de dados não relacionais permitem que os desenvolvedores selecionem um equilíbrio ideal entre a consistência forte e a ideal para cada aplicativo. Isso concede aos desenvolvedores a possibilidade de combinar os benefícios dos dois mundos. Por exemplo, informações como "saber quem na sua lista de amigos está on-line em um determinado momento" ou "saber quantos usuários marcaram sua postagem com +1" são casos de uso em que a consistência forte não é necessária. É possível fornecer escalabilidade e desempenho para esses casos de uso utilizando a consistência eventual. Casos de uso que exigem consistência forte incluem informações como "se um usuário concluiu ou não o processo de faturamento" ou "o número de pontos que um jogador fez durante uma sessão de combate".

Para generalizar os exemplos dados, os casos de uso com números de entidades muito grandes geralmente sugerem que a consistência eventual é o melhor modelo. Se houver um número de resultados muito grande em uma consulta, a experiência do usuário talvez não seja afetada pela inclusão ou exclusão de entidades específicas. Por outro lado, casos de uso com um número de entidades pequeno e um contexto limitado sugerem que a consistência forte é necessária. A experiência do usuário será afetada, já que o contexto deixará os usuários cientes de quais entidades precisam ser incluídas ou excluídas.

Por essas razões, é importante para os desenvolvedores entenderem as características não relacionais do Cloud Datastore. As seções a seguir discutem como os modelos de consistência forte e eventual podem ser combinados para criar um aplicativo escalável, de alta disponibilidade e alto desempenho. Com isso, os requisitos de consistência para uma experiência de usuário positiva ainda serão atendidos.

Consistência eventual no Cloud Datastore

É necessário selecionar a API correta quando uma visualização de dados fortemente consistente for exigida. Os diferentes tipos de APIs de consulta do Cloud Datastore e os modelos de consistência correspondentes são mostrados na Tabela 1.

Cloud Datastore API

Leitura do valor de entidade

Leitura de índice

Consulta global

Consistência eventual

Consistência eventual

Consulta global apenas de chaves

N/D

Consistência eventual

Consulta de ancestral

Consistência forte

Consistência forte

Pesquisa por chave (get())

Consistência forte

N/D

Tabela 1: chamadas de "get"/consultas do Cloud Datastore e possíveis comportamentos de consistência

As consultas do Cloud Datastore sem um ancestral são conhecidas como consultas globais e são projetadas para funcionar com um modelo de consistência eventual. Isso não garante a consistência forte. Uma consulta global apenas de chaves é uma que retorna apenas as chaves das entidades que correspondem à consulta, não os valores de atributo das entidades. Uma consulta de ancestral determina o âmbito da consulta com base em uma entidade de ancestral. As seções a seguir abordam cada comportamento de consistência mais detalhadamente.

Consistência eventual na leitura dos valores de entidade

Com exceção das consultas de ancestral, um valor de entidade atualizado talvez não apareça imediatamente em uma consulta. Para entender o impacto da consistência eventual ao ler os valores de entidade, considere um cenário em que uma entidade, Player, possui uma propriedade, Score. Considere, por exemplo, que a Score inicial tem o valor de 100. Após um tempo, o valor de Score é atualizado para 200. Se uma consulta global for feita com a mesma entidade Player no resultado, é possível que o valor da propriedade Score da entidade retornada apareça inalterado, em 100.

Esse comportamento é causado pela replicação entre os servidores do Cloud Datastore. A replicação é gerenciada pelo Bigtable e pelo Megastore, as tecnologias subjacentes do Google Cloud Datastore. Consulte Recursos adicionais para mais detalhes sobre Cloud Bigtable e Megastore. A replicação é realizada com o algoritmo Paxos, que espera de maneira síncrona até que a maioria das réplicas reconheça a solicitação de atualização. A réplica é atualizada com os dados da solicitação após um período. Esse período normalmente é curto, mas não há garantias quanto à duração real. A consulta pode ler os dados desatualizados caso seja executada antes da conclusão da atualização.

Em muitos casos, a atualização atingirá todas as réplicas muito rapidamente. No entanto, há diversos fatores que, quando combinados, podem aumentar o tempo necessário para atingir a consistência. Esses fatores incluem quaisquer incidentes que afetem todo o datacenter que envolvam a comutação de uma grande quantidade de servidores entre datacenters. Considerando a variação desses fatores, é impossível fornecer quaisquer requisitos de tempo definitivos para o estabelecimento da consistência completa.

O tempo necessário para uma consulta retornar o valor mais recente normalmente é bastante curto. Porém, em raras situações em que a latência de replicação aumenta, o tempo pode ser muito maior. Os aplicativos que usam as consultas globais do Cloud Datastore precisam ser desenvolvidos cuidadosamente para lidar com esses casos apropriadamente.

A consistência eventual ao ler os valores de entidade pode ser evitada com o uso de uma consulta apenas de chaves, uma de ancestral ou a pesquisa por chave (o método "get()"). Discutiremos sobre esses diferentes tipos de consulta mais detalhadamente a seguir.

Consistência eventual na leitura de um índice

É possível que um índice ainda não esteja atualizado no momento da realização de uma consulta global. Isso significa que, mesmo que seja possível ler os valores de propriedade mais recentes das entidades, a "lista de entidades" incluída no resultado da consulta talvez seja filtrada com base nos valores de índice antigos.

Para entender o impacto da consistência eventual na leitura de um índice, imagine um cenário em que uma nova entidade, Player, é inserida no Cloud Datastore. A entidade tem uma propriedade, Score, com o valor inicial de 300. Imediatamente após a inserção, você executa uma consulta apenas de chaves para buscar todas as entidades com um valor de Score maior que 0. Seria esperado que a entidade Player, recentemente inserida, fosse exibida nos resultados da consulta. Em vez disso, talvez inesperadamente, você pode descobrir que a entidade Player não aparece nos resultados. Essa situação pode ocorrer quando a tabela de índice da propriedade Score ainda não tiver sido atualizada com o valor recém-inserido no momento da consulta.

Lembre-se de que todas as consultas no Cloud Datastore são feitas nas tabelas de índice, mas as atualizações nessas tabelas são assíncronas. Essencialmente, todas as atualizações de entidade são compostas por duas fases. Na primeira fase, a fase de realização, é realizada uma gravação no registro de transação. Na segunda fase, os dados são gravados e os índices são atualizados. Se a fase de realização for realizada corretamente, o sucesso da fase de gravação é garantido, mas não deve ocorrer imediatamente. Se você consultar uma entidade antes da atualização dos índices, poderá visualizar dados que ainda não estão consistentes.

Como resultado desse processo de duas fases, há um tempo de espera antes que as últimas atualizações das entidades se tornem visíveis em consultas globais. Assim como para a consistência eventual do valor de entidade, o tempo de espera normalmente é pequeno, mas pode ser maior (até mesmo vários minutos, em circunstâncias excepcionais).

O mesmo também pode acontecer após as atualizações. Por exemplo, suponha que você atualize uma entidade existente, Player, com um novo valor de propriedade Score de 0 e execute a mesma consulta imediatamente após. Seria esperado que a entidade não aparecesse nos resultados da consulta porque o novo valor 0 de Score iria excluí-la. No entanto, devido ao mesmo comportamento de atualização de índice assíncrono, a entidade ainda pode ser incluída no resultado.

É possível evitar a consistência eventual na leitura de um índice apenas com o uso de uma consulta de ancestral ou do método de pesquisa por chave. Uma consulta apenas de chaves não pode evitar esse comportamento.

Consistência forte na leitura dos valores de entidade e índices

No Cloud Datastore, há apenas duas APIs que oferecem uma visualização fortemente consistente para a leitura de valores de entidade e índices: (1) o método de pesquisa por chave e (2) a consulta de ancestral. Se a lógica do aplicativo exigir consistência forte, o desenvolvedor precisará usar um desses métodos para ler as entidades do Cloud Datastore.

O Cloud Datastore foi projetado especialmente para oferecer consistência forte nessas APIs. Ao chamar qualquer uma delas, ele descarrega todas as atualizações pendentes em uma das réplicas e tabelas de índice e faz a consulta de ancestral ou a pesquisa. Dessa forma, o valor mais recente da entidade, baseado na tabela de índice atualizada, sempre será retornado com os valores baseados nas últimas atualizações.

A chamada da pesquisa por chave, em comparação com as consultas, retorna apenas uma entidade ou um conjunto de entidades especificado por uma chave ou conjunto de chaves. Isso significa que uma consulta de ancestral é a única maneira de atender ao requisito de consistência forte em conjunto com um requisito de filtragem no Cloud Datastore. No entanto, as consultas de ancestral não funcionam sem a especificação de um grupo de entidades.

Consulta de ancestral e grupo de entidades

Conforme discutido no início deste documento, um dos benefícios do Cloud Datastore é que os desenvolvedores podem encontrar o equilíbrio ideal entre a consistência forte e a eventual. No Google Cloud Datastore, um grupo de entidades é uma unidade com consistência forte, transacionalidade e localidade. Ao utilizar os grupos de entidades, os desenvolvedores podem definir o escopo da consistência forte entre as entidades em um aplicativo. Dessa maneira, o aplicativo pode manter a consistência dentro do grupo de entidades, ao mesmo tempo que atinge alta escalabilidade, disponibilidade e desempenho como um sistema completo.

Um grupo de entidades é uma hierarquia composta por uma entidade raiz e as filhas ou sucessoras dessa entidade.[1] Para criar um grupo de entidades, o desenvolvedor especifica um caminho de ancestral, que é, basicamente, uma série de chaves mãe que atribuem prefixo à chave filha. O conceito do grupo de entidades é ilustrado na Figura 3. Nesse caso, a entidade raiz com a chave "ateam" tem duas filhas com as chaves "ateam/098745" e "ateam/098746".

Figura 3: visualização esquemática do conceito de grupo de entidades

Dentro do grupo de entidades, estas características são garantidas:

  • consistência forte
    • Uma consulta de ancestral no grupo de entidades retornará um resultado fortemente consistente. Dessa forma, ela reflete os valores de entidade mais recentes filtrados pelo último estado de índice.
  • transacionalidade
    • Ao demarcar uma transação de maneira programática, o grupo de entidades fornece as características de ACID (atomicidade, consistência, isolamento e durabilidade) na transação.
  • localidade
    • As entidades de um grupo de entidades são armazenadas em lugares fisicamente próximos nos servidores do Cloud Datastore, já que todas as entidades são classificadas e armazenadas pela ordem lexicográfica das chaves. Assim, uma consulta de ancestral pode analisar rapidamente o grupo de entidades com o mínimo de E/S.

Uma consulta de ancestral é uma forma especial de consulta executada apenas em relação a um grupo de entidades especificado. Ela é feita com consistência forte. Nos bastidores, o Cloud Datastore garante que todas as atualizações de índice e replicações pendentes sejam aplicadas antes da realização da consulta.

Exemplo de consulta de ancestral

Nesta seção, descrevemos como usar os grupos de entidades e as consultas de ancestral na prática. No exemplo a seguir, examinamos o problema do gerenciamento de registros de dados para pessoas. Suponha que temos um código que adiciona uma entidade de um tipo específico seguido imediatamente por uma consulta sobre aquele tipo. Esse conceito é demonstrado pelo código Python de exemplo a seguir.

# Define the Person entity
class Person(db.Model):
    given_name = db.StringProperty()
    surname = db.StringProperty()
    organization = db.StringProperty()
# Add a person and retrieve the list of all people
class MainPage(webapp2.RequestHandler):
    def post(self):
        person = Person(given_name='GI', surname='Joe', organization='ATeam')
        person.put()
        q = db.GqlQuery("SELECT * FROM Person")
        people = []
        for p in q.run():
            people.append({'given_name': p.given_name,
                        'surname': p.surname,
                        'organization': p.organization})

O problema com esse código é que, na maioria dos casos, a consulta não retornará a entidade adicionada na instrução acima dele. Visto que a consulta está na linha imediatamente após a inserção, o índice não estará atualizado quando a consulta for executada. No entanto, há também um problema com a validade desse caso de uso: realmente existe a necessidade de retornar uma lista de todas as pessoas em uma página sem nenhum contexto? E se houver um milhão de pessoas? A página demoraria muito para retornar.

A natureza do caso de uso sugere que devemos fornecer contexto para restringir a consulta. Neste exemplo, o contexto que usaremos será a organização. Com isso, poderemos usar a organização como o grupo de entidades e executar uma consulta de ancestral, que resolve nosso problema de consistência. Isso é demonstrado pelo código Python abaixo.

class Organization(db.Model):
    name = db.StringProperty()
class Person(db.Model):
    given_name = db.StringProperty()
    surname = db.StringProperty()
class MainPage(webapp2.RequestHandler):
    def post(self):
        org = Organization.get_or_insert('ateam', name='ATeam')
        person = Person(parent=org)
        person.given_name='GI'
        person.surname='Joe'
        person.put()
        q = db.GqlQuery("SELECT * FROM Person WHERE ANCESTOR IS :1 ", org)
        people = []
        for p in q.run():
            people.append({'given_name': p.given_name,
                        'surname': p.surname})

Dessa vez, com o ancestral org especificado em GqlQuery, a consulta retorna a entidade recém-inserida. O exemplo poderia ser estendido para detalhar uma pessoa específica consultando o nome dela com o ancestral como parte da consulta. Ou isso também poderia ter sido feito salvando a chave da entidade e usando-a para o detalhamento com uma pesquisa por chave.

Como manter a consistência entre o Memcache e o Cloud Datastore

Também é possível usar os grupos de entidades como uma unidade para manter a consistência entre as entradas do Memcache e as entidades do Cloud Datastore. Por exemplo, considere um cenário em que você conta o número de pessoas em cada equipe e armazena no Memcache. Para garantir que os dados armazenados em cache estejam consistentes com os valores mais recentes no Cloud Datastore, você pode usar os metadados do grupo de entidades. Os metadados retornam o número da versão mais recente do grupo de entidades especificado. É possível comparar o número da versão com o número armazenado no Memcache. Ao usar esse método, você detecta uma alteração em qualquer uma das entidades em todo o grupo de entidades fazendo a leitura de um conjunto de metadados, em vez de verificar todas as entidades individuais no grupo.

Limitações do grupo de entidades e da consulta de ancestral

A abordagem do uso de grupos de entidades e consultas de ancestral não é uma solução milagrosa. Há dois desafios na prática que dificultam a aplicação da técnica em geral, conforme indicado a seguir.

  1. Há um limite de uma atualização por segundo de gravação para cada grupo de entidades.
  2. A relação do grupo de entidades não pode ser alterada após a criação da entidade.

Limite de gravação

Um desafio considerável é projetar o sistema para conter o número de atualizações ou transações em cada grupo de entidades. O limite aceito é o de uma atualização por segundo, por grupo de entidades.[2] Se o número de atualizações precisar ultrapassar esse limite, o grupo de entidades talvez represente um gargalo de desempenho.

No exemplo acima, é possível que cada organização precise atualizar o registro de uma pessoa da organização. Considere um cenário em que há 1.000 pessoas em "ateam" e cada uma delas pode realizar uma atualização por segundo em qualquer uma das propriedades. Como resultado, podem haver até 1.000 atualizações por segundo em todo o grupo de entidades, algo que não seria possível em virtude do limite de atualizações. Isso ilustra que é importante escolher um design de grupo de entidades adequado que considere os requisitos de desempenho. Esse é um dos desafios de encontrar o equilíbrio ideal entre a consistência forte e a eventual.

Imutabilidade das relações do grupo de entidades

O segundo desafio é a imutabilidade das relações do grupo de entidades. A relação do grupo de entidades é criada estaticamente com base na nomenclatura da chave. Ela não pode ser alterada após a criação da entidade. A única opção disponível para alterar a relação é excluir as entidades em um grupo de entidades e criá-las novamente. Esse desafio impede o uso de grupos de entidades para definir escopos ad hoc para consistência ou transacionalidade de maneira dinâmica. Em vez disso, o escopo de consistência e transacionalidade são intimamente ligados ao grupo de entidades estático definido no momento do design.

Por exemplo, considere um cenário em que você quer implementar uma transferência eletrônica entre duas contas bancárias. Esse cenário de negócios requer consistência forte e transacionalidade. Porém, não é possível reunir as duas contas em um grupo de entidades de última hora ou se basear em uma mãe global. Esse grupo de entidades criaria um afunilamento para todo o sistema que impediria a execução de outras solicitações de transferência eletrônica. Portanto, não é possível usar os grupos de entidades dessa maneira.

Existe uma alternativa para implementar uma transferência eletrônica com escalabilidade eficiente e boa disponibilidade. Em vez de colocar todas as contas em um único grupo de entidades, você pode criar um grupo de entidades para cada conta. Ao fazer isso, você pode usar transações para garantir atualizações ACID para as duas contas bancárias. As transações são um recurso do Cloud Datastore que permite criar conjuntos de operações com características ACID para até 25 grupos de entidades. Observe que, em uma transação, você precisa usar consultas fortemente consistentes, como pesquisas por consultas de chave e de ancestral. Para mais informações sobre restrições de transações, consulte Transações e grupos de entidades.

Alternativas às consultas de ancestral

Caso você já tenha um aplicativo com uma grande quantidade de entidades armazenadas no Cloud Datastore, talvez seja difícil incorporar grupos de entidades posteriormente em um exercício de refatoração. Seria necessário excluir todas as entidades e adicioná-las a uma relação de grupo de entidades. Portanto, na modelagem de dados para o Cloud Datastore, é importante tomar uma decisão sobre o design do grupo de entidades na fase inicial de design do aplicativo. Caso contrário, é possível que você fique limitado na refatoração a outras alternativas para atingir um determinado nível de consistência, como uma consulta apenas de chaves seguida por uma pesquisa por chave ou usando o Memcache.

Consulta global apenas de chaves seguida pela pesquisa por chave

Uma consulta global apenas de chaves é um tipo especial de consulta global que retorna apenas chaves, sem os valores de propriedade das entidades. Como os valores retornados são apenas chaves, a consulta não envolve um valor de entidade com um possível problema de consistência. A combinação da consulta global apenas de chaves com o método de pesquisa lê os valores de entidade mais recentes. Porém, a consulta global apenas de chaves não é capaz de excluir a possibilidade do índice ainda não estar consistente no momento da consulta, o que pode fazer com que a entidade não seja recuperada de nenhuma forma. O resultado da consulta poderia ser gerado com base na filtragem de valores de índice antigos. Em resumo, o desenvolvedor pode usar a consulta global apenas de chaves seguida pela pesquisa por chave quando o requisito de um aplicativo permitir que o valor de índice ainda não seja consistente no momento da pesquisa.

Como usar o Memcache

O serviço do Memcache é volátil, mas fortemente consistente. Portanto, combinando as pesquisas do Memcache com as consultas do Cloud Datastore, é possível criar um sistema que minimizará os problemas de consistência na maioria das vezes.

Por exemplo, considere o cenário de um aplicativo de jogo que mantém uma lista de entidades Player, cada uma com a pontuação maior que zero.

  • Para solicitações de inserção ou atualização, aplique-as na lista de entidades Player no Memcache e no Cloud Datastore.
  • Para solicitações de consulta, leia a lista de entidades Player do Memcache e faça uma consulta apenas de chaves no Cloud Datastore quando a lista não estiver presente no Memcache.

A lista retornada será consistente sempre que a armazenada em cache estiver presente no Memcache. Se a entrada tiver sido removida ou o serviço do Memcache estiver temporariamente indisponível, o sistema precisará ler o valor de uma consulta do Cloud Datastore, que talvez retorne um resultado inconsistente. Essa técnica pode ser usada em qualquer aplicativo com tolerância a um pouco de inconsistência.

Há algumas práticas recomendadas para uso do Memcache, como uma camada de armazenamento em cache para o Cloud Datastore:

  • Detectar exceções e erros do Memcache para manter a consistência entre o valor do Memcache e do Cloud Datastore. Se você receber uma exceção ao atualizar a entrada no Memcache, invalide a entrada antiga nele. Caso contrário, talvez haja valores diferentes para uma entidade, ou seja, um valor antigo no Memcache e um novo no Cloud Datastore.
  • Definir um período de validade nas entradas do Memcache. É recomendável definir períodos curtos para a validade de cada entrada para minimizar a possibilidade de inconsistência no caso de exceções do Memcache.
  • Usar o recurso de comparar e definir ao atualizar as entradas para o controle de simultaneidade. Isso ajudará a garantir que atualizações simultâneas na mesma entrada não interfiram umas nas outras.

Migração gradual para grupos de entidades

As sugestões apresentadas na seção anterior apenas diminuem a possibilidade do comportamento inconsistente. Quando a consistência forte for necessária, é melhor projetar o aplicativo com base em grupos de entidades e consultas de ancestral. No entanto, pode não ser possível migrar um aplicativo existente, o que pode incluir a mudança da lógica do aplicativo e do modelo de dados existente de consultas globais para consultas de ancestral. Uma maneira de conseguir isso é adotar um processo de transição gradual, como este:

  1. Identificar e priorizar as funções do aplicativo que exijam consistência forte.
  2. Escrever a nova lógica para as funções insert() ou update() usando grupos de entidades juntamente com a lógica existente, em vez de substitui-la. Dessa forma, todas as novas inserções ou atualizações nos novos grupos de entidades e nas entidades antigas podem ser tratadas pela função adequada.
  3. Modificar a lógica existente para as funções de consulta ou leitura. As consultas de ancestral são executadas primeiro se houver um novo grupo de entidades para a solicitação. Executar a consulta global antiga como a lógica substitua se o grupo de entidades não existir.

Essa estratégia permite a migração gradual de um modelo de dados existente para um novo modelo com base em grupos de entidades que minimiza o risco de problemas causados pela consistência eventual. Na prática, essa abordagem depende dos requisitos e casos de uso específicos para sua aplicação a um sistema real.

Substituição pelo modo degradado

Atualmente, é difícil detectar uma situação de maneira programática quando um aplicativo apresenta consistência deteriorada. No entanto, se você usar outros meios para determinar quando um aplicativo apresenta consistência deteriorada, será possível implementar um modo degradado que pode ser ativado e desativado para acionar algumas áreas da lógica do aplicativo que exijam consistência forte. Por exemplo, em vez de mostrar um resultado de consulta inconsistente em uma tela de relatório de faturamento, é possível mostrar uma mensagem de manutenção nessa tela específica. Dessa forma, os outros serviços do aplicativo podem continuar em funcionamento e, assim, reduzir o impacto na experiência do usuário.

Como minimizar o tempo para atingir a consistência completa

Em um aplicativo grande com milhões de usuários ou terabytes de entidades do Cloud Datastore, o uso inadequado do Cloud Datastore pode levar à consistência deteriorada. Essas práticas incluem:

  • numeração sequencial em chaves de entidade;
  • muitos índices.

Essas práticas não afetam aplicativos pequenos. Porém, uma vez que o aplicativo fique muito grande, essas práticas aumentam a possibilidade de tempos maiores necessários para a consistência. Portanto, é melhor evitá-las nos estágios iniciais do design do aplicativo.

Antipadrão 1: numeração sequencial de chaves de entidade

Antes do lançamento do App Engine SDK 1.8.1, o Cloud Datastore usava uma sequência de pequenos códigos de números inteiros, com padrões geralmente consecutivos, como os nomes de chave padrão gerados automaticamente. Em alguns documentos, isso é chamado de "política legada" para a criação de quaisquer entidades sem um aplicativo especificado no nome da chave. Essa política legada gera nomes de chave de entidade com numeração sequencial, como 1000, 1001, 1002, por exemplo. No entanto, como vimos anteriormente, o Cloud Datastore armazena as entidades pela ordem lexicográfica dos nomes das chaves. Assim, é bastante provável que essas entidades sejam armazenadas nos mesmos servidores do Cloud Datastore. Se um aplicativo atrair um tráfego muito alto, essa numeração sequencial poderá gerar uma concentração de operações em um servidor específico, o que talvez resulte em uma latência maior para a consistência.

No App Engine SDK 1.8.1, o Cloud Datastore introduziu um novo método de numeração de código com uma política padrão que usa códigos dispersos (veja a documentação de referência). Essa política padrão gera uma sequência aleatória de códigos com até 16 dígitos que são distribuídas de maneira quase uniforme. Com o uso dessa política, é provável que o tráfego de um aplicativo grande seja melhor distribuído entre um conjunto de servidores do Cloud Datastore, com um tempo menor para atingir a consistência. A menos que seu aplicativo exija especificamente a compatibilidade com a política legada, a política padrão é recomendada.

Se você definir explicitamente nomes de chave nas entidades, deverá projetar o esquema de nomenclatura para acessar as entidades de maneira uniforme em todo intervalo de nomes de chave. Em outras palavras, não concentre o acesso em um intervalo específico, uma vez que a classificação é feita de acordo com a ordem lexicográfica dos nomes das chaves. Caso contrário, pode ocorrer o mesmo problema apresentado pela numeração sequencial.

Para entender a distribuição irregular do acesso no keyspace, considere um exemplo em que as entidades são criadas com nomes de chave sequenciais, conforme mostrado neste código:

p1 = Person(key_name='0001')
p2 = Person(key_name='0002')
p3 = Person(key_name='0003')
...

O padrão de acesso do aplicativo pode criar um "hot spot" em um determinado intervalo de nomes de chave, como no caso da concentração do acesso nas entidades "Person" recém-criadas. Nesse caso, todas as chaves acessadas com frequência terão códigos maiores. A carga pode ser concentrada em um servidor específico do Cloud Datastore.

De maneira alternativa, para entender a distribuição uniforme no keyspace, considere o uso de strings longas e aleatórias para os nomes de chave. Isso é ilustrado no exemplo abaixo:

p1 = Person(key_name='t9P776g5kAecChuKW4JKCnh44uRvBDhU')
p2 = Person(key_name='hCdVjL2jCzLqRnPdNNcPCAN8Rinug9kq')
p3 = Person(key_name='PaV9fsXCdra7zCMkt7UX3THvFmu6xsUd')
...

Agora as entidades "Person" recém-criadas estão dispersas no keyspace e em diversos servidores. Isso pressupõe que há um número suficientemente grande de entidades "Person".

Antipadrão 2: muitos índices

No Cloud Datastore, uma atualização em uma entidade levará à atualização de todos os índices definidos para esse tipo de entidade. Veja o artigo Duração de uma gravação no armazenamento de dados para encontrar detalhes. Se um aplicativo usar diversos índices personalizados, uma atualização poderá envolver dezenas, centenas ou até mesmo milhares de atualizações nas tabelas de índice. Em um aplicativo grande, o uso excessivo de índices personalizados pode resultar em uma carga maior no servidor, além de aumentar a latência para atingir a consistência.

Na maioria dos casos, os índices personalizados são adicionados para permitir a compatibilidade com requisitos, como tarefas de análise de dados, solução de problemas e suporte ao cliente. O BigQuery é um mecanismo de consulta imensamente escalável que pode fazer consultas específicas em grandes conjuntos de dados sem os índices criados previamente. Ele é mais adequado para os casos de uso como suporte ao cliente, solução de problemas ou análises de dados que exigem consultas complexas do que o Cloud Datastore.

Uma prática recomendada é combinar o Cloud Datastore com o BigQuery para atender a diferentes requisitos de negócios. Use o Cloud Datastore no processamento transacional on-line (OLTP, na sigla em inglês) necessário para a lógica central do aplicativo e o BigQuery no processamento analítico on-line (OLAP, na sigla em inglês) para operações de back-end. Pode ser necessário implementar um fluxo contínuo de exportação de dados do Cloud Datastore ao BigQuery para mover os dados necessários para essas consultas.

Além de uma implementação alternativa para os índices personalizados, outra recomendação é especificar explicitamente as propriedades não indexadas. Consulte Tipos de valor e propriedades. Por padrão, o Cloud Datastore cria uma tabela de índice diferente para cada propriedade de um tipo de entidade. Se você tiver 100 propriedades de um tipo, haverá 100 tabelas de índice para ele, além de 100 atualizações adicionais em cada atualização de uma entidade. Dessa forma, é uma prática recomendada definir as propriedades não indexadas onde for possível, caso elas não sejam necessárias para uma condição de consulta.

Além de apresentar menor possibilidade de haver aumentos de tempo para atingir a consistência, é possível que essas otimizações de índice resultem em uma grande redução dos custos de armazenamento do Cloud Datastore em um aplicativo grande que use os índices com frequência.

Conclusão

A consistência eventual é um elemento essencial dos bancos de dados não relacionais que permite que os desenvolvedores encontrem o equilíbrio ideal entre a escalabilidade, o desempenho e a consistência. É importante entender como lidar com o equilíbrio entre a consistência forte e a eventual para projetar um modelo de dados ideal para seu aplicativo. No Cloud Datastore, o uso de grupos de entidades e consultas de ancestral é a melhor maneira de garantir a consistência forte em um escopo de entidades. Caso não seja possível incorporar grupos de entidades com seu aplicativo em virtude das limitações descritas anteriormente, considere outras opções, como o uso de consultas apenas de chaves ou do Memcache. Para aplicativos grandes, aplique as práticas recomendadas, como o uso de códigos dispersos e indexação reduzida, para diminuir o tempo necessário para atingir a consistência. Também pode ser importante combinar o Cloud Datastore com o BigQuery para atender aos requisitos de negócios nas consultas complexas e reduzir o uso de índices do Cloud Datastore o máximo possível.

Outros recursos

Estes recursos oferecem mais informações sobre os tópicos discutidos neste documento:




[1] Um grupo de entidades pode ser formado até mesmo especificando apenas uma chave da entidade pai ou raiz, sem armazenar as entidades reais do pai ou raiz, porque todas as funções do grupo de entidades são implementadas com base nas relações entre as chaves.

[2] O limite aceito é o de uma atualização por segundo por grupo de entidades fora das transações ou uma transação por segundo por grupo de entidades. Ao agregar várias atualizações a uma transação, você ficará limitado ao tamanho máximo de transação de 10 MB e à taxa máxima de gravação do servidor do Datastore.

Esta página foi útil? Conte sua opinião sobre:

Enviar comentários sobre…

Documentação do Cloud Datastore