Semeando bancos de dados de microsserviços

10

Dado o serviço A (CMS) que controla um modelo (Produto, vamos assumir os únicos campos que ele possui são id, título, preço) e serviços B (Remessa) e C (E-mails) que precisam exibir um determinado modelo, qual deve ser a abordagem sincronizar as informações de modelo fornecidas nesses serviços na abordagem de fornecimento de eventos? Vamos supor que o catálogo de produtos raramente mude (mas mude) e que haja administradores que possam acessar dados de remessas e e-mails com muita frequência (as funcionalidades de exemplo são: B: display titles of products the order containede C display content of email about shipping that is going to be sent:). Cada um dos serviços possui seu próprio banco de dados.

Solução 1

Envie todas as informações necessárias sobre o produto no evento - isso significa a seguinte estrutura para order_placed:

{
    order_id: [guid],
    product: {
        id: [guid],
        title: 'Foo',
        price: 1000
    }
}

No serviço B e C, as informações do produto são armazenadas no productatributo JSON na orderstabela

Como tal, para exibir as informações necessárias, apenas os dados recuperados do evento são usados

Problemas : dependendo de quais outras informações precisam ser apresentadas em B e C, a quantidade de dados no evento pode aumentar. B e C podem não exigir as mesmas informações sobre o Produto, mas o evento precisará conter os dois (a menos que os separemos em dois). Se dados não estiverem presentes em determinado evento, o código não poderá ser usado - se adicionarmos uma opção de cor a determinado Produto, para pedidos existentes em B e C, o produto será incolor, a menos que atualizemos os eventos e os executemos novamente .

Solução 2

Enviar apenas guia do produto no evento - isso significa a seguinte estrutura para order_placed:

{
    order_id: [guid],
    product_id: [guid]
}

Nos serviços B e C, as informações do produto são armazenadas no product_idatributo na orderstabela

As informações do produto são recuperadas pelos serviços B e C quando necessário, executando uma chamada de API para o A/product/[guid]terminal

Problemas : isso torna B e C dependentes de A (o tempo todo). Se o esquema do Produto mudar em A, é necessário fazer alterações em todos os serviços que dependem deles (de repente)

Solução 3

Enviar apenas guia do produto no evento - isso significa a seguinte estrutura para order_placed:

{
    order_id: [guid],
    product_id: [guid]
}

Nos serviços B e C, as informações do produto são armazenadas na productstabela; ainda existe product_idna orderstabela, mas há replicação de productsdados entre A, B e C; B e C podem conter informações diferentes sobre o Produto e A

As informações do produto são propagadas quando os serviços B e C são criados e atualizados sempre que as informações sobre os Produtos são alteradas, fazendo uma chamada para o A/productterminal (que exibe as informações necessárias de todos os produtos) ou executando um acesso direto ao DB de A e copiando as informações necessárias do produto necessárias serviço.

Problemas : isso torna B e C dependentes de A (ao semear). Se o esquema do Produto mudar em A, é necessário fazer alterações em todos os serviços que dependem deles (quando propagação)


Pelo meu entendimento, a abordagem correta seria seguir a solução 1 e atualizar o histórico de eventos por determinada lógica (se o catálogo de produtos não mudou e queremos adicionar cores a serem exibidas, podemos atualizar com segurança o histórico para obter o estado atual de Produtos e preencha os dados ausentes nos eventos) ou atenda à inexistência de dados fornecidos (se o catálogo de produtos mudou e queremos adicionar cores a serem exibidas, não podemos ter certeza se naquele momento no passado, dado o produto tinha uma cor ou não - podemos supor que todos os produtos no catálogo anterior eram pretos e atendidos pela atualização de eventos ou código)

