Editar

Criar APIs para microsserviços

Azure DevOps

Uma boa conceção de API é importante numa arquitetura de microsserviços, porque todas as trocas de dados entre serviços ocorrem através de mensagens ou chamadas à API. As APIs têm de ser eficientes para evitar criar E/S chatty. Uma vez que os serviços são concebidos por equipas que trabalham de forma independente, as APIs têm de ter esquemas de semântica e controlo de versões bem definidos, para que as atualizações não quebrem outros serviços.

Design de API para microsserviços

É importante distinguir entre dois tipos de API:

  • APIs públicas que as aplicações cliente chamam.
  • APIs de back-end que são utilizadas para comunicação entre serviços.

Estes dois casos de utilização têm requisitos um pouco diferentes. Uma API pública tem de ser compatível com aplicações cliente, normalmente aplicações de browser ou aplicações móveis nativas. Na maioria das vezes, significa que a API pública utilizará REST através de HTTP. No entanto, para as APIs de back-end, tem de ter em conta o desempenho da rede. Consoante a granularidade dos seus serviços, a comunicação entre serviços pode resultar num grande tráfego de rede. Os serviços podem tornar-se rapidamente vinculados à E/S. Por esse motivo, considerações como a velocidade de serialização e o tamanho do payload tornam-se mais importantes. Algumas alternativas populares à utilização do REST através de HTTP incluem gRPC, Apache Avro e Apache Thrift. Estes protocolos suportam a serialização binária e são geralmente mais eficientes do que HTTP.

Considerações

Eis alguns aspetos em que deve pensar ao escolher como implementar uma API.

REST versus RPC. Considere as desvantagens entre a utilização de uma interface de estilo REST em comparação com uma interface de estilo RPC.

  • O REST modela recursos, o que pode ser uma forma natural de expressar o seu modelo de domínio. Define uma interface uniforme baseada em verbos HTTP, o que incentiva a evolução. Tem semântica bem definida em termos de idempotência, efeitos colaterais e códigos de resposta. Além disso, impõe a comunicação sem estado, o que melhora a escalabilidade.

  • O RPC é mais orientado em torno de operações ou comandos. Uma vez que as interfaces RPC parecem chamadas de método local, pode levar-lhe a criar APIs excessivamente chatas. No entanto, isso não significa que rPC deve ser chatty. Significa apenas que tem de ter cuidado ao estruturar a interface.

Para uma interface RESTful, a escolha mais comum é REST em http com JSON. Para uma interface de estilo RPC, existem várias arquiteturas populares, incluindo gRPC, Apache Avro e Apache Thrift.

Eficiência. Considere a eficiência em termos de velocidade, memória e tamanho do payload. Normalmente, uma interface baseada em gRPC é mais rápida do que REST através de HTTP.

IDL (interface definition language) (IDL[IDL]). Um IDL é utilizado para definir os métodos, parâmetros e valores devolvidos de uma API. Um IDL pode ser utilizado para gerar código de cliente, código de serialização e documentação da API. Os IDLs também podem ser consumidos por ferramentas de teste de API, como o Postman. Arquiteturas como gRPC, Avro e Thrift definem as suas próprias especificações de IDL. O REST através de HTTP não tem um formato de IDL padrão, mas uma opção comum é OpenAPI (anteriormente Swagger). Também pode criar uma API REST HTTP sem utilizar uma linguagem de definição formal, mas depois perde as vantagens da geração e teste de código.

Serialização. Como é que os objetos são serializados através do fio? As opções incluem formatos baseados em texto (principalmente JSON) e formatos binários, como a memória intermédia do protocolo. Geralmente, os formatos binários são mais rápidos do que os formatos baseados em texto. No entanto, o JSON tem vantagens em termos de interoperabilidade, porque a maioria das linguagens e arquiteturas suportam a serialização JSON. Alguns formatos de serialização requerem um esquema fixo e alguns requerem a compilação de um ficheiro de definição de esquema. Nesse caso, terá de incorporar este passo no processo de compilação.

Suporte de arquitetura e linguagem. O HTTP é suportado em quase todas as arquiteturas e linguagens. GRPC, Avro e Thrift têm bibliotecas para C++, C#, Java e Python. A Thrift e a gRPC também suportam Go.

Compatibilidade e interoperabilidade. Se escolher um protocolo como gRPC, poderá precisar de uma camada de tradução de protocolos entre a API pública e o back-end. Um gateway pode executar essa função. Se estiver a utilizar uma malha de serviços, considere que protocolos são compatíveis com a malha de serviços. Por exemplo, o Linkerd tem suporte incorporado para HTTP, Thrift e gRPC.

