A comunicação entre microsserviços deve ser eficiente e robusta. Com muitos pequenos serviços interagindo para concluir uma única atividade comercial, isso pode ser um desafio. Neste artigo, examinamos as compensações entre mensagens assíncronas versus APIs síncronas. Em seguida, analisamos alguns dos desafios na conceção de uma comunicação interserviços resiliente.
Desafios
Aqui estão alguns dos principais desafios decorrentes da comunicação serviço-a-serviço. As malhas de serviço, descritas mais adiante neste artigo, são projetadas para lidar com muitos desses desafios.
Resiliência. Pode haver dezenas ou até centenas de instâncias de qualquer microsserviço. Uma instância pode falhar por vários motivos. Pode haver uma falha no nível do nó, como uma falha de hardware ou uma reinicialização da VM. Uma instância pode falhar ou ficar sobrecarregada com solicitações e incapaz de processar novas solicitações. Qualquer um desses eventos pode fazer com que uma chamada de rede falhe. Há dois padrões de design que podem ajudar a tornar as chamadas de rede serviço a serviço mais resilientes:
Repetir. Uma chamada de rede pode falhar devido a uma falha transitória que desaparece por si só. Em vez de falhar completamente, o chamador normalmente deve repetir a operação um determinado número de vezes, ou até que um período de tempo limite configurado transcorra. No entanto, se uma operação não é idempotente, novas tentativas podem causar efeitos colaterais não intencionais. A chamada original pode ser bem-sucedida, mas o chamador nunca recebe uma resposta. Se o chamador repetir, a operação pode ser invocada duas vezes. Geralmente, não é seguro repetir os métodos POST ou PATCH, porque não é garantido que estes sejam idempotentes.
Disjuntor. Muitas solicitações com falha podem causar um gargalo, pois as solicitações pendentes se acumulam na fila. Estes pedidos bloqueados poderão conter recursos de sistema cruciais, como memória, threads, ligações de base de dados, etc., que podem provocar falhas em cascata. O padrão de disjuntor pode impedir que um serviço tente repetidamente uma operação que provavelmente falhará.
Balanceamento de carga. Quando o serviço "A" chama o serviço "B", a solicitação deve chegar a uma instância em execução do serviço "B". No Kubernetes, o Service
tipo de recurso fornece um endereço IP estável para um grupo de pods. O tráfego de rede para o endereço IP do serviço é encaminhado para um pod por meio de regras iptable. Por padrão, um pod aleatório é escolhido. Uma malha de serviço (veja abaixo) pode fornecer algoritmos de balanceamento de carga mais inteligentes com base na latência observada ou em outras métricas.
Rastreamento distribuído. Uma única transação pode abranger vários serviços. Isso pode dificultar o monitoramento do desempenho geral e da integridade do sistema. Mesmo que cada serviço gere logs e métricas, sem alguma maneira de vinculá-los, eles são de uso limitado.
Controle de versão do serviço. Quando uma equipe implanta uma nova versão de um serviço, ela deve evitar quebrar quaisquer outros serviços ou clientes externos que dependem dele. Além disso, talvez você queira executar várias versões de um serviço lado a lado e rotear solicitações para uma versão específica. Consulte Controle de versão da API para obter mais informações sobre esse problema.
Criptografia TLS e autenticação TLS mútua. Por motivos de segurança, convém criptografar o tráfego entre serviços com TLS e usar a autenticação TLS mútua para autenticar chamadores.
Mensagens síncronas versus assíncronas
Há dois padrões básicos de mensagens que os microsserviços podem usar para se comunicar com outros microsserviços.
Comunicação síncrona. Nesse padrão, um serviço chama uma API que outro serviço expõe, usando um protocolo como HTTP ou gRPC. Esta opção é um padrão de mensagens síncronas porque o chamador aguarda uma resposta do recetor.
Passagem assíncrona de mensagens. Nesse padrão, um serviço envia uma mensagem sem esperar por uma resposta e um ou mais serviços processam a mensagem de forma assíncrona.
É importante distinguir entre E/S assíncrona e um protocolo assíncrono. E/S assíncrona significa que o thread de chamada não é bloqueado enquanto a E/S é concluída. Isso é importante para o desempenho, mas é um detalhe de implementação em termos de arquitetura. Um protocolo assíncrono significa que o remetente não espera por uma resposta. HTTP é um protocolo síncrono, mesmo que um cliente HTTP possa usar E/S assíncrona quando envia uma solicitação.
Há compensações para cada padrão. Solicitação/resposta é um paradigma bem compreendido, portanto, projetar uma API pode parecer mais natural do que projetar um sistema de mensagens. No entanto, o sistema de mensagens assíncronas tem algumas vantagens que podem ser úteis em uma arquitetura de microsserviços:
Acoplamento reduzido. O remetente da mensagem não precisa saber sobre o consumidor.
Vários assinantes. Usando um modelo de pub/sub, vários consumidores podem se inscrever para receber eventos. Consulte Estilo de arquitetura orientada a eventos.
Isolamento de falhas. Se o consumidor falhar, o remetente ainda pode enviar mensagens. As mensagens serão recolhidas quando o consumidor recuperar. Essa capacidade é especialmente útil em uma arquitetura de microsserviços, porque cada serviço tem seu próprio ciclo de vida. Um serviço pode ficar indisponível ou ser substituído por uma versão mais recente a qualquer momento. As mensagens assíncronas podem lidar com o tempo de inatividade intermitente. As APIs síncronas, por outro lado, exigem que o serviço downstream esteja disponível ou a operação falha.
Capacidade de resposta. Um serviço a montante pode responder mais rapidamente se não esperar pelos serviços a jusante. Isso é especialmente útil em uma arquitetura de microsserviços. Se houver uma cadeia de dependências de serviço (o serviço A chama B, que chama C e assim por diante), a espera em chamadas síncronas pode adicionar quantidades inaceitáveis de latência.
Nivelamento de carga. Uma fila pode atuar como um buffer para nivelar a carga de trabalho, para que os recetores possam processar mensagens em sua própria taxa.
Fluxos de trabalho. As filas podem ser usadas para gerenciar um fluxo de trabalho, marcando a mensagem após cada etapa do fluxo de trabalho.
No entanto, também há alguns desafios para usar mensagens assíncronas de forma eficaz.
Acoplamento com a infraestrutura de mensagens. O uso de uma infraestrutura de mensagens específica pode causar um acoplamento estreito com essa infraestrutura. Será difícil mudar para outra infraestrutura de mensagens mais tarde.
Latência. A latência de ponta a ponta de uma operação pode se tornar alta se as filas de mensagens forem preenchidas.
Custo. Em altas taxas de transferência, o custo monetário da infraestrutura de mensagens pode ser significativo.
Complexidade. Lidar com mensagens assíncronas não é uma tarefa trivial. Por exemplo, você deve lidar com mensagens duplicadas, seja eliminando a duplicação ou tornando as operações idempotentes. Também é difícil implementar semântica de solicitação-resposta usando mensagens assíncronas. Para enviar uma resposta, você precisa de outra fila, além de uma maneira de correlacionar mensagens de solicitação e resposta.
Débito. Se as mensagens exigirem semântica de fila, a fila pode se tornar um gargalo no sistema. Cada mensagem requer pelo menos uma operação de fila e uma operação de retirada de fila. Além disso, a semântica da fila geralmente requer algum tipo de bloqueio dentro da infraestrutura de mensagens. Se a fila for um serviço gerenciado, pode haver latência adicional, porque a fila é externa à rede virtual do cluster. Você pode atenuar esses problemas enviando mensagens em lote, mas isso complica o código. Se as mensagens não exigirem semântica de fila, talvez seja possível usar um fluxo de eventos em vez de uma fila. Para obter mais informações, consulte Estilo arquitetônico controlado por eventos.
Entrega por drone: Escolhendo os padrões de mensagens
Esta solução utiliza o exemplo do Drone Delivery. É ideal para as indústrias aeroespacial e aeronáutica.
Com essas considerações em mente, a equipe de desenvolvimento fez as seguintes escolhas de design para o aplicativo Drone Delivery:
O serviço Ingestão expõe uma API REST pública que os aplicativos cliente usam para agendar, atualizar ou cancelar entregas.
O serviço Ingestão usa Hubs de Eventos para enviar mensagens assíncronas para o serviço Agendador. As mensagens assíncronas são necessárias para implementar o nivelamento de carga necessário para a ingestão.
Os serviços de Conta, Entrega, Pacote, Drone e Transporte de Terceiros expõem APIs REST internas. O serviço Agendador chama essas APIs para executar uma solicitação do usuário. Um motivo para usar APIs síncronas é que o Agendador precisa obter uma resposta de cada um dos serviços downstream. Uma falha em qualquer um dos serviços a jusante significa que toda a operação falhou. No entanto, um problema potencial é a quantidade de latência que é introduzida chamando os serviços de back-end.
Se qualquer serviço a jusante tiver uma falha não transitória, toda a transação deve ser marcada como falha. Para lidar com esse caso, o serviço Agendador envia uma mensagem assíncrona ao Supervisor, para que o Supervisor possa agendar transações de compensação.
O serviço de entrega expõe uma API pública que os clientes podem usar para obter o status de uma entrega. No artigo API gateway, discutimos como um gateway de API pode ocultar os serviços subjacentes do cliente, para que o cliente não precise saber quais serviços expõem quais APIs.
Enquanto um drone está em voo, o serviço Drone envia eventos que contêm a localização e o status atuais do drone. O serviço de entrega escuta esses eventos para acompanhar o status de uma entrega.
Quando o status de uma entrega muda, o serviço de Entrega envia um evento de status de entrega, como
DeliveryCreated
ouDeliveryCompleted
. Qualquer serviço pode subscrever estes eventos. No design atual, o serviço Histórico de Entregas é o único assinante, mas pode haver outros assinantes mais tarde. Por exemplo, os eventos podem ir para um serviço de análise em tempo real. E como o Agendador não precisa esperar por uma resposta, adicionar mais assinantes não afeta o caminho principal do fluxo de trabalho.
Observe que os eventos de status de entrega são derivados de eventos de localização de drones. Por exemplo, quando um drone chega a um local de entrega e entrega um pacote, o serviço de Entrega traduz isso em um evento DeliveryCompleted. Este é um exemplo de pensamento em termos de modelos de domínio. Como descrito anteriormente, o Gerenciamento de Drones pertence a um contexto limitado separado. Os eventos com drones transmitem a localização física de um drone. Os eventos de entrega, por outro lado, representam mudanças no status de uma entrega, que é uma entidade comercial diferente.
Usando uma malha de serviço
Uma malha de serviço é uma camada de software que lida com a comunicação serviço-a-serviço. As malhas de serviço são projetadas para abordar muitas das preocupações listadas na seção anterior e para transferir a responsabilidade por essas preocupações dos próprios microsserviços para uma camada compartilhada. A malha de serviço atua como um proxy que interceta a comunicação de rede entre microsserviços no cluster. Atualmente, o conceito de malha de serviço se aplica principalmente a orquestradores de contêineres, em vez de arquiteturas sem servidor.
Nota
A malha de serviço é um exemplo do padrão Ambassador — um serviço auxiliar que envia solicitações de rede em nome do aplicativo.
No momento, as principais opções para uma malha de serviço no Kubernetes são Linkerd e Istio. Ambas as tecnologias estão a evoluir rapidamente. No entanto, alguns recursos que o Linkerd e o Istio têm em comum incluem:
Balanceamento de carga no nível da sessão, com base nas latências observadas ou no número de solicitações pendentes. Isso pode melhorar o desempenho em relação ao balanceamento de carga de camada 4 fornecido pelo Kubernetes.
Roteamento de camada 7 com base no caminho da URL, cabeçalho do host, versão da API ou outras regras no nível do aplicativo.
Repetição de solicitações com falha. Uma malha de serviço compreende códigos de erro HTTP e pode repetir automaticamente solicitações com falha. Você pode configurar esse número máximo de tentativas, juntamente com um período de tempo limite para vincular a latência máxima.
Disjuntor. Se uma instância falhar consistentemente nas solicitações, a malha de serviço a marcará temporariamente como indisponível. Após um período de recuo, ele tentará a instância novamente. Você pode configurar o disjuntor com base em vários critérios, como o número de falhas consecutivas,
A malha de serviço captura métricas sobre chamadas entre serviços, como o volume de solicitações, latência, taxas de erro e sucesso e tamanhos de resposta. A malha de serviço também permite o rastreamento distribuído adicionando informações de correlação para cada salto em uma solicitação.
Autenticação TLS mútua para chamadas de serviço a serviço.
Precisa de uma malha de serviço? Depende. Sem uma malha de serviço, você precisará considerar cada um dos desafios mencionados no início deste artigo. Você pode resolver problemas como nova tentativa, disjuntor e rastreamento distribuído sem uma malha de serviço, mas uma malha de serviço move essas preocupações dos serviços individuais para uma camada dedicada. Por outro lado, uma malha de serviço adiciona complexidade à instalação e configuração do cluster. Pode haver implicações de desempenho, porque as solicitações agora são roteadas por meio do proxy de malha de serviço e porque serviços extras agora estão sendo executados em todos os nós do cluster. Você deve fazer testes completos de desempenho e carga antes de implantar uma malha de serviço na produção.
Transações distribuídas
Um desafio comum em microsserviços é lidar corretamente com transações que abrangem vários serviços. Muitas vezes, nesse cenário, o sucesso de uma transação é tudo ou nada — se um dos serviços participantes falhar, toda a transação deve falhar.
Há dois casos a considerar:
Um serviço pode enfrentar uma falha transitória , como um tempo limite de rede. Muitas vezes, esses erros podem ser resolvidos simplesmente tentando novamente a chamada. Se a operação ainda falhar após um certo número de tentativas, é considerada uma falha não transitória.
Uma falha não transitória é qualquer falha que é improvável que desapareça por si só. Falhas não transitórias incluem condições de erro normais, como entrada inválida. Eles também incluem exceções não tratadas no código do aplicativo ou uma falha de processo. Se esse tipo de erro ocorrer, toda a transação comercial deverá ser marcada como uma falha. Pode ser necessário desfazer outras etapas na mesma transação que já foram bem-sucedidas.
Após uma falha não transitória, a transação atual pode estar em um estado de falha parcial, onde uma ou mais etapas já foram concluídas com êxito. Por exemplo, se o serviço de Drone já agendou um drone, o drone deve ser cancelado. Nesse caso, o aplicativo precisa desfazer as etapas que tiveram êxito, usando uma Transação de Compensação. Em alguns casos, essa ação deve ser feita por um sistema externo ou até mesmo por um processo manual. No seu projeto, lembre-se de que as medidas compensatórias também estão sujeitas a falhas.
Se a lógica para compensar transações for complexa, considere a criação de um serviço separado que seja responsável por esse processo. No aplicativo Drone Delivery, o serviço Scheduler coloca operações com falha em uma fila dedicada. Um microsserviço separado, chamado Supervisor, lê essa fila e chama uma API de cancelamento nos serviços que precisam compensar. Esta é uma variação do padrão Scheduler Agent Supervisor. O serviço Supervisor também pode tomar outras ações, como notificar o usuário por texto ou e-mail ou enviar um alerta para um painel de operações.
O próprio serviço Agendador pode falhar (por exemplo, porque um nó falha). Nesse caso, uma nova instância pode girar e assumir o controle. No entanto, todas as transações que já estavam em andamento devem ser retomadas.
Uma abordagem é salvar um ponto de verificação em um armazenamento durável depois que cada etapa do fluxo de trabalho for concluída. Se uma instância do serviço Agendador falhar no meio de uma transação, uma nova instância poderá usar o ponto de verificação para retomar de onde a instância anterior parou. No entanto, escrever pontos de verificação pode criar uma sobrecarga de desempenho.
Outra opção é projetar todas as operações para serem idempotentes. Uma operação é idempotente se puder ser chamada várias vezes sem produzir efeitos colaterais adicionais após a primeira chamada. Essencialmente, o serviço a jusante deve ignorar chamadas duplicadas, o que significa que o serviço deve ser capaz de detetar chamadas duplicadas. Nem sempre é simples implementar métodos idempotentes. Para obter mais informações, consulte Operações idempotentes.
Próximos passos
Para microsserviços que conversam diretamente entre si, é importante criar APIs bem projetadas.