eithed
fonte
No que diz respeito a updating event history- No caso, o histórico de eventos é a sua fonte de verdade e nunca deve ser alterada, mas apenas seguir em frente. Se os eventos mudarem, você poderá usar a versão do evento ou soluções semelhantes, mas ao reproduzir seus eventos até um momento específico, o estado dos dados deve estar como estava naquele momento.
Não
Em relação ao armazenamento de dados (esquemas, etc.) para consulta e campos sendo adicionados / removidos, etc. usamos o cosmosDB armazenando os dados em JSON como estão naquele momento. A única coisa que precisa de controle de versão é eventos e / ou comandos. Você também precisa atualizar contratos de terminal e objetos de valor que contenham os dados que respondem às consultas de um cliente (Web, dispositivos móveis, etc ...). Os dados mais antigos que não possuem um campo terão um valor padrão ou em branco, o que sempre se adequa aos negócios, mas o histórico de eventos permanece intacto e apenas avança.
Não
@ updating event historyNão, quero dizer: passar por todos os eventos, copiando-os de um fluxo (v1) para outro fluxo (v2) para manter um esquema de eventos consistente.
eithed
Além disso, na área de comércio / comércio eletrônico, convém capturar o preço conforme indicado, uma vez que o preço é alterado com frequência. Um preço exibido ao usuário pode ser diferente no momento em que o pedido real é capturado. Existem várias maneiras de resolver o problema, mas é uma que deve ser considerada.
CPerson 2/03
@CPerson yup - o preço pode ser um dos atributos passados ​​no próprio evento. Por outro lado, o URL da imagem pode existir no evento (representando a intenção de display image at the point when purchase was made) ou não (representar a intenção de display current image as it within catalog)
eithed

Respostas:

3

A solução 3 está realmente próxima da idéia certa.

Uma maneira de pensar sobre isso: B e C estão cada um em cache cópias "locais" dos dados de que precisam. As mensagens processadas em B (e igualmente em C) usam as informações armazenadas em cache localmente. Da mesma forma, os relatórios são produzidos usando as informações armazenadas em cache localmente.

Os dados são replicados da origem para os caches por meio de uma API estável. B e C nem precisam usar a mesma API - eles usam o protocolo de busca apropriado para suas necessidades. Com efeito, definimos um contrato - protocolo e esquema de mensagens - que restringe o provedor e o consumidor. Qualquer consumidor desse contrato pode ser conectado a qualquer fornecedor. Alterações incompatíveis com versões anteriores exigem um novo contrato.

Os serviços escolhem a estratégia de invalidação de cache apropriada para suas necessidades. Isso pode significar extrair alterações da fonte em uma programação regular ou em resposta a uma notificação de que as coisas podem ter mudado, ou até "sob demanda" - atuando como um cache de leitura, voltando à cópia armazenada dos dados quando a fonte não está disponível.

Isso fornece "autonomia", no sentido de que B e C podem continuar a fornecer valor comercial quando A estiver temporariamente indisponível.

Leitura recomendada: Dados externos, Dados internos , Pat Helland 2005.

VoiceOfUnreason
fonte
Sim, concordo plenamente com o que você escreveu aqui e a solução 3 é a solução que eu apliquei, no entanto, não é a abordagem de fornecimento de eventos, pois, se reproduzirmos os eventos, não queremos necessariamente use o estado atual do produto; queremos usar o estado como estava no ponto do evento. Obviamente, isso pode ser bom (depende dos requisitos de negócios). Se, no entanto, desejamos acompanhar as alterações no catálogo, isso exige que a fonte de eventos também seja dependente da quantidade de dados, dependendo da quantidade de dados, é melhor voltarmos à solução 1.
eithed
11
Eu acho que você tem w / solução # 3. Se você precisar repetir a consistência com o catálogo, origem do evento também. Você só precisa reproduzir quando reinicializar, o que provavelmente está na inicialização - assim que estiver pronto, você só precisa olhar para novos eventos, para que a quantidade de dados provavelmente não seja um problema real. No entanto, mesmo assim, você tem a opção (se necessário) de usar pontos de verificação, ou seja, "aqui é o estado do evento 1.000", então você aceita isso e agora só precisa reproduzir o evento 1.001 a atual em vez de todo o histórico .
Mike B.
2