A nossa recomendação de linha de base é escolher REST em vez de HTTP, a menos que precise dos benefícios de desempenho de um protocolo binário. REST através de HTTP não requer bibliotecas especiais. Cria um acoplamento mínimo, porque os autores de chamadas não precisam de um stub de cliente para comunicar com o serviço. Existem ecossistemas avançados de ferramentas para suportar definições de esquema, testes e monitorização de pontos finais HTTP RESTful. Por fim, HTTP é compatível com clientes de browser, pelo que não precisa de uma camada de tradução de protocolos entre o cliente e o back-end.

No entanto, se escolher REST em vez de HTTP, deve efetuar testes de desempenho e carga no início do processo de desenvolvimento, para validar se tem um bom desempenho para o seu cenário.

Estrutura da API RESTful

Existem muitos recursos para criar APIs RESTful. Eis algumas que poderá considerar úteis:

Seguem-se algumas considerações específicas a ter em conta.

  • Tenha atenção às APIs que divulgam detalhes de implementação interna ou simplesmente espelham um esquema de base de dados interno. A API deve modelar o domínio. É um contrato entre serviços e, idealmente, só deve ser alterado quando é adicionada uma nova funcionalidade, não apenas porque refatorizou algum código ou normalizou uma tabela de base de dados.

  • Diferentes tipos de cliente, como a aplicação móvel e o browser de ambiente de trabalho, podem exigir diferentes tamanhos de payload ou padrões de interação. Considere utilizar o padrão Back-ends para Front-ends para criar back-ends separados para cada cliente, o que expõe uma interface ideal para esse cliente.

  • Para operações com efeitos colaterais, considere torná-los idempotentes e implementá-los como métodos PUT. Isto permitirá repetições seguras e pode melhorar a resiliência. O artigo Comunicação entre serviços aborda este problema mais detalhadamente.

  • Os métodos HTTP podem ter semântica assíncrona, em que o método devolve uma resposta imediatamente, mas o serviço executa a operação de forma assíncrona. Nesse caso, o método deve devolver um código de resposta HTTP 202 , que indica que o pedido foi aceite para processamento, mas o processamento ainda não foi concluído. Para obter mais informações, veja Padrão de Request-Reply assíncrona.

Mapear padrões REST para DDD

Padrões como entidade, agregação e objeto de valor foram concebidos para colocar determinadas restrições nos objetos no seu modelo de domínio. Em muitas discussões de DDD, os padrões são modelados através de conceitos de linguagem orientada para objetos (OO), como construtores ou getters de propriedade e setters. Por exemplo, os objetos de valor devem ser imutáveis. Numa linguagem de programação OO, imporia isto ao atribuir os valores no construtor e ao tornar as propriedades só de leitura:

export class Location {
    readonly latitude: number;
    readonly longitude: number;

    constructor(latitude: number, longitude: number) {
        if (latitude < -90 || latitude > 90) {
            throw new RangeError('latitude must be between -90 and 90');
        }
        if (longitude < -180 || longitude > 180) {
            throw new RangeError('longitude must be between -180 and 180');
        }
        this.latitude = latitude;
        this.longitude = longitude;
    }
}

Estes tipos de práticas de codificação são particularmente importantes na criação de uma aplicação monolítica tradicional. Com uma base de código grande, muitos subsistemas podem utilizar o Location objeto, pelo que é importante que o objeto aplique o comportamento correto.

Outro exemplo é o padrão repositório, que garante que outras partes da aplicação não fazem leituras diretas ou escritas no arquivo de dados:

Diagrama de um repositório drone.

No entanto, numa arquitetura de microsserviços, os serviços não partilham a mesma base de código e não partilham arquivos de dados. Em vez disso, comunicam através de APIs. Considere o caso em que o serviço Scheduler pede informações sobre um drone do serviço Drone. O serviço Drone tem o seu modelo interno de um drone, expresso através de código. No entanto, o Scheduler não vê isso. Em vez disso, obtém uma representação da entidade drone — talvez um objeto JSON numa resposta HTTP.

Este exemplo é ideal para as indústrias aeronáutica e aeroespacial.

Diagrama do serviço Drone.

O serviço Scheduler não pode modificar os modelos internos do serviço Drone nem escrever no arquivo de dados do serviço Drone. Isto significa que o código que implementa o serviço Drone tem uma área de superfície exposta mais pequena, em comparação com o código num monólito tradicional. Se o serviço Drone definir uma classe Localização, o âmbito dessa classe é limitado . Nenhum outro serviço consumirá diretamente a classe.

