Um guia para transações entre microsserviços
1. Introdução
Neste artigo, discutiremos opções para implementar uma transação em microsserviços.
Também verificaremos algumas alternativas para transações em um cenário de microsserviço distribuído.
2. Evitando transações em microsserviços
Uma transação distribuída é um processo muito complexo, com muitas partes móveis que podem falhar. Além disso, se essas peças rodarem em máquinas diferentes ou mesmo em centros de dados diferentes, o processo de confirmar uma transação pode se tornar muito longo e não confiável.
Isso pode afetar seriamente a experiência do usuário e a largura de banda geral do sistema. Entãoone of the best ways to solve the problem of distributed transactions is to avoid them completely.
2.1. Exemplo de arquitetura que requer transações
Geralmente, um microsserviço é projetado de maneira a ser independente e útil por si só. Deve ser capaz de resolver alguma tarefa de negócios atômica.
Se pudéssemos dividir nosso sistema em tais microsserviços, há uma boa chance de não precisarmos implementar transações entre eles.
Por exemplo, vamos considerar um sistema de transmissão de mensagens entre usuários.
O microsserviçouser estaria relacionado ao perfil do usuário (criação de um novo usuário, edição de dados do perfil etc.) com a seguinte classe de domínio subjacente:
@Entity
public class User implements Serializable {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private long id;
@Basic
private String name;
@Basic
private String surname;
@Basic
private Instant lastMessageTime;
}
O microsserviçomessage estaria relacionado à transmissão. Ele encapsula a entidadeMessage e tudo ao seu redor:
@Entity
public class Message implements Serializable {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private long id;
@Basic
private long userId;
@Basic
private String contents;
@Basic
private Instant messageTimestamp;
}
Cada microsserviço possui seu próprio banco de dados. Observe que não nos referimos à entidadeUser da entidadeMessage, pois as classes de usuário não são acessíveis a partir do microsserviçomessage. Nós nos referimos ao usuário apenas por ID.
Agora a entidadeUser contém o campolastMessageTime porque queremos mostrar as informações sobre o tempo da última atividade do usuário em seu perfil.
No entanto, para adicionar uma nova mensagem ao usuário e atualizar seulastMessageTime, agora teríamos que implementar uma transação entre microsserviços.
2.2. Abordagem Alternativa sem Transações
Podemos alterar nossa arquitetura de microsserviço e remover o campolastMessageTime da entidadeUser.
Em seguida, poderíamos exibir esse tempo no perfil do usuário emitindo uma solicitação separada para o microsserviço de mensagens e encontrando o valor máximo demessageTimestamp para todas as mensagens desse usuário.
Provavelmente, se o microsserviçomessage estiver sobrecarregado ou mesmo inativo, não poderemos mostrar a hora da última mensagem do usuário em seu perfil.
Mas isso poderia ser mais aceitável do que deixar de confirmar uma transação distribuída para salvar uma mensagem apenas porque o microsserviço do usuário não respondeu a tempo.
É claro que existem cenários mais complexos quando temos que implementar um processo de negócios em vários microsserviços e não queremos permitir a inconsistência entre esses microsserviços.
3. Protocolo de confirmação de duas fases
Two-phase commit protocol (ou 2PC) é um mecanismo para implementar uma transação em diferentes componentes de software (vários bancos de dados, filas de mensagens, etc.)
3.1. A arquitetura de 2PC
Um dos participantes importantes de uma transação distribuída é o coordenador da transação. A transação distribuída consiste em duas etapas:
-
Fase de preparação - durante esta fase, todos os participantes da transação se preparam para confirmar e notificam o coordenador de que estão prontos para concluir a transação.
-
Fase de confirmação ou reversão - durante esta fase, um comando de confirmação ou reversão é emitido pelo coordenador da transação para todos os participantes
O problema com o 2PC é que ele é muito lento em comparação com o tempo de operação de um único microsserviço.
Coordinating the transaction between microservices, even if they are on the same network, can really slow the system down, portanto, esta abordagem geralmente não é usada em um cenário de alta carga.
3.2. Padrão XA
OXA standard é uma especificação para conduzir as transações distribuídas 2PC entre os recursos de suporte. Qualquer servidor de aplicativos compatível com JTA (JBoss, GlassFish etc.) suporta-o imediatamente.
Os recursos que participam de transações distribuídas podem ser, por exemplo, dois bancos de dados de dois microsserviços diferentes.
No entanto, para tirar vantagem desse mecanismo, os recursos devem ser implantados em uma única plataforma JTA. Isso nem sempre é viável para uma arquitetura de microsserviço.
3.3. Rascunho Padrão REST-AT
Outro padrão proposto éREST-AT, que passou por algum desenvolvimento pela RedHat, mas ainda não saiu do estágio de rascunho. No entanto, é compatível com o servidor de aplicativos WildFly pronto para uso.
Esse padrão permite usar o servidor de aplicativos como coordenador de transações com uma API REST específica para criar e ingressar nas transações distribuídas.
Os serviços da web RESTful que desejam participar da transação de duas fases também precisam oferecer suporte a uma API REST específica.
Infelizmente, para criar uma ponte entre uma transação distribuída e recursos locais do microsserviço, ainda teríamos que implantar esses recursos em uma única plataforma JTA ou resolver uma tarefa não trivial de escrever essa ponte nós mesmos.
4. Consistência Eventual e Compensação
De longe, um dos modelos mais viáveis de tratamento da consistência em microsserviços éeventual consistency.
Este modelo não impõe transações ACID distribuídas em microsserviços. Em vez disso, propõe usar alguns mecanismos para garantir que o sistema seja eventualmente consistente em algum momento no futuro.
4.1. Um Caso para Consistência Eventual
Por exemplo, suponha que precisamos resolver a seguinte tarefa:
-
registrar um perfil de usuário
-
faça alguma verificação automatizada de antecedentes para que o usuário possa realmente acessar o sistema
A segunda tarefa é garantir, por exemplo, que este usuário não foi banido de nossos servidores por algum motivo.
Mas pode levar algum tempo e gostaríamos de extraí-lo para um microsserviço separado. Não seria razoável deixar o usuário esperando por tanto tempo apenas para saber que ele foi registrado com sucesso.
One way to solve it would be with a message-driven approach including compensation. Vamos considerar a seguinte arquitetura:
-
o microsserviçouser encarregado de registrar um perfil de usuário
-
o microsserviçovalidation encarregado de fazer uma verificação de antecedentes
-
a plataforma de mensagens que suporta filas persistentes
A plataforma do sistema de mensagens pode garantir que as mensagens enviadas pelos microsserviços sejam mantidas. Em seguida, eles seriam entregues em um momento posterior se o receptor não estivesse disponível no momento
4.2. Cenário Feliz
Nessa arquitetura, um cenário feliz seria:
-
o microsserviçouser registra um usuário, salvando informações sobre ele em seu banco de dados local
-
o microsserviçouser marca este usuário com um sinalizador. Isso pode significar que este usuário ainda não foi validado e não tem acesso à funcionalidade completa do sistema
-
uma confirmação de registro é enviada ao usuário com um aviso de que nem todas as funcionalidades do sistema estão acessíveis imediatamente
-
o microsserviçouser envia uma mensagem para o microsserviçovalidation para fazer a verificação de antecedentes de um usuário
-
o microsserviçovalidation executa a verificação de antecedentes e envia uma mensagem para o microsserviçouser com os resultados da verificação
-
se os resultados forem positivos, o microsserviçouser desbloqueia o usuário
-
se os resultados forem negativos, o microsserviçouser exclui a conta do usuário
-
Depois de passar por todas essas etapas, o sistema deve estar em um estado consistente. No entanto, por algum período, a entidade do usuário parecia estar em um estado incompleto.
The last step, when the user microservice removes the invalid account, is a compensation phase.
4.3. Cenários de falha
Agora, vamos considerar alguns cenários de falha:
-
se o microsserviçovalidation não estiver acessível, a plataforma de mensagens com sua funcionalidade de fila persistente garante que o microsserviçovalidation receba essa mensagem posteriormente
-
suponha que a plataforma de mensagens falhe, então o microsserviçouser tenta enviar a mensagem novamente em algum momento posterior, por exemplo, por processamento em lote programado de todos os usuários que ainda não foram validados
-
se o microsserviçovalidation receber a mensagem, valida o usuário, mas não puder enviar a resposta de volta devido à falha da plataforma de mensagens, o microsserviçovalidation também tentará enviar a mensagem novamente mais tarde
-
se uma das mensagens foi perdida ou alguma outra falha aconteceu, o microsserviçouser encontra todos os usuários não validados por processamento em lote agendado e envia solicitações de validação novamente
Mesmo que algumas das mensagens fossem emitidas várias vezes, isso não afetaria a consistência dos dados nos bancos de dados dos microsserviços.
Ao considerar cuidadosamente todos os cenários de falha possíveis, podemos garantir que nosso sistema satisfaria as condições de consistência eventual. Ao mesmo tempo, não precisaríamos lidar com as caras transações distribuídas.
Mas precisamos estar cientes de que garantir uma consistência eventual é uma tarefa complexa. Não há uma solução única para todos os casos.
5. Conclusão
Neste artigo, discutimos alguns dos mecanismos para implementar transações em microsserviços.
E também exploramos algumas alternativas para fazer esse estilo de transações em primeiro lugar.