Editor – Esta série de artigos em sete partes está agora completa:
Você também pode baixar o conjunto completo de artigos, além de informações sobre a implementação de microsserviços usando o NGINX Plus, como um e-book – Microsserviços: Do design à implantação . Além disso, confira a nova página Soluções de Microsserviços .
Este é o terceiro artigo da nossa série sobre como criar aplicativos com uma arquitetura de microsserviços. O primeiro artigo apresenta o padrão de Arquitetura de Microsserviços , compara-o com o padrão de Arquitetura Monolítica e discute os benefícios e desvantagens do uso de microsserviços. O segundo artigo descreve como os clientes de um aplicativo se comunicam com os microsserviços por meio de um intermediário conhecido como API Gateway . Neste artigo, veremos como os serviços dentro de um sistema se comunicam entre si. O quarto artigo explora o problema intimamente relacionado da descoberta de serviços.
Em um aplicativo monolítico, os componentes invocam uns aos outros por meio de chamadas de métodos ou funções em nível de linguagem. Em contraste, um aplicativo baseado em microsserviços é um sistema distribuído executado em várias máquinas. Cada instância de serviço é normalmente um processo. Consequentemente, como mostra o diagrama a seguir, os serviços devem interagir usando um mecanismo de comunicação entre processos (IPC).
Mais tarde, veremos tecnologias IPC específicas, mas primeiro vamos explorar várias questões de design.
Ao selecionar um mecanismo de IPC para um serviço, é útil pensar primeiro em como os serviços interagem. Há uma variedade de estilos de interação cliente⇔serviço. Eles podem ser categorizados em duas dimensões. A primeira dimensão é se a interação é um-para-um ou um-para-muitos:
A segunda dimensão é se a interação é síncrona ou assíncrona:
A tabela a seguir mostra os vários estilos de interação.
Um para um | Um para muitos | |
---|---|---|
Síncrono | Solicitação/resposta | — |
Assíncrono | Notificação | Publicar/assinar |
Solicitação/resposta assíncrona | Respostas publicadas/assíncronas |
Existem os seguintes tipos de interações um-para-um:
Existem os seguintes tipos de interações um-para-muitos:
Cada serviço normalmente usa uma combinação desses estilos de interação. Para alguns serviços, um único mecanismo IPC é suficiente. Outros serviços podem precisar usar uma combinação de mecanismos de IPC. O diagrama a seguir mostra como os serviços em um aplicativo de táxi podem interagir quando o usuário solicita uma viagem.
Os serviços usam uma combinação de notificações, solicitação/resposta e publicação/assinatura. Por exemplo, o smartphone do passageiro envia uma notificação ao serviço Trip Management para solicitar uma coleta. O serviço de gerenciamento de viagens verifica se a conta do passageiro está ativa usando solicitação/resposta para invocar o serviço de passageiros. O serviço de gerenciamento de viagens cria a viagem e usa publicar/assinar para notificar outros serviços, incluindo o Dispatcher, que localiza um motorista disponível.
Agora que vimos os estilos de interação, vamos dar uma olhada em como definir APIs.
A API de um serviço é um contrato entre o serviço e seus clientes. Independentemente da sua escolha do mecanismo IPC, é importante definir precisamente a API de um serviço usando algum tipo de linguagem de definição de interface (IDL). Há até bons argumentos para usar uma abordagem API-first para definir serviços. Você começa o desenvolvimento de um serviço escrevendo a definição da interface e revisando-a com os desenvolvedores do cliente. Somente após iterar na definição da API você implementa o serviço. Fazer esse design antecipadamente aumenta suas chances de criar um serviço que atenda às necessidades de seus clientes.
Como você verá mais adiante neste artigo, a natureza da definição da API depende de qual mecanismo IPC você está usando. Se você estiver usando mensagens, a API consiste nos canais de mensagens e nos tipos de mensagens. Se você estiver usando HTTP, a API consiste em URLs e nos formatos de solicitação e resposta. Mais adiante descreveremos alguns IDLs com mais detalhes.
A API de um serviço muda invariavelmente ao longo do tempo. Em um aplicativo monolítico, geralmente é simples alterar a API e atualizar todos os chamadores. Em um aplicativo baseado em microsserviços, é muito mais difícil, mesmo que todos os consumidores da sua API sejam outros serviços no mesmo aplicativo. Normalmente, não é possível forçar todos os clientes a atualizarem em conjunto com o serviço. Além disso, você provavelmente implantará incrementalmente novas versões de um serviço, de modo que versões antigas e novas do serviço sejam executadas simultaneamente. É importante ter uma estratégia para lidar com essas questões.
A maneira como você lida com uma alteração de API depende do tamanho da alteração. Algumas alterações são pequenas e compatíveis com a versão anterior. Você pode, por exemplo, adicionar atributos a solicitações ou respostas. Faz sentido projetar clientes e serviços de modo que observem o princípio da robustez . Clientes que usam uma API mais antiga devem continuar trabalhando com a nova versão do serviço. O serviço fornece valores padrão para os atributos de solicitação ausentes e os clientes ignoram quaisquer atributos de resposta extras. É importante usar um mecanismo IPC e um formato de mensagem que permitam que você evolua facilmente suas APIs.
Às vezes, no entanto, você precisa fazer grandes mudanças incompatíveis em uma API. Como você não pode forçar os clientes a atualizarem imediatamente, um serviço precisa dar suporte a versões mais antigas da API por algum período de tempo. Se você estiver usando um mecanismo baseado em HTTP, como REST, uma abordagem é incorporar o número da versão na URL. Cada instância de serviço pode manipular várias versões simultaneamente. Como alternativa, você pode implantar instâncias diferentes, cada uma manipulando uma versão específica.
Considere, por exemplo, o cenário Detalhes do produto desse artigo. Vamos imaginar que o Serviço de Recomendação não esteja respondendo. Uma implementação ingênua de um cliente pode bloquear a espera indefinidamente por uma resposta. Isso não só resultaria em uma experiência ruim para o usuário, mas em muitos aplicativos consumiria um recurso precioso, como um thread. Eventualmente, o tempo de execução ficaria sem threads e não responderia, como mostrado na figura a seguir.
Para evitar esse problema, é essencial que você projete seus serviços para lidar com falhas parciais.
Uma boa abordagem a seguir é a descrita pela Netflix . As estratégias para lidar com falhas parciais incluem:
Netflix Hystrix é uma biblioteca de código aberto que implementa esses e outros padrões. Se você estiver usando a JVM, você definitivamente deve considerar usar o Hystrix. E, se você estiver executando em um ambiente não JVM, deverá usar uma biblioteca equivalente.
Há muitas tecnologias IPC diferentes para escolher. Os serviços podem usar mecanismos de comunicação baseados em solicitação/resposta síncrona, como REST baseado em HTTP ou Thrift. Como alternativa, eles podem usar mecanismos de comunicação assíncronos baseados em mensagens, como AMQP ou STOMP. Há também uma variedade de formatos de mensagens diferentes. Os serviços podem usar formatos baseados em texto legíveis por humanos, como JSON ou XML. Como alternativa, eles podem usar um formato binário (que é mais eficiente), como Avro ou Protocol Buffers. Mais tarde, veremos os mecanismos IPC síncronos, mas primeiro vamos discutir os mecanismos IPC assíncronos.
Ao usar mensagens, os processos se comunicam por meio de troca de mensagens assíncrona. Um cliente faz uma solicitação a um serviço enviando uma mensagem. Se for esperado que o serviço responda, ele o fará enviando uma mensagem separada ao cliente. Como a comunicação é assíncrona, o cliente não fica bloqueado esperando por uma resposta. Em vez disso, o cliente é escrito assumindo que a resposta não será recebida imediatamente.
Uma mensagem consiste em cabeçalhos (metadados como o remetente) e um corpo de mensagem. As mensagens são trocadas por canais . Qualquer número de produtores pode enviar mensagens para um canal. Da mesma forma, qualquer número de consumidores pode receber mensagens de um canal. Existem dois tipos de canais: ponto a ponto e publicação-assinatura . Um canal ponto a ponto entrega uma mensagem exatamente para um dos consumidores que está lendo o canal. Os serviços usam canais ponto a ponto para os estilos de interação um a um descritos anteriormente. Um canal de publicação-assinatura entrega cada mensagem a todos os consumidores vinculados. Os serviços usam canais de publicação-assinatura para os estilos de interação de um para muitos descritos acima.
O diagrama a seguir mostra como o aplicativo de chamada de táxi pode usar canais de publicação-assinatura.
O serviço de gerenciamento de viagens notifica serviços interessados, como o Dispatcher, sobre uma nova viagem escrevendo uma mensagem de viagem criada em um canal de publicação-assinatura. O Dispatcher encontra um driver disponível e notifica outros serviços escrevendo uma mensagem Driver Proposed em um canal de publicação-assinatura.
Há muitos sistemas de mensagens para escolher. Você deve escolher um que suporte uma variedade de linguagens de programação. Alguns sistemas de mensagens suportam protocolos padrão, como AMQP e STOMP. Outros sistemas de mensagens têm protocolos proprietários, mas documentados. Há um grande número de sistemas de mensagens de código aberto para escolher, incluindo RabbitMQ , Apache Kafka , Apache ActiveMQ e NSQ . Em um nível mais alto, todos eles apoiam alguma forma de mensagens e canais. Todos eles se esforçam para ser confiáveis, de alto desempenho e escaláveis. No entanto, há diferenças significativas nos detalhes do modelo de mensagens de cada corretor.
Há muitas vantagens em usar mensagens:
No entanto, há algumas desvantagens em usar mensagens:
Agora que vimos como usar o IPC baseado em mensagens, vamos examinar o IPC baseado em solicitação/resposta.
Ao usar um mecanismo IPC síncrono baseado em solicitação/resposta, um cliente envia uma solicitação a um serviço. O serviço processa a solicitação e envia uma resposta. Em muitos clientes, o thread que faz a solicitação fica bloqueado enquanto aguarda uma resposta. Outros clientes podem usar código de cliente assíncrono e orientado a eventos, que talvez seja encapsulado por Futures ou Rx Observables. No entanto, diferentemente do uso de mensagens, o cliente assume que a resposta chegará em tempo hábil. Há vários protocolos para escolher. Dois protocolos populares são REST e Thrift. Vamos primeiro dar uma olhada no REST.
Hoje em dia está na moda desenvolver APIs no estilo RESTful . REST é um mecanismo IPC que (quase sempre) usa HTTP. Um conceito-chave em REST é um recurso, que normalmente representa um objeto de negócios, como um Cliente ou Produto, ou uma coleção de objetos de negócios. REST usa verbos HTTP para manipular recursos, que são referenciados usando uma URL. Por exemplo, uma solicitação GET
retorna a representação de um recurso, que pode estar no formato de um documento XML ou objeto JSON. Uma solicitação POST
cria um novo recurso e uma solicitação PUT
atualiza um recurso. Para citar Roy Fielding, o criador do REST:
O diagrama a seguir mostra uma das maneiras pelas quais o aplicativo de chamada de táxi pode usar REST.
O smartphone do passageiro solicita uma viagem fazendo uma solicitação POST
ao recurso /trips
do serviço de gerenciamento de viagens. Este serviço processa a solicitação enviando uma solicitação GET
de informações sobre o passageiro para o serviço de Gerenciamento de Passageiros. Após verificar se o passageiro está autorizado a criar uma viagem, o serviço de Gerenciamento de Viagens cria a viagem e retorna um201
resposta ao smartphone.
Muitos desenvolvedores afirmam que suas APIs baseadas em HTTP são RESTful. No entanto, como Fielding descreve nesta postagem do blog , nem todos eles realmente são. Leonard Richardson (sem parentesco) define um modelo de maturidade muito útil para REST que consiste nos seguintes níveis.
POST
para seu único ponto de extremidade de URL. Cada solicitação especifica a ação a ser executada, o alvo da ação (por exemplo, o objeto de negócios) e quaisquer parâmetros.POST
que especifica a ação a ser executada e quaisquer parâmetros.GET
para recuperar, POST
para criar e PUT
para atualizar. Os parâmetros da consulta de solicitação e o corpo, se houver, especificam os parâmetros da ação. Isso permite que os serviços aproveitem a infraestrutura da web, como o cache para solicitações GET
.GET
contém links para executar as ações permitidas naquele recurso. Por exemplo, um cliente pode cancelar um pedido usando um link na representação do pedido retornada em resposta à solicitação GET
enviada para recuperar o pedido. Os benefícios do HATEOAS incluem não ter mais que conectar URLs ao código do cliente. Outro benefício é que, como a representação de um recurso contém links para as ações permitidas, o cliente não precisa adivinhar quais ações podem ser executadas em um recurso em seu estado atual.Existem inúmeros benefícios em usar um protocolo baseado em HTTP:
curl
(supondo que JSON ou algum outro formato de texto seja usado).Existem algumas desvantagens em usar HTTP:
A comunidade de desenvolvedores redescobriu recentemente o valor de uma linguagem de definição de interface para APIs RESTful. Existem algumas opções, incluindo RAML e Swagger . Alguns IDLs, como o Swagger, permitem que você defina o formato das mensagens de solicitação e resposta. Outros, como RAML, exigem que você use uma especificação separada, como JSON Schema . Além de descrever APIs, os IDLs normalmente têm ferramentas que geram stubs de cliente e esqueletos de servidor a partir de uma definição de interface.
O Apache Thrift é uma alternativa interessante ao REST. É uma estrutura para escrever clientes e servidores RPC entre linguagens. O Thrift fornece um IDL estilo C para definir suas APIs. Você usa o compilador Thrift para gerar stubs do lado do cliente e esqueletos do lado do servidor. O compilador gera código para uma variedade de linguagens, incluindo C++, Java, Python, PHP, Ruby, Erlang e Node.js.
Uma interface Thrift consiste em um ou mais serviços. Uma definição de serviço é análoga a uma interface Java. É uma coleção de métodos fortemente tipados. Os métodos de economia podem retornar um valor (possivelmente nulo) ou podem ser definidos como unidirecionais. Métodos que retornam um valor implementam o estilo de interação de solicitação/resposta. O cliente aguarda uma resposta e pode gerar uma exceção. Os métodos unidirecionais correspondem ao estilo de interação de notificação. O servidor não envia uma resposta.
O Thrift suporta vários formatos de mensagem: JSON, binário e binário compacto. O binário é mais eficiente que o JSON porque é mais rápido de decodificar. E, como o nome sugere, o binário compacto é um formato que economiza espaço. JSON é, obviamente, amigável ao ser humano e ao navegador. O Thrift também oferece uma escolha de protocolos de transporte, incluindo TCP e HTTP brutos. O TCP bruto provavelmente é mais eficiente que o HTTP. No entanto, o HTTP é amigável ao firewall, ao navegador e ao ser humano.
Agora que vimos HTTP e Thrift, vamos examinar a questão dos formatos de mensagem. Se estiver usando um sistema de mensagens ou REST, você poderá escolher o formato da mensagem. Outros mecanismos de IPC, como o Thrift, podem suportar apenas um pequeno número de formatos de mensagem, talvez apenas um. Em ambos os casos, é importante usar um formato de mensagem multilíngue. Mesmo que você esteja escrevendo seus microsserviços em uma única linguagem hoje, é provável que você use outras linguagens no futuro.
Existem dois tipos principais de formatos de mensagem: texto e binário. Exemplos de formatos baseados em texto incluem JSON e XML. Uma vantagem desses formatos é que eles não são apenas legíveis por humanos, mas também autodescritivos. Em JSON, os atributos de um objeto são representados por uma coleção de pares nome-valor. Da mesma forma, em XML os atributos são representados por elementos e valores nomeados. Isso permite que o consumidor de uma mensagem escolha os valores nos quais está interessado e ignore o resto. Consequentemente, pequenas alterações no formato da mensagem podem ser facilmente compatíveis com versões anteriores.
A estrutura dos documentos XML é especificada por um esquema XML . Com o tempo, a comunidade de desenvolvedores percebeu que o JSON também precisa de um mecanismo semelhante. Uma opção é usar o JSON Schema , seja de forma independente ou como parte de um IDL, como o Swagger.
Uma desvantagem de usar um formato de mensagem baseado em texto é que as mensagens tendem a ser prolixas, especialmente XML. Como as mensagens são autodescritivas, cada mensagem contém o nome dos atributos, além de seus valores. Outra desvantagem é a sobrecarga de análise de texto. Consequentemente, você pode considerar usar um formato binário.
Há vários formatos binários para escolher. Se você estiver usando o Thrift RPC, você pode usar o Thrift binário. Se você puder escolher o formato da mensagem, as opções populares incluem Protocol Buffers e Apache Avro . Ambos os formatos fornecem um IDL digitado para definir a estrutura das suas mensagens. Uma diferença, no entanto, é que os Protocol Buffers usam campos marcados, enquanto um consumidor Avro precisa conhecer o esquema para interpretar mensagens. Como resultado, a evolução da API é mais fácil com Protocol Buffers do que com Avro. Esta postagem do blog é uma excelente comparação entre Thrift, Protocol Buffers e Avro.
Os microsserviços devem se comunicar usando um mecanismo de comunicação entre processos. Ao projetar como seus serviços se comunicarão, você precisa considerar várias questões: como os serviços interagem, como especificar a API para cada serviço, como evoluir as APIs e como lidar com falhas parciais. Existem dois tipos de mecanismos de IPC que os microsserviços podem usar: mensagens assíncronas e solicitação/resposta síncrona. No próximo artigo da série, veremos o problema da descoberta de serviços em uma arquitetura de microsserviços.
Editor – Esta série de artigos em sete partes está agora completa:
Você também pode baixar o conjunto completo de artigos, além de informações sobre a implementação de microsserviços usando o NGINX Plus, como um e-book – Microsserviços: Do design à implantação .
O blogueiro convidado Chris Richardson é o fundador do CloudFoundry.com original, um dos primeiros Java PaaS (Plataforma como Serviço) para Amazon EC2. Agora, ele presta consultoria a organizações para melhorar a maneira como elas desenvolvem e implantam aplicativos. Ele também escreve regularmente sobre microsserviços em http://microservices.io .
"Esta postagem do blog pode fazer referência a produtos que não estão mais disponíveis e/ou não têm mais suporte. Para obter as informações mais atualizadas sobre os produtos e soluções F5 NGINX disponíveis, explore nossa família de produtos NGINX . O NGINX agora faz parte do F5. Todos os links anteriores do NGINX.com redirecionarão para conteúdo semelhante do NGINX no F5.com."