Por estas razões, esta orientação não se centra muito nas práticas de codificação, uma vez que se relacionam com os padrões de DDD táticos. No entanto, também pode modelar muitos dos padrões DDD através de APIs REST.

Por exemplo:

  • As agregações mapeiam naturalmente para recursos em REST. Por exemplo, a agregação entrega seria exposta como um recurso pela API de Entrega.

  • Os agregados são limites de consistência. As operações em agregados nunca devem deixar uma agregação num estado inconsistente. Por conseguinte, deve evitar a criação de APIs que permitam a um cliente manipular o estado interno de uma agregação. Em vez disso, favoreça as APIs agregadas que expõem agregados como recursos.

  • As entidades têm identidades exclusivas. No REST, os recursos têm identificadores exclusivos sob a forma de URLs. Crie URLs de recursos que correspondam à identidade de domínio de uma entidade. O mapeamento do URL para a identidade de domínio pode ser opaco para o cliente.

  • As entidades subordinadas de uma agregação podem ser acedidas ao navegar a partir da entidade raiz. Se seguir os princípios HATEOAS , as entidades subordinadas podem ser acedidas através de ligações na representação da entidade principal.

  • Uma vez que os objetos de valor são imutáveis, as atualizações são executadas ao substituir todo o objeto de valor. No REST, implemente atualizações através de pedidos PUT ou PATCH.

  • Um repositório permite aos clientes consultar, adicionar ou remover objetos numa coleção, abstraindo os detalhes do arquivo de dados subjacente. No REST, uma coleção pode ser um recurso distinto, com métodos para consultar a coleção ou adicionar novas entidades à coleção.

Quando estruturar as suas APIs, pense na forma como expressam o modelo de domínio, não apenas os dados dentro do modelo, mas também as operações empresariais e as restrições nos dados.

Conceito de DDD Rest equivalente Exemplo
Agregação Recurso { "1":1234, "status":"pending"... }
Identidade URL https://delivery-service/deliveries/1
Entidades subordinadas Ligações { "href": "/deliveries/1/confirmation" }
Atualizar objetos de valor PUT ou PATCH PUT https://delivery-service/deliveries/1/dropoff
Repositório Coleção https://delivery-service/deliveries?status=pending

Controlo de versões da API

Uma API é um contrato entre um serviço e clientes ou consumidores desse serviço. Se uma API for alterada, existe o risco de quebrar clientes que dependem da API, quer sejam clientes externos ou outros microsserviços. Por conseguinte, é uma boa ideia minimizar o número de alterações à API que efetua. Muitas vezes, as alterações na implementação subjacente não requerem alterações à API. Realisticamente, no entanto, em algum momento, vai querer adicionar novas funcionalidades ou novas capacidades que requerem a alteração de uma API existente.

Sempre que possível, torne as alterações da API retrocompatíveis. Por exemplo, evite remover um campo de um modelo, uma vez que pode interromper os clientes que esperam que o campo esteja lá. Adicionar um campo não interrompe a compatibilidade, porque os clientes devem ignorar quaisquer campos que não compreendam numa resposta. No entanto, o serviço tem de processar o caso em que um cliente mais antigo omite o novo campo num pedido.

Suporte para o controlo de versões no seu contrato de API. Se introduzir uma alteração interruptiva da API, introduza uma nova versão da API. Continue a suportar a versão anterior e permita que os clientes selecionem a versão a chamar. Há algumas formas de o fazer. Uma delas é simplesmente expor ambas as versões no mesmo serviço. Outra opção é executar duas versões do serviço lado a lado e encaminhar pedidos para uma ou outra versão, com base em regras de encaminhamento HTTP.

Diagrama a mostrar duas opções para suportar o controlo de versões.

O diagrama tem duas partes. "O serviço suporta duas versões" mostra o Cliente v1 e o Cliente v2 a apontar para um Serviço. "Implementação lado a lado" mostra o Cliente v1 a apontar para um Serviço v1 e o Cliente v2 a apontar para um Serviço v2.

Existe um custo para suportar várias versões, em termos de tempo de programador, teste e sobrecarga operacional. Portanto, é bom preterir versões antigas o mais rapidamente possível. Para as APIs internas, a equipa proprietária da API pode trabalhar com outras equipas para as ajudar a migrar para a nova versão. Isto acontece quando um processo de governação entre equipas é útil. Para APIs externas (públicas), pode ser mais difícil preterir uma versão da API, especialmente se a API for consumida por terceiros ou por aplicações cliente nativas.

