Editar

Conceber a comunicação entre serviços para microsserviços

Azure DevOps

A comunicação entre microsserviços tem de ser eficiente e robusta. Com muitos serviços pequenos a interagir para concluir uma única atividade empresarial, isto pode ser um desafio. Neste artigo, vamos analisar as vantagens entre as mensagens assíncronas e as APIs síncronas. Em seguida, analisamos alguns dos desafios na conceção de uma comunicação intersserviço resiliente.

Desafios

Eis alguns dos principais desafios decorrentes da comunicação serviço a serviço. As malhas de serviço, descritas mais adiante neste artigo, foram concebidas para lidar com muitos destes desafios.

Resiliência. Podem existir dezenas ou mesmo centenas de instâncias de um determinado microsserviço. Uma instância pode falhar por vários motivos. Pode ocorrer uma falha ao nível do nó, como uma falha de hardware ou um reinício da VM. Uma instância pode falhar ou ficar sobrecarregada com pedidos e não conseguir processar novos pedidos. Qualquer um destes eventos pode fazer com que uma chamada de rede falhe. Existem dois padrões de estrutura que podem ajudar a tornar as chamadas de rede serviço a serviço mais resilientes:

  • Repita. Uma chamada de rede pode falhar devido a uma falha transitória que desaparece por si só. Em vez de falhar totalmente, o autor da chamada deve normalmente repetir a operação um determinado número de vezes ou até que decorra um período de tempo limite configurado. No entanto, se uma operação não for idempotente, as repetições podem causar efeitos colaterais indesejados. A chamada original pode ser bem-sucedida, mas o autor da chamada nunca recebe uma resposta. Se o autor da chamada tentar novamente, a operação poderá ser invocada duas vezes. Geralmente, não é seguro repetir métodos POST ou PATCH, uma vez que estes não são garantidos como idempotentes.

  • Disjuntor Automático. Demasiados pedidos falhados podem causar um estrangulamento, uma vez que os pedidos 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 disjuntor automático pode impedir que um serviço tente repetidamente uma operação que possa falhar.

Balanceamento de carga. Quando o serviço "A" chama o serviço "B", o pedido tem de 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 é reencaminhado para um pod através de regras iptable. Por predefinição, é escolhido um pod aleatório. Uma malha de serviço (ver abaixo) pode fornecer algoritmos de balanceamento de carga mais inteligentes com base na latência observada ou noutras métricas.

Rastreio distribuído. Uma única transação pode abranger vários serviços. Isto pode dificultar a monitorização do desempenho geral e do estado de funcionamento do sistema. Mesmo que todos os serviços gerem registos e métricas, sem alguma forma de as associar, são de utilização limitada. O artigo Registo e monitorização fala mais sobre rastreio distribuído, mas mencioná-lo aqui como um desafio.

Controlo de versões do serviço. Quando uma equipa implementa uma nova versão de um serviço, tem de evitar interromper quaisquer outros serviços ou clientes externos que dependam do mesmo. Além disso, poderá querer executar várias versões de um serviço lado a lado e encaminhar pedidos para uma versão específica. Veja Controlo de Versões da API para obter mais discussões sobre este problema.

Encriptação TLS e autenticação TLS mútua. Por motivos de segurança, poderá querer encriptar o tráfego entre serviços com TLS e utilizar a autenticação TLS mútua para autenticar os autores de chamadas.

Mensagens síncronas versus assíncronas

Existem dois padrões básicos de mensagens que os microsserviços podem utilizar para comunicar com outros microsserviços.

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

  2. Mensagem assíncrona a passar. Neste 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 está bloqueado enquanto a E/S estiver concluída. É importante para o desempenho, mas é um detalhe de implementação em termos da arquitetura. Um protocolo assíncrono significa que o remetente não aguarda uma resposta. HTTP é um protocolo síncrono, embora um cliente HTTP possa utilizar E/S assíncrona quando envia um pedido.

Existem desvantagens para cada padrão. O pedido/resposta é um paradigma bem compreendido, pelo que conceber uma API pode parecer mais natural do que conceber um sistema de mensagens. No entanto, as mensagens assíncronas têm algumas vantagens que podem ser úteis numa arquitetura de microsserviços:

  • Acoplamento reduzido. O remetente da mensagem não precisa de saber sobre o consumidor.

  • Vários subscritores. Através de um modelo de pub/sub, vários consumidores podem subscrever eventos de receção. Veja Estilo de arquitetura condicionada por eventos.

  • Isolamento de falhas. Se o consumidor falhar, o remetente ainda pode enviar mensagens. As mensagens serão recolhidas quando o consumidor recuperar. Esta capacidade é especialmente útil numa arquitetura de microsserviços, uma vez que cada serviço tem o 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 períodos de indisponibilidade intermitentes. Por outro lado, as APIs síncronas exigem que o serviço a jusante esteja disponível ou a operação falhe.

  • Capacidade de resposta. Um serviço a montante pode responder mais rapidamente se não aguardar pelos serviços a jusante. Isto é especialmente útil numa arquitetura de microsserviços. Se existir uma cadeia de dependências de serviço (o serviço A chama B, que chama C, etc.), aguardar chamadas síncronas pode adicionar quantidades inaceitáveis de latência.

  • Nivelamento de carga. Uma fila pode funcionar como uma memória intermédia para nivelar a carga de trabalho, para que os recetores possam processar mensagens ao seu próprio ritmo.

  • Fluxos de trabalho. As filas podem ser utilizadas para gerir um fluxo de trabalho ao apontar a mensagem após cada passo no fluxo de trabalho.

