Padrões para manter a consistência em um sistema distribuído de origem de eventos?


12

Ultimamente, tenho lido sobre o fornecimento de eventos e realmente gosto das idéias por trás dele, mas estou com o seguinte problema.

Digamos que você tenha N processos simultâneos que recebem comandos (por exemplo, servidores web), geram eventos como resultado e os armazenam em um armazenamento centralizado. Vamos supor também que todo o estado transitório do aplicativo seja mantido na memória dos processos individuais, aplicando sequencialmente eventos do armazenamento.

Agora, digamos que temos a seguinte regra de negócios: cada usuário distinto deve ter um nome de usuário exclusivo.

Se dois processos recebem um comando de registro de usuário para o mesmo nome de usuário X, ambos verificam se X não está na lista de nomes de usuários, a regra é validada para ambos os processos e ambos armazenam um evento "novo usuário com nome de usuário X" na loja .

Agora entramos em um estado global inconsistente porque a regra de negócios é violada (há dois usuários distintos com o mesmo nome de usuário).

Em um sistema tradicional de estilo N servidor <-> 1 RDBMS, o banco de dados é usado como um ponto central de sincronização, o que ajuda a evitar essas inconsistências.

Minha pergunta é: como os sistemas de origem de eventos normalmente abordam esse problema? Eles simplesmente processam todos os comandos sequencialmente (por exemplo, limitam a quantidade de processos que podem ser gravados na loja para 1)?


1
Essa restrição é controlada pelo código ou é uma restrição de banco de dados? N eventos podem ou não ser processados ​​e despachados em sequência ... N eventos podem passar por validações ao mesmo tempo, sem que sejam descartados. Se o pedido for importante, você precisará sincronizar a validação. Ou, para usar a fila para enfileirar os eventos, despache-os sequencialmente #
Laiv

@Laiv right. Por simplicidade, assumi que não havia banco de dados, todos os estados mantidos na memória. Processar tipos específicos de comandos seqüencialmente por meio de uma fila seria uma opção, mas parece ser complexo decidir quais comandos podem afetar outros causalmente e eu provavelmente acabaria colocando todos os comandos na mesma fila, o que equivale a um único processo de processamento de comandos : / Por exemplo, se eu tiver um usuário adicionando comentário em uma postagem do blog, "excluir usuário", "suspender usuário", "excluir postagem do blog", "desativar comentários da postagem do blog" etc. etc. deverão permanecer na mesma fila.
Olivier Lalonde

1
Estou de acordo com você, trabalhar com filas ou semáforos não é simples. Nem para trabalhar com padrões de simultaneidade ou de origem de eventos. Mas basicamente todas as soluções acabam com o sistema de orquestração do tráfego de eventos. No entanto, é um paradigma interessante. Também existem caches externos orientados a tuplas como Redis que podem ajudar a gerenciar esse tráfego entre nós, como armazenar em cache o último estado de uma entidade ou se essa entidade está sendo processada no momento. Caches compartilhados são bastante comuns nesse tipo de desenvolvimento. Pode parecer complexo, mas não giveup ;-) é bastante interessante
LAIV

Respostas:


6

Em um sistema tradicional de estilo N servidor <-> 1 RDBMS, o banco de dados é usado como um ponto central de sincronização, o que ajuda a evitar essas inconsistências.

Nos sistemas de origem de eventos, o "armazenamento de eventos" desempenha a mesma função. Para um objeto de origem de evento, sua gravação é um acréscimo de seus novos eventos a uma versão específica do fluxo de eventos. Portanto, assim como na programação simultânea, você pode adquirir um bloqueio nesse histórico ao processar o comando. É mais comum que os sistemas de origem de eventos adotem uma abordagem mais otimista - carregue o histórico anterior, calcule o novo histórico e compare-e-troque. Se algum outro comando também tiver sido gravado nesse fluxo, sua comparação e troca falharão. A partir daí, você reexecuta seu comando ou abandona seu comando, ou talvez mescla seus resultados ao histórico.