Quando uma implementação de serviço é alterada, é útil etiquetar a alteração com uma versão. A versão fornece informações importantes ao resolver erros. Pode ser muito útil para a análise da causa raiz saber exatamente qual a versão do serviço que foi chamada. Considere utilizar o controlo de versões semântico para versões de serviço. O controlo de versões semântico utiliza um MAJOR. MENOR. Formato PATCH . No entanto, os clientes só devem selecionar uma API pelo número da versão principal ou, possivelmente, a versão secundária se existirem alterações significativas (mas não interruptivas) entre versões secundárias. Por outras palavras, é razoável que os clientes selecionem entre a versão 1 e a versão 2 de uma API, mas não para selecionar a versão 2.1.3. Se permitir esse nível de granularidade, arrisca-se a ter de suportar uma proliferação de versões.

Para mais discussões sobre o controlo de versões da API, veja Controlo de versões de uma API Web RESTful.

Operações Idempotent

Uma operação é idempotente se puder ser chamada várias vezes sem produzir efeitos colaterais adicionais após a primeira chamada. A Idempotência pode ser uma estratégia de resiliência útil, porque permite que um serviço a montante invoque uma operação em segurança várias vezes. Para um debate sobre este ponto, veja Transações distribuídas.

A especificação HTTP indica que os métodos GET, PUT e DELETE têm de ser idempotentes. Não é garantido que os métodos POST sejam idempotentes. Se um método POST criar um novo recurso, geralmente não há garantias de que esta operação seja idempotente. A especificação define idempotent desta forma:

Um método de pedido é considerado "idempotente" se o efeito pretendido no servidor de vários pedidos idênticos com esse método for o mesmo que o efeito para um único pedido desse tipo. (RFC 7231)

É importante compreender a diferença entre a semântica PUT e POST ao criar uma nova entidade. Em ambos os casos, o cliente envia uma representação de uma entidade no corpo do pedido. Mas o significado do URI é diferente.

  • Para um método POST, o URI representa um recurso principal da nova entidade, como uma coleção. Por exemplo, para criar uma nova entrega, o URI pode ser /api/deliveries. O servidor cria a entidade e atribui-lhe um novo URI, como /api/deliveries/39660. Este URI é devolvido no cabeçalho Localização da resposta. Sempre que o cliente enviar um pedido, o servidor criará uma nova entidade com um novo URI.

  • Para um método PUT, o URI identifica a entidade. Se já existir uma entidade com esse URI, o servidor substitui a entidade existente pela versão no pedido. Se não existir nenhuma entidade com esse URI, o servidor cria uma. Por exemplo, suponha que o cliente envia um pedido PUT para api/deliveries/39660. Partindo do princípio de que não existe nenhuma entrega com esse URI, o servidor cria um novo. Agora, se o cliente enviar o mesmo pedido novamente, o servidor substituirá a entidade existente.

Eis a implementação do método PUT pelo serviço de Entrega.

[HttpPut("{id}")]
[ProducesResponseType(typeof(Delivery), 201)]
[ProducesResponseType(typeof(void), 204)]
public async Task<IActionResult> Put([FromBody]Delivery delivery, string id)
{
    logger.LogInformation("In Put action with delivery {Id}: {@DeliveryInfo}", id, delivery.ToLogInfo());
    try
    {
        var internalDelivery = delivery.ToInternal();

        // Create the new delivery entity.
        await deliveryRepository.CreateAsync(internalDelivery);

        // Create a delivery status event.
        var deliveryStatusEvent = new DeliveryStatusEvent { DeliveryId = delivery.Id, Stage = DeliveryEventType.Created };
        await deliveryStatusEventRepository.AddAsync(deliveryStatusEvent);

        // Return HTTP 201 (Created)
        return CreatedAtRoute("GetDelivery", new { id= delivery.Id }, delivery);
    }
    catch (DuplicateResourceException)
    {
        // This method is mainly used to create deliveries. If the delivery already exists then update it.
        logger.LogInformation("Updating resource with delivery id: {DeliveryId}", id);

        var internalDelivery = delivery.ToInternal();
        await deliveryRepository.UpdateAsync(id, internalDelivery);

        // Return HTTP 204 (No Content)
        return NoContent();
    }
}

Espera-se que a maioria dos pedidos crie uma nova entidade, pelo que o método chama CreateAsync o objeto de repositório de forma otimista e, em seguida, processa quaisquer exceções de recursos duplicados ao atualizar o recurso.

Passos seguintes

Saiba como utilizar um gateway de API no limite entre aplicações cliente e microsserviços.