Há duas coisas difíceis na Ciência da Computação, e uma delas é a invalidação de cache.

A solução 2 é absolutamente minha posição padrão e, geralmente, você só deve considerar a implementação do cache se encontrar um dos seguintes cenários:

  1. A chamada da API ao Serviço A está causando problemas de desempenho.
  2. O custo do Serviço A estar baixo e incapaz de recuperar os dados é significativo para os negócios.

Os problemas de desempenho são realmente o principal driver. Existem muitas maneiras de solucionar o problema nº 2 que não envolvem armazenamento em cache, como garantir que o Serviço A esteja altamente disponível.

O armazenamento em cache adiciona complexidade significativa ao sistema e pode criar casos extremos difíceis de raciocinar e erros difíceis de replicar. Você também precisa reduzir o risco de fornecer dados obsoletos quando existirem dados mais novos, o que pode ser muito pior do ponto de vista comercial do que (por exemplo) exibir uma mensagem de que "O serviço A está inoperante - tente novamente mais tarde".

Deste excelente artigo de Udi Dahan:

Essas dependências surgem lentamente em você, amarrando os cadarços, diminuindo gradualmente o ritmo do desenvolvimento, prejudicando a estabilidade da sua base de código, onde alterações em uma parte do sistema quebram outras partes. É uma morte lenta por mil cortes e, como resultado, ninguém sabe exatamente qual a grande decisão que tomamos que fez tudo correr mal.

Além disso, se você precisar fazer uma consulta pontual dos dados do produto, isso deve ser tratado da maneira como os dados são armazenados no banco de dados do Produto (por exemplo, datas de início / término), deve ser claramente exposto na API (a data efetiva precisa ser uma entrada para a chamada da API para consultar os dados).

Phil Sandler
fonte
11
@SavvasKleanthous "A rede é confiável" é uma das falácias da computação distribuída. Mas a resposta a essa falácia não deve ser "armazenar em cache todos os dados de todos os serviços de qualquer outro serviço" (percebo que é um pouco hiperbólico). Espere que um serviço não esteja disponível e lide com isso como uma condição de erro. Se você tiver uma situação rara em que o Serviço A em queda tenha um grande impacto nos negócios, considere (com cuidado!) Outras opções.
Phil Sandler
11
O @SavvasKleanthous também considera (como mencionei na minha resposta) que retornar dados antigos em muitos casos pode ser muito pior do que gerar um erro.
Phil Sandler
11
@eithed Eu estava me referindo a este comentário: "Se, no entanto, queremos acompanhar as alterações no catálogo, isso também exige a terceirização de eventos". De qualquer forma, você tem a ideia certa - o serviço do Produto deve ser responsável por rastrear as alterações ao longo do tempo, não os serviços posteriores.
Phil Sandler
11
Além disso, o armazenamento de dados que você observa, embora tenha algumas semelhanças com o armazenamento em cache, não apresenta os mesmos problemas. Mais especificamente, a invalidação não é necessária; você obtém a nova versão dos dados quando isso acontece. O que você experimenta é consistência atrasada. No entanto, mesmo usando solicitação da Web, há uma janela de inconsistência (embora pequena).
Savvas Kleanthous
11
@SavvasKleanthous De qualquer forma, meu ponto principal não é tentar resolver problemas que ainda não existem, especialmente com soluções que tragam seus próprios problemas e riscos. A opção 2 é a solução mais simples e deve ser a opção padrão até que não atenda aos requisitos de negócios . Se você acha que escolher a solução mais simples que pode funcionar é (como você diz) "muito ruim", acho que simplesmente discordamos.
Phil Sandler
2

É muito difícil simplesmente dizer que uma solução é melhor que a outra. A escolha de uma entre as Soluções 2 e 3 depende de outros fatores (duração do cache, tolerância de consistência, ...)

Meus 2 centavos:

A invalidação do cache pode ser difícil, mas a declaração do problema menciona que o catálogo de produtos é alterado raramente. Esse fato torna os dados do produto um bom candidato para o cache

Solução 1 (NOK)

  • Os dados são duplicados em vários sistemas

Solução 2 (OK)

  • Oferece forte consistência
  • Funciona apenas quando o serviço do produto está altamente disponível e oferece bom desempenho
  • Se o serviço de email preparar um resumo (com muitos produtos), o tempo total de resposta poderá ser maior

Solução # 3 (complexa, mas preferida)

  • Prefira a abordagem da API em vez do acesso direto ao banco de dados para recuperar informações do produto
  • Serviços de consumo resiliente não são afetados quando o serviço do produto está inoperante
  • Os aplicativos consumidores (serviços de remessa e email) recuperam os detalhes do produto imediatamente após a publicação de um evento. A possibilidade de o serviço do produto cair dentro desses poucos milissegundos é muito remota.
Sudhir
fonte
1

De um modo geral, recomendo fortemente a opção 2 por causa do acoplamento temporal entre esses dois serviços (a menos que a comunicação entre esses serviços seja super estável e não muito frequente). O acoplamento temporal é o que você descreve como this makes B and C dependant upon A (at all times)e significa que, se A estiver inativo ou inacessível de B ou C, B e C não podem cumprir sua função.

Pessoalmente, acredito que as opções 1 e 3 têm situações em que são opções válidas.

Se a comunicação entre A e B e C for muito alta ou a quantidade de dados necessária para entrar no evento for grande o suficiente para torná-lo uma preocupação, a opção 3 é a melhor opção, porque a carga na rede é muito menor , e a latência das operações diminuirá à medida que o tamanho da mensagem diminuir. Outras preocupações a serem consideradas aqui são:

  1. Estabilidade do contrato: se o contrato da mensagem que deixa A mudasse com frequência, a colocação de muitas propriedades na mensagem resultaria em muitas mudanças nos consumidores. No entanto, neste caso, acredito que este não seja um grande problema porque:
    1. Você mencionou que o sistema A é um CMS. Isso significa que você está trabalhando em um domínio estável e, como tal, não acredito que verá mudanças frequentes
    2. Como os B e C são remessa e e-mail, e você recebe dados de A, acredito que você sofrerá alterações adicionais em vez de quebradas, que são seguras de serem adicionadas sempre que você as descobrir sem retrabalho.
  2. Acoplamento: Há muito pouco ou nenhum acoplamento aqui. Primeiro, como a comunicação é via mensagens, não há acoplamento entre os serviços além de um curto temporal durante a propagação dos dados e o contrato dessa operação (que não é um acoplamento que você pode ou deve tentar evitar)

A opção 1 não é algo que eu descartaria. Existe a mesma quantidade de acoplamento, mas em termos de desenvolvimento deve ser fácil (sem necessidade de ações especiais), e a estabilidade do domínio deve significar que elas não mudam frequentemente (como já mencionei).

Outra opção que eu sugiro é uma pequena variação para 3, que não é executar o processo durante a inicialização, mas sim observar o evento "ProductAdded e" ProductDetailsChanged "em B e C, sempre que houver uma alteração no catálogo de produtos. em A. Isso tornaria suas implantações mais rápidas (e, portanto, mais fáceis de corrigir um problema / bug, se houver).


Editar 2020-03-03

Eu tenho uma ordem específica de prioridades ao determinar a abordagem de integração:

  1. Qual é o custo da consistência? Podemos aceitar alguns milissegundos de inconsistência entre as coisas alteradas em A e refletidas em B & C?
  2. Você precisa de consultas pontuais (também chamadas de consultas temporais)?
  3. Existe alguma fonte de verdade para os dados? Um serviço que os possui e é considerado a montante?
  4. Se existe um proprietário / única fonte de verdade, isso é estável? Ou esperamos ver mudanças frequentes?

Se o custo da inconsistência for alto (basicamente os dados do produto em A precisam ser consistentes o mais rápido possível com o produto armazenado em cache em B e C), você não poderá evitar a necessidade de aceitar a indisponibilidade e fazer uma solicitação síncrona (como uma Web / rest request) de B & C para A para buscar os dados. Estar ciente! Isso ainda não significa consistente em termos de transação, mas apenas minimiza as janelas de inconsistência. Se você absolutamente, positivamente, precisar ser imediatamente consistente, precisará refazer seus limites de serviço. No entanto, I muito acreditamos fortemente que este não deve ser um problema. Por experiência, é realmente extremamente raro que a empresa não possa aceitar alguns segundos de inconsistência; portanto, você nem precisa fazer solicitações síncronas.

Se você precisar de consultas point-in-time (que eu não notei na sua pergunta e, portanto, não incluímos acima, talvez de forma errada), o custo de manter isso nos serviços downstream é tão alto (você precisaria duplicar lógica interna de projeção de eventos em todos os serviços downstream) que torna a decisão clara: você deve deixar a propriedade para A e consultar uma solicitação ad-hoc pela Web (ou similar) e A deve usar a fonte de eventos para recuperar todos os eventos que você conhecia no momento de projetar para o estado e devolvê-lo. Acho que essa pode ser a opção 2 (se entendi corretamente?), Mas os custos são tais que, enquanto o acoplamento temporal é melhor que o custo de manutenção de eventos duplicados e lógica de projeção.

Se você não precisar de um ponto no tempo e não houver um proprietário claro e único dos dados (que na minha resposta inicial assumi isso com base na sua pergunta), um padrão bastante razoável seria manter representações do produto em cada serviço separadamente. Ao atualizar os dados dos produtos, você atualiza A, B e C em paralelo, fazendo solicitações da Web paralelas para cada uma delas, ou possui uma API de comando que envia vários comandos para cada uma das categorias A, B e C. versão local dos dados para realizar seu trabalho, que pode ou não ser obsoleto. Essa não é uma das opções acima (embora possa ser feita para se aproximar da opção 3), pois os dados em A, B e C podem diferir e o "todo" do produto pode ser uma composição dos três dados fontes.

Saber se a fonte da verdade tem um contrato estável é útil porque você pode usá-lo para usar os eventos de domínio / interno (ou eventos que você armazena na fonte de eventos como padrão de armazenamento em A) para integração entre A e os serviços B e C. Se o contrato for estável, você poderá integrar os eventos do domínio. No entanto, você tem uma preocupação adicional no caso em que as alterações são frequentes ou esse contrato de mensagem é grande o suficiente para tornar o transporte uma preocupação.

Se você tiver um proprietário claro, com um contrato que se espera estável, as melhores opções seriam a opção 1; um pedido conteria todas as informações necessárias e, em seguida, B e C executariam sua função usando os dados no evento.

Se o contrato é suscetível de alterar ou quebrar com frequência, seguindo a opção 3, que é voltar às solicitações da Web para buscar dados do produto, na verdade é uma opção melhor, pois é muito mais fácil manter várias versões. Então, B faria uma solicitação na v3 do produto.

Savvas Kleanthous
fonte
Sim, eu concordo. Embora ProductAddedou ProductDetailsChangedadicione complexidade ao rastreamento de alterações no catálogo de produtos, precisamos manter esses dados sincronizados entre os bancos de dados de alguma forma, caso os eventos sejam reproduzidos e precisamos acessar os dados do catálogo do passado.
eithed
Atualizei a resposta para expandir algumas suposições que fiz.
Savvas Kleanthous 03/03