Compartilhar via


Criar comunicação entre serviços para microsserviços

Azure DevOps

A comunicação entre os microsserviços deve ser eficiente e robusta. Com vários serviços pequenos interagindo para concluir uma única atividade de negócios, isso pode ser um desafio. Neste artigo, examinamos as compensações entre mensagens assíncronas versus APIs síncronas. Em seguida, consideramos alguns dos desafios no design de comunicação interserviços resiliente.

Desafios

Aqui estão alguns dos principais desafios decorrentes da comunicação de 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 um determinado microsserviço. Uma instância pode falhar por vários motivos. Pode haver uma falha no nível de nó, como uma falha de hardware ou uma reinicialização da VM. Uma instância pode falhar ou ficar sobrecarregada com solicitações e, assim, impossibilitada de processar solicitações novas. 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 de serviço a serviço mais resilientes:

  • Tentar novamente. Uma chamada de rede pode falhar devido a uma falha transitória que é resolvida por conta própria. Em vez de falhar completamente, o chamador geralmente deve tentar novamente a operação um número determinado de vezes ou até que o período de tempo limite configurado expire.

    Se uma operação não for idempotente, novas tentativas poderão causar efeitos não intencionais. A chamada original talvez seja bem-sucedida, mas o autor da chamada nunca receberá uma resposta. Se o chamador tentar novamente, a operação poderá ser invocada duas vezes. Geralmente, não é seguro repetir métodos POST ou PATCH porque essas operações não são garantidamente idempotentes. Para obter mais informações, confira Manipulação de falhas transitórias.

  • Disjuntor. Um número excessivo de solicitações com falha pode causar um gargalo, já que as solicitações pendentes se acumulam na fila. Essas solicitações bloqueadas podem conter recursos críticos do sistema, como memória, threads e conexões de banco de dados, o que pode causar falhas em cascata. O padrão Circuit Breaker pode impedir que um serviço tente repetir várias vezes uma operação que provavelmente falhará.

Balanceamento de carga. Quando o serviço "A" chama o serviço "B", a solicitação deve alcançar 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 de iptables. Por padrão, um pod aleatório é escolhido. Uma malha de serviço 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 forma associá-los, eles serão de utilidade limitada.

Controle de versão do serviço. Quando uma equipe implanta uma nova versão de um serviço, ela deve evitar a interrupção de qualquer outro serviço ou cliente externo que dependa dele. Além disso, talvez você queira executar várias versões de uma serviço lado a lado e rotear solicitações para uma versão específica. Para obter mais informações, consulte o controle de versão da API.

Criptografia de TLS e autenticação de TLS mútua. Por motivos de segurança, talvez você queira criptografar o tráfego entre serviços com TLS e usar a autenticação TLS mútua para autenticar os chamadores.

Sistema de mensagens síncrono versus assíncrono

Há dois padrões básicos de mensagens que os microsserviços podem utilizar para se comunicarem com outros microsserviços.

  1. Comunicação síncrona. Nesse padrão, um serviço chama uma API que outro serviço expõe usando um protocolo, como o HTTP ou o gRPC. Esta opção é um padrão de sistema de mensagens síncrono porque o autor da chamada aguarda uma resposta do receptor.

  2. Transmissão de mensagens assíncronas. Nesse padrão, um serviço envia a mensagem sem aguardar uma resposta, e um ou mais serviços processam a mensagem de maneira assíncrona.

É importante distinguir entre uma E/S assíncrona e um protocolo assíncrono. E/S assíncrona significa que o thread que faz a chamada não está 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 indica que o remetente não aguardará uma resposta. HTTP é um protocolo síncrono, embora um cliente HTTP possa usar E/S assíncrona ao enviar uma solicitação.

Há vantagens e desvantagens 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 pub/sub, vários consumidores podem se inscrever para receber eventos. Consulte Arquitetura orientada a eventos.

  • Isolamento de falha. Se o consumidor falhar, o remetente ainda poderá enviar mensagens. As mensagens são coletadas quando o consumidor se recupera. Essa capacidade é especialmente útil em uma arquitetura de microsserviços, uma vez que cada serviço tem seu próprio ciclo de vida. Um serviço pode se tornar indisponível ou ser substituído por uma versão mais recente a qualquer momento. O sistema de mensagens assíncrono pode controlar o tempo de inatividade intermitente. Por outro lado, as APIs síncronas exigem que o serviço de downstream esteja disponível, ou a operação falhará.

  • Capacidade de resposta. Os serviços upstream conseguem responder mais rapidamente se não aguardarem os serviços downstream. Isso é especialmente útil em uma arquitetura de microsserviços. Se houver uma cadeia de dependências de serviço (por exemplo, o serviço A invoca B, que invoca C), aguardar chamadas síncronas pode adicionar uma latência inaceitável.

  • Nivelamento de carga. Uma fila pode atuar como um buffer para nivelar a carga de trabalho, de modo que os destinatários possam processar as mensagens em seu próprio ritmo.

  • Fluxos de trabalho. As filas podem ser usadas para gerenciar um fluxo de trabalho, marcando a mensagem após cada etapa no fluxo de trabalho.