A contenção se torna um grande problema se todos os N servidores com seus comandos M estiverem tentando gravar em um único fluxo. A resposta usual aqui é alocar um histórico para cada entidade originada por evento em seu modelo. Portanto, o usuário (Bob) teria um histórico distinto do usuário (Alice) e as gravações em um não bloquearão as gravações no outro.

Minha pergunta é: como os sistemas de origem de eventos normalmente abordam esse problema? Eles simplesmente processam todos os comandos sequencialmente?

Greg Young na validação de conjunto

Existe uma maneira elegante de verificar restrições exclusivas nos atributos do objeto de domínio sem mover a lógica de negócios para a camada de serviço?

Resposta curta, em muitos casos, investigar mais profundamente esse requisito revela que (a) é um proxy pouco compreendido para outro requisito, ou (b) que violações da "regra" são aceitáveis ​​se puderem ser detectadas (relatório de exceção) , mitigados dentro de uma janela de tempo ou com baixa frequência (por exemplo: os clientes podem verificar se um nome está disponível antes de enviar um comando para usá-lo).

Em alguns casos, onde seu armazenamento de eventos é bom na validação de conjunto (ou seja: um banco de dados relacional), você implementa o requisito gravando em uma tabela de "nomes exclusivos" na mesma transação que persiste nos eventos.

Em alguns casos, você só pode impor o requisito publicando todos os nomes de usuário no mesmo fluxo (o que permite avaliar o conjunto de nomes na memória, como parte do seu modelo de domínio). - Nesse caso, dois processos atualizarão a tentativa de atualizar "o" histórico do fluxo, mas uma das operações de comparação e troca falhará e a nova tentativa desse comando poderá detectar o conflito.


1) Obrigado pelas sugestões e referências. Quando você diz "comparar e trocar", você quer dizer que, no momento de armazenar um evento, o processo detectaria a chegada de novos eventos desde o início do processamento do comando? Eu acho que isso exigiria um armazenamento de eventos que suporte a semântica "compare-and-swap", correto? (por exemplo, "escreva este evento apenas e somente se o último evento tiver o ID X")?
Olivier Lalonde

2) Também gosto da ideia de aceitar inconsistências temporárias e repará-las eventualmente, mas não tenho certeza de como codificaria isso de maneira confiável ... talvez tenha um processo dedicado que valide os eventos sequencialmente e crie eventos de reversão quando detectar algo deu errado? Obrigado!
Olivier Lalonde

(1) eu diria "nova versão da história" em vez de "novos eventos", mas você tem a idéia; só substitua a história se é a que estamos esperando.
VoiceOfUnreason

(2) Sim. É um pouco de lógica que lê eventos da loja em lotes e, no final do lote, transmite um relatório de exceção ("temos muitos usuários chamados Bob") ou envia comandos para compensar o problema (supondo que a resposta correta seja computável sem intervenção humana).
VoiceOfUnreason

2

Parece que você pode implementar um processo de negócios ( sagano contexto de Domain Driven Design) para o registro do usuário em que o usuário é tratado como a CRDT.

Recursos

  1. https://doc.akka.io/docs/akka/current/distributed-data.html http://archive.is/t0QIx

  2. "CRDTs com dados distribuídos da Akka" https://www.slideshare.net/markusjura/crdts-with-akka-distributed-data para obter informações sobre

    • CmRDTs - CRDTs baseados em operação
    • CvRDTs - CRTDs baseados no estado
  3. Exemplos de código no Scala https://github.com/akka/akka-samples/tree/master/akka-sample-distributed-data-scala . Talvez o "carrinho de compras" seja o mais adequado.

  4. Tour do Akka Cluster - Akka Distributed Data https://manuel.bernhardt.io/2018/01/03/tour-akka-cluster-akka-distributed-data/
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.