Tratamento de erros no sistema distribuído


8

Esta é a sequência comum de dois componentes distribuídos em nosso aplicativo Java:

1  A sends request to B
2      B starts some job J in parallel thread
3      B returns response to A
4  A accepts response
5      Job finishes after some time
6      Job sends information to A
7  A receives response from a Job and updates

Este é o cenário ideal, supondo que tudo funcione. Obviamente, a vida real é cheia de falhas. Por exemplo, um dos piores casos pode ser se #6falhar simplesmente por causa da rede: o trabalho foi executado corretamente, mas Anão sabe nada sobre ele.

Estou procurando uma abordagem leve sobre como gerenciar erros neste sistema. Observe que temos muitos componentes; portanto, agrupar todos eles apenas por causa do tratamento de erros não faz sentido. Em seguida, abandonei o uso de qualquer memória / repositório distribuído que seria novamente instalado em cada componente pelo mesmo motivo.

Meus pensamentos estão indo na direção de ter um estado absoluto em um B e nunca ter um estado persistente em um A. Isso significa o seguinte:

  • antes de #1marcarmos Aque a unidade de trabalho, ou seja, a mudança está prestes a começar
  • somente Bpode desmarcar esse estado.
  • Apode buscar informações sobre a Bqualquer momento, para atualizar o estado.
  • nenhuma nova alteração na mesma unidade pode ser ativada A.

O que você acha? Existe alguma maneira leve de domar os erros neste sistema?


Esta é uma pergunta antiga. Você encontrou uma boa solução? ... Se sim, você pode compartilhá-lo?
svidgen

Respostas:


2

Anexar a um log persistente em A deve ser suficiente. Isso lida com reinicializações e partições de rede para obter consistência eventual ou sinalizar quebras que impedem essa convergência. Com a confirmação do grupo amortizado , pode levar menos de uma única gravação para manter uma entrada de log.

Você sugeriu tornar B responsável por desmarcar o estado. Discordo. Somente A fica ciente do novo trabalho, e somente A deve ser responsável pelo rastreamento e relatório de erros, como tempos limite. B envia mensagens idempotentes para A e A atualiza o estado, consultando novamente a intervalos conforme necessário.

Na etapa 0, A toma conhecimento de uma nova solicitação e a registra. Isso constitui uma obrigação que A deve cumprir posteriormente em algum prazo - A continuará executando e repetirá as etapas subseqüentes até que A saiba que o processamento da solicitação foi concluído.

Alguns pedidos serão mais longos que outros. As estimativas do tempo de processamento estarão disponíveis em A e B, talvez revisadas à medida que o processamento continua. Tais estimativas podem ser retornadas para A, de modo que raramente produzem tempos limite falso-positivos. Pense nisso como uma mensagem keep alive que diz "ainda trabalhando, ainda trabalhando".


1

Adote uma atração em vez da estratégia de envio. Faça cada peça extrair alterações das outras e atualize seus próprios registros.

  • A registra o que B deve fazer em uma fila
  • B puxa da fila de A e faz o trabalho
  • B registra o que fez em uma fila
  • A puxa da fila de B para saber qual foi o resultado do trabalho

(Estou usando a fila de palavras, mas você pode substituir o log ou o tópico.)

Você pode assar a fila nos serviços ou pode ter um intermediário de mensagens separado. Uma implementação inserida em um serviço pode ser tão simples quantoGET /jobrequests?from=<timestamp> (com B acompanhando o registro de data e hora da última solicitação de trabalho processada).

Uma parte complicada dessa arquitetura é decidir a semântica, pelo menos uma vez, e no máximo uma vez. Concretamente: se B puxa um item da fila e depois trava enquanto o executa, o que deve acontecer? Existem duas possibilidades, e a mais apropriada depende do seu caso de uso:

  • Pelo menos uma vez: B confirma apenas o ponto da fila em que chegou depois de concluir uma ação, há o risco de executar duas vezes. Se você projetar ações para serem idempotentes, poderá obter um comportamento exatamente uma vez usando essa abordagem. (Eu uso kafka para esse cenário.)
  • No máximo uma vez: B consome apenas todos os itens da fila uma vez. Se travar durante a execução, o item nunca será executado.

Benefícios desta abordagem:

  • Os serviços que consomem fila não precisam estar prontos para que o envio da fila ocorra. Isso significa que você pode reiniciar B enquanto A estiver trabalhando ou reiniciar A enquanto B estiver trabalhando. A hospedagem redundante de serviços em segundo plano é necessária apenas para garantir o tempo de resposta geral, não uma operação confiável.
  • O ritmo de extração de itens da fila pode ser controlado pelo consumidor, o que permite armazenar temporariamente os picos de carga na fila.
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.