No entanto, há também alguns desafios para usar o sistema de mensagens assíncrono com eficiência.

  • Acoplamento com a infraestrutura de mensagens. O uso de uma infraestrutura de mensagens específica pode causar um acoplamento apertado com essa infraestrutura. Pode 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 aumentar se as filas de mensagens ficarem cheias.

  • Custo. Nas taxas de transferência altas, 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 ao eliminar a duplicação ou ao tornar as operações idempotentes. Também é difícil implementar a semântica de solicitação-resposta usando o sistema de mensagens assíncrono. Para enviar uma resposta, você precisa de outra fila, além de uma maneira de correlacionar as mensagens de solicitação e de resposta.

  • Taxa de transferência. Se as mensagens exigirem uma semântica de fila, a fila poderá se tornar um gargalo no sistema. Cada mensagem exige, pelo menos, uma operação de fila e uma operação de remoção da fila. Além disso, a semântica de fila geralmente exige algum tipo de bloqueio na infraestrutura de mensagens. Se a fila for um serviço gerenciado, poderá haver latência adicional, pois a fila é externa à rede virtual do cluster. Você pode mitigar esses problemas por meio de mensagens de lote, mas isso complica o código. Se as mensagens não exigirem semântica de fila, você poderá usar um fluxo de eventos em vez de uma fila. Para obter mais informações, consulte o estilo de arquitetura controlado por eventos.

Entrega por drone: escolhendo os padrões de mensagens

Esta solução utiliza o exemplo de Entrega de Drones. É ideal para as indústrias aeroespacial e aeronáutica.

Com essas considerações em mente, a equipe de desenvolvimento fez as seguintes opções de design para o aplicativo de entrega por drone:

  • O serviço Ingestão expõe uma API de REST pública que os aplicativos clientes usam para agendar, atualizar ou cancelar as entregas.

  • O serviço de ingestão usa os Hubs de Eventos para enviar mensagens assíncronas para o serviço de agendamento. As mensagens assíncronas são necessárias para implementar o nivelamento de carga que é necessário para a ingestão de dados.

  • Os serviços Conta, Entrega, Pacote, Drone e Transporte de Terceiros todos expõem APIs REST internas. O serviço Agendador chama essas APIs para executar uma solicitação de usuário. Um motivo para usar as APIs síncronas é que o Agendador precisa obter uma resposta de cada um dos serviços de downstream. Uma falha em qualquer um dos serviços de downstream indica que toda a operação falhou. No entanto, um problema potencial é a quantidade de latência que é introduzida ao chamar os serviços de back-end.

  • Se algum serviço de downstream tiver uma falha não transitória, toda a transação deverá ser marcada como com falha. Para lidar com este caso, o serviço Agendador envia uma mensagem assíncrona para o Supervisor, para que o Supervisor possa agendar transações de compensação.

  • O serviço Entrega expõe uma API pública que os clientes podem usar para obter o status de uma entrega. No artigo Gateway da API, discutiremos 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 as APIs.

  • Enquanto um drone está em trânsito, o serviço Drone envia eventos que contêm a localização e o status atual do drone. O serviço de entrega ouve esses eventos para monitorar o status de uma entrega.

  • Quando o status de uma entrega é alterado, o serviço Entrega envia um evento de status de entrega, como DeliveryCreated ou DeliveryCompleted. Qualquer serviço pode assinar esses eventos. No projeto atual, o serviço Histórico de Entrega é o único assinante, mas poderá haver outros assinantes posteriormente. Por exemplo, os eventos poderão ir para um serviço de análise em tempo real. E, uma vez que o Agendador não precisa aguardar uma resposta, a adição de mais assinantes não afeta o caminho principal do fluxo de trabalho.

Diagrama de comunicação do drone

Observe que os eventos de status de entrega são derivados de eventos de localização de drone. Por exemplo, quando um drone alcança um local de entrega e deixa um pacote, o serviço de entrega traduz isso para um evento <>. Este é um exemplo de raciocínio em termos de modelos de domínio. Conforme descrito anteriormente, o Gerenciamento de Drone pertence a um contexto limitado separado. Os eventos de drone transmitem a localização física de um drone. Por outro lado, os eventos de entrega representam as alterações no status de uma entrega, que é uma entidade de negócios diferente.

Usando uma malha de serviço

Uma malha de serviço é uma camada de software que gerencia a comunicação entre serviços. As malhas de serviço são projetadas para abordar muitas das preocupações listadas na seção anterior e para levar a responsabilidade por essas preocupações para longe dos próprios microsserviços e para dentro de uma camada compartilhada. A malha de serviço atua como um proxy que intercepta a comunicação de rede entre os microsserviços no cluster. Atualmente, o conceito de malha de serviço se aplica principalmente a orquestradores de contêiner, em vez de arquiteturas sem servidor.

Observação

A malha de serviço é um exemplo do Padrão embaixador — um serviço auxiliar que envia as 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 evoluindo rapidamente. No entanto, alguns recursos que o Linkerd e o Istio têm em comum incluem:

  • O 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 da camada 4 fornecido pelo Kubernetes.

  • Roteamento da camada 7 com base no caminho da URL, no cabeçalho de Host, na versão da API ou em outras regras no nível de aplicativo.

  • Repetição de solicitações com falha. Uma malha de serviço reconhece os códigos de erro de HTTP e pode repetir automaticamente as solicitações com falha. Você pode configurar o número máximo de repetições, juntamente com um período de tempo limite para delimitar a latência máxima.

  • Interrupção de circuito 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 retirada, ele tenta 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ção, a latência, as taxas de êxito e de erro e os tamanhos das respostas. A malha de serviço também habilita o rastreamento distribuído ao adicionar informações de correlação para cada salto em uma solicitação.

  • Autenticação de TLS mútua para chamadas de serviço a serviço.

Você precisa de uma malha de serviço? Depende. Sem uma malha de serviço, você precisa considerar cada um dos desafios mencionados no início deste artigo. Você pode resolver problemas como repetição, disjuntor e rastreamento distribuído sem uma malha de serviço, mas uma malha de serviço transfere esses problemas dos serviços individuais para uma camada dedicada. Por outro lado, a implantação de uma malha de serviço adiciona complexidade à instalação e à configuração do cluster. Pode haver implicações de desempenho, pois as solicitações agora são roteadas por meio do proxy da malha de serviços e porque os serviços extras agora estão em execução em todos os nós do cluster. Você deve realizar testes de carga e de desempenho minuciosos antes de implementar 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 deverá falhar.

Há dois casos a serem considerados:

  • Um serviço pode sofrer uma falha transitória , como um timeout de rede. Esses erros geralmente podem ser resolvidos repetindo a chamada. Se a operação ainda falhar após um determinado número de tentativas, ela será considerada uma falha não transitória.

  • Uma falha não transitória é qualquer falha que dificilmente desaparecerá por si só. As falhas não transitórias incluem condições de erro normais, como uma entrada inválida. Elas também incluem exceções sem tratamento no código do aplicativo ou uma falha de processo. Se esse tipo de erro ocorrer, a transação comercial inteira deverá ser marcada como uma falha. Talvez seja necessário desfazer outras etapas na mesma transação que já foi bem-sucedida.

Após uma falha não transitória, a transação atual poderá 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á tiver agendado um drone, ele deverá ser cancelado. Nesse caso, o aplicativo deverá desfazer as etapas concluídas com êxito usando uma transação de compensação. Em alguns casos, essa ação deve ser realizada por um sistema externo ou até mesmo por um processo manual. Em seu design, lembre-se de que as medidas compensatórias também estão sujeitas a falhas.

Se a lógica das transações de compensação for complexa, considere a criação de um serviço separado responsável por esse processo. No aplicativo Drone Delivery, o serviço Scheduler coloca as operações com falha em uma fila dedicada. Um microsserviço separado, denominado Supervisor, lê dessa fila e chama uma API de cancelamento nos serviços que precisam ser compensados. Essa é uma variação do Padrão de Supervisor de Agente do Agendador. O serviço de Supervisor também pode executar outras ações, como notificar o usuário por e-mail ou por SMS ou enviar um alerta para um painel de operações.

Diagrama mostrando o microsserviço de Supervisor

O serviço de Agendador em si pode falhar (por exemplo, por causa de uma falha no nó). Nesse caso, uma nova instância pode girar e assumir o controle. No entanto, as transações ainda em andamento deverão 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 será idempotente se puder ser chamada várias vezes sem produzir efeitos colaterais adicionais após a primeira chamada. Essencialmente, o serviço downstream deve ignorar chamadas duplicadas, o que significa que o serviço deve ser capaz de detectar chamadas duplicadas. Nem sempre é simples implementar métodos idempotentes. Para saber mais, confira Operações idempotentes.

Próximas etapas

Para microsserviços que conversam diretamente entre si, é importante criar APIs bem projetadas.