No entanto, também existem alguns desafios na utilização eficaz de mensagens assíncronas.

  • Acoplamento com a infraestrutura de mensagens. A utilização de uma infraestrutura de mensagens específica pode causar um acoplamento apertado com essa infraestrutura. Será difícil mudar para outra infraestrutura de mensagens mais tarde.

  • Latência. A latência ponto a ponto de uma operação pode tornar-se elevada se as filas de mensagens estiverem preenchidas.

  • Custo. Com elevados débitos, o custo monetário da infra-estrutura de mensagens pode ser significativo.

  • Complexidade. Processar mensagens assíncronas não é uma tarefa trivial. Por exemplo, tem de processar mensagens duplicadas, quer ao des duplicar, quer ao tornar as operações idempotentes. Também é difícil implementar semântica de pedido-resposta com mensagens assíncronas. Para enviar uma resposta, precisa de outra fila, além de uma forma de correlacionar as mensagens de pedido e resposta.

  • Débito. Se as mensagens exigirem semântica de fila, a fila pode tornar-se um estrangulamento no sistema. Cada mensagem requer, pelo menos, uma operação de fila e uma operação de desagregue. Além disso, a semântica de fila geralmente requer algum tipo de bloqueio dentro da infraestrutura de mensagens. Se a fila for um serviço gerido, poderá existir latência adicional, uma vez que a fila é externa à rede virtual do cluster. Pode mitigar estes problemas ao criar mensagens em lotes, mas isso complica o código. Se as mensagens não exigirem semântica de fila, poderá utilizar um fluxo de eventos em vez de uma fila. Para obter mais informações, veja Estilo de arquitetura condicionada por eventos.

Entrega por Drone: Escolher os padrões de mensagens

Esta solução utiliza o exemplo de Entrega por Drone. É ideal para as indústrias aeroespacial e de aeronaves.

Com estas considerações em mente, a equipa de desenvolvimento fez as seguintes escolhas de design para a aplicação Entrega de Drones:

  • O serviço ingestão expõe uma API REST pública que as aplicações cliente utilizam para agendar, atualizar ou cancelar entregas.

  • O serviço ingestão utiliza Hubs de Eventos para enviar mensagens assíncronas para o serviço Scheduler. 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 todas as APIs REST internas. O serviço Scheduler chama estas APIs para realizar um pedido de utilizador. Uma das razões para utilizar APIs síncronas é que o Scheduler precisa de obter uma resposta de cada um dos serviços a jusante. 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 ao chamar os serviços de back-end.

  • Se qualquer serviço a jusante tiver uma falha nãotransiente, toda a transação deve ser marcada como falhada. Para lidar com este caso, o serviço Scheduler 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 utilizar para obter o estado de uma entrega. No artigo Gateway de API, discutimos como um gateway de API pode ocultar os serviços subjacentes do cliente, pelo que o cliente não precisa de saber quais os serviços que expõem as APIs.

  • Enquanto um drone está em voo, o serviço Drone envia eventos que contêm a localização e o estado atuais do drone. O Serviço de entrega escuta estes eventos para controlar o estado de uma entrega.

  • Quando o estado de uma entrega é alterado, o Serviço de entrega envia um evento de estado de entrega, como DeliveryCreated ou DeliveryCompleted. Qualquer serviço pode subscrever estes eventos. Na conceção atual, o serviço Histórico de Entrega é o único subscritor, mas poderá haver outros subscritores mais tarde. Por exemplo, os eventos podem ir para um serviço de análise em tempo real. E como o Scheduler não tem de esperar por uma resposta, adicionar mais subscritores não afeta o caminho do fluxo de trabalho principal.

Diagrama de comunicação com drones

Repare que os eventos de estado de entrega são derivados de eventos de localização de drones. Por exemplo, quando um drone chega a uma localização de entrega e deixa um pacote, o serviço de Entrega traduz-o num evento DeliveryCompleted. Este é um exemplo de pensamento em termos de modelos de domínio. Conforme descrito anteriormente, a Gestão de Drones pertence a um contexto vinculado separado. Os eventos dos drones transmitem a localização física de um drone. Os eventos de entrega, por outro lado, representam alterações no estado de uma entrega, que é uma entidade empresarial diferente.

Utilizar uma malha de serviço

Uma malha de serviço é uma camada de software que processa a comunicação serviço a serviço. As malhas de serviço foram concebidas para abordar muitas das preocupações listadas na secção anterior e mover a responsabilidade por estas preocupações para longe dos próprios microsserviços e para uma camada partilhada. A malha de serviço funciona como um proxy que interceta a comunicação de rede entre microsserviços no cluster. Atualmente, o conceito de malha de serviço aplica-se principalmente aos orquestradores de contentores, em vez de arquiteturas sem servidor.

Nota

A malha de serviços é um exemplo do padrão Ambassador – um serviço auxiliar que envia pedidos de rede em nome da aplicação.

Neste 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, algumas funcionalidades que o Linkerd e o Istio têm em comum incluem:

  • Balanceamento de carga ao nível da sessão, com base nas latências observadas ou no número de pedidos pendentes. Isto pode melhorar o desempenho através do balanceamento de carga de camada 4 fornecido pelo Kubernetes.

  • Encaminhamento de camada 7 com base no caminho do URL, cabeçalho do anfitrião, versão da API ou outras regras ao nível da aplicação.

  • Repetição de pedidos falhados. Uma malha de serviço compreende os códigos de erro HTTP e pode repetir automaticamente pedidos falhados. Pode configurar esse número máximo de repetições, juntamente com um período de tempo limite para vincular a latência máxima.

  • Circuito a quebrar. Se uma instância falhar consistentemente nos pedidos, a malha de serviço irá marcá-la temporariamente como indisponível. Após um período de backoff, tentará novamente a instância. Pode configurar o disjuntor automático com base em vários critérios, como o número de falhas consecutivas,

  • O Service Mesh captura métricas sobre chamadas intersserviços, como o volume de pedidos, latência, taxas de erro e sucesso e tamanhos de resposta. A malha de serviço também permite o rastreio distribuído ao adicionar informações de correlação para cada salto num pedido.

  • 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, terá de considerar cada um dos desafios mencionados no início deste artigo. Pode resolver problemas como a repetição, o disjuntor automático e o rastreio distribuído sem uma malha de serviço, mas uma malha de serviço move estas preocupações para fora dos serviços individuais e para uma camada dedicada. Por outro lado, uma malha de serviço adiciona complexidade à configuração e configuração do cluster. Podem existir implicações de desempenho, porque os pedidos são agora encaminhados através do proxy de malha de serviço e porque os serviços adicionais estão agora em execução em todos os nós do cluster. Deve efetuar testes de carga e desempenho minuciosos antes de implementar uma malha de serviço na produção.

Transações distribuídas

Um desafio comum nos microsserviços é processar corretamente transações que abrangem vários serviços. Muitas vezes, neste cenário, o sucesso de uma transação é tudo ou nada — se um dos serviços participantes falhar, toda a transação tem de falhar.

Existem dois casos a considerar:

  • Um serviço pode deparar-se com uma falha transitória , como um tempo limite de rede. Muitas vezes, estes erros podem ser resolvidos simplesmente repetindo a chamada. Se a operação continuar a falhar após um determinado número de tentativas, é considerada uma falha nãotransiente.

  • Uma falha nãotransiente é qualquer falha que é improvável que desapareça por si só. As falhas nãotransientes incluem condições de erro normais, tais como entrada inválida. Também incluem exceções não processadas no código da aplicação ou um processo que falha. Se este tipo de erro ocorrer, toda a transação empresarial tem de ser marcada como uma falha. Pode ser necessário anular outros passos na mesma transação que já foi bem-sucedida.

Após uma falha nãotransiente, a transação atual pode estar num estado parcialmente falhado , em que um ou mais passos já foram concluídos com êxito. Por exemplo, se o serviço Drone já tiver agendado um drone, o drone tem de ser cancelado. Nesse caso, a aplicação tem de anular os passos que foram bem-sucedidos através de uma Transação de Compensação. Em alguns casos, isto tem de ser feito por um sistema externo ou mesmo por um processo manual.

Se a lógica para compensar transações for complexa, considere criar um serviço separado responsável por este processo. Na aplicação Entrega por Drone, o serviço Scheduler coloca operações falhadas numa fila dedicada. Um microsserviço separado, denominado Supervisor, lê a partir desta fila e chama uma API de cancelamento nos serviços que precisam de compensar. Esta é uma variação do padrão do Supervisor do Agente do Scheduler. O serviço Supervisor também pode efetuar outras ações, como notificar o utilizador por sms ou e-mail ou enviar um alerta para um dashboard de operações.

Diagrama a mostrar o microsserviço supervisor

O próprio serviço Scheduler pode falhar (por exemplo, porque um nó falha). Nesse caso, uma nova instância pode girar e assumir o controlo. No entanto, todas as transações que já estavam em curso têm de ser retomadas.

Uma abordagem é guardar um ponto de verificação num arquivo durável depois de cada passo no fluxo de trabalho ser concluído. Se uma instância do serviço Scheduler falhar no meio de uma transação, uma nova instância pode utilizar o ponto de verificação para retomar o local onde a instância anterior ficou desativada. No entanto, escrever pontos de verificação pode criar uma sobrecarga de desempenho.

Outra opção é conceber 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 tem de ser capaz de detetar chamadas duplicadas. Nem sempre é simples implementar métodos idempotentes. Para obter mais informações, veja Operações Idempotentes.

Passos seguintes

Para microsserviços que falam diretamente entre si, é importante criar APIs bem concebidas.