Contratos, endereços e APIs para microsserviços

Os microsserviços no App Engine normalmente chamam uns aos outros usando APIs RESTful baseadas em HTTP. Também é possível invocá-los em segundo plano usando filas de tarefas, e os princípios de design da API descritos aqui se aplicam. É importante seguir determinados padrões para garantir que o aplicativo baseado em microsserviços seja estável, seguro e funcione bem.

Usar contratos fortes

Um dos aspectos mais importantes de aplicativos baseados em microsserviços é a capacidade de implantar os microsserviços completamente independentes uns dos outros. Para conquistar essa independência, cada microsserviço precisa fornecer um contrato bem definido, com controle de versão, aos clientes, que são outros microsserviços. O serviço não pode quebrar esses contratos com controle de versão até que se tenha certeza de que nenhum outro microsserviço depende de um determinado contrato com controle de versão. Observe que talvez seja necessário reverter outros microsserviços para uma versão de código anterior que exija um contrato anterior. Por isso, é importante considerar esse fato nas políticas de suspensão de uso e desativação.

Uma cultura relacionada a contratos fortes, com controle de versão, é provavelmente o aspecto organizacional mais desafiador de um aplicativo estável baseado em microsserviços. As equipes de desenvolvimento precisam entender a diferença entre uma alteração importante e uma não importante. Elas precisam saber quando uma nova versão principal é obrigatória. Elas também precisam entender como e quando um contrato antigo pode ser retirado de serviço. As equipes precisam empregar técnicas de comunicação apropriadas, inclusive notificações de suspensão de uso e desativação, para garantir a conscientização de alterações feitas em contratos de microsserviços. Isso pode parecer complicado, mas a criação dessas práticas na cultura do desenvolvimento produzirá grandes melhorias na velocidade e na qualidade ao longo do tempo.

Endereçar microsserviços

Os serviços e as versões de código podem ser endereçados diretamente. Dessa forma, é possível implantar novas versões de código lado a lado com versões de código existentes, além de testar o novo código antes de torná-lo a versão de disponibilização padrão.

Cada projeto do App Engine tem um serviço padrão, e cada serviço tem uma versão de código padrão. Para endereçar o serviço padrão da versão padrão de um projeto com o ID de aplicativo my-app, você usaria o seguinte URL:

http://my-app.appspot.com

Se implantar um serviço chamado user-service, você poderá acessar a versão de disponibilização padrão desse serviço usando o seguinte URL:

http://user-service.my-app.appspot.com

Se implantar uma segunda versão de código não padrão chamada banana no serviço user-service, você poderá acessar diretamente essa versão do código usando o seguinte URL:

http://banana.user-service.my-app.appspot.com

Se implantar uma segunda versão de código não padrão chamada cherry no serviço default, você poderá acessar essa versão do código usando o seguinte URL:

http://cherry.my-app.appspot.com

O App Engine aplica a regra de que os nomes das versões de código no serviço padrão não podem ser iguais aos nomes dos serviços.

O endereçamento direto de versões de código específicas só deve ser usado em testes de fumaça e para facilitar os testes A/B, operações roll-forward e rollback. Em vez disso, o código de cliente precisa endereçar apenas a versão de disponibilização padrão do serviço padrão ou de um serviço específico:

http://my-app.appspot.com
http://user-service.my-app.appspot.com

Esse estilo de endereçamento permite que os microsserviços implementem novas versões dos serviços deles, inclusive correções de bugs, sem exigir alterações nos clientes.

Existem equivalentes de HTTPS de todos os nomes de host acima. O certificado integrado do App Engine em appspot.com não aceita nomes de host profundos de serviços e versões. Por isso, os nomes precisam ser simplificados usando -dot-. Por exemplo, os seguintes nomes de host são os equivalentes dos exemplos anteriores, mas modificados para HTTPS:

https://my-app.appspot.com
https://user-service-dot-my-app.appspot.com
https://banana-dot-user-service-dot-my-app.appspot.com  # whew, that's a mouthful
https://cherry-dot-my-app.appspot.com

Usar versões da API

Toda API de microsserviço precisa ter uma versão principal da API no URL, como:

/user-service/v1/

Essa versão principal da API identifica claramente nos registros qual versão de API do microsserviço está sendo chamada. Mais importante ainda, a versão principal da API produz URLs diferentes. Dessa forma, as novas versões principais da API podem ser disponibilizadas lado a lado com as versões principais anteriores da API:

/user-service/v1/
/user-service/v2/

Não é necessário incluir a versão secundária da API no URL porque as versões secundárias da API, por definição, não introduzirão alterações importantes. De fato, incluir a versão secundária da API no URL resultaria em uma proliferação de URLs e causaria incerteza sobre a capacidade de um cliente de se mover para uma nova versão secundária da API.

Observe que, neste artigo, consideramos um ambiente de entrega e integração contínuas em que o branch principal está sempre sendo implantado no App Engine. Existem dois conceitos distintos de versão neste artigo:

  • Versão do código, associada diretamente a uma versão de serviço do App Engine e que representa uma determinada tag de efetivação do branch principal.

  • Versão da API, associada diretamente a um URL da API e que representa a forma dos argumentos de solicitação, a forma do documento de resposta e o comportamento da API.

Neste artigo, também pressupomos que uma implantação de código único implementará as versões anteriores e novas de uma API em uma versão de código comum. Por exemplo, o branch principal implantado pode implementar /user-service/v1/ e /user-service/v2/. Ao implementar novas versões secundárias e de patch, essa abordagem permite dividir o tráfego entre duas versões de código, independentemente das versões da API que o código efetivamente implementa.

A organização pode optar por desenvolver /user-service/v1/ e /user-service/v2/ em branches de código diferentes, ou seja, nenhuma implantação de código as implementará ao mesmo tempo. Esse modelo também é possível no App Engine. No entanto, para dividir o tráfego, você precisaria mover a versão principal da API para o próprio nome de serviço. Por exemplo, os clientes usariam os seguintes URLs:

http://user-service-v1.my-app.appspot.com/user-service/v1/
http://user-service-v2.my-app.appspot.com/user-service/v2/

A versão principal da API migra para o próprio nome de serviço, como user-service-v1 e user-service-v2. As partes /v1/ e /v2/ do caminho são redundantes nesse modelo e podem ser removidas, mas ainda podem ser úteis na análise do registro. Esse modelo requer um pouco mais de trabalho porque provavelmente exige atualizações nos scripts de implantação para implantar novos serviços nas alterações da versão principal da API. Além disso, esteja ciente do número máximo de serviços permitidos por aplicativo do App Engine.

Alterações importantes versus não importantes

É necessário compreender a diferença entre uma alteração importante e uma não importante. As alterações importantes normalmente são subtrativas, o que significa que elas removem parte do documento de solicitação ou resposta. Alterar a forma do documento ou o nome das chaves pode causar uma alteração importante. Novos argumentos obrigatórios são sempre alterações importantes. Essas alterações também poderão ocorrer se o comportamento do microsserviço mudar.

As alterações não importantes tendem a ser aditivas. Um novo argumento de solicitação opcional ou uma nova seção no documento de resposta são alterações não importantes. Para conseguir alterações não importantes, a escolha da serialização em trânsito é essencial. Muitas serializações são boas para alterações não importantes: JSON, buffers de protocolo ou Thrift. Quando desserializadas, essas serializações ignoram silenciosamente informações extras e inesperadas. Em linguagens dinâmicas, as informações extras simplesmente são exibidas no objeto desserializado.

Pense na seguinte definição JSON para o serviço /user-service/v1/:

{
  "userId": "UID-123",
  "firstName": "Jake",
  "lastName": "Cole",
  "username": "jcole@example.com"
}

A seguinte alteração importante exigiria uma nova versão do serviço como /user-service/v2/:

{
  "userId": "UID-123",
  "name": "Jake Cole",  # combined fields
  "email": "jcole@example.com"  # key change
}

No entanto, a seguinte alteração não importante não requer uma nova versão:

{
  "userId": "UID-123",
  "firstName": "Jake",
  "lastName": "Cole",
  "username": "jcole@example.com",
  "company": "Acme Corp."  # new key
}

Implantar novas versões secundárias e não importantes da API

Ao implantar uma nova versão secundária da API, o App Engine possibilita o lançamento da nova versão de código lado a lado com a versão de código anterior. No App Engine, é possível endereçar diretamente qualquer uma das versões implantadas, mas apenas uma versão é a de disponibilização padrão. Lembre-se de que existe uma versão de disponibilização padrão para cada serviço. Neste exemplo, temos a versão de código anterior, chamada de apple, que é a versão de disponibilização padrão. Além disso, implantamos a nova versão de código como uma versão lado a lado chamada de banana. Os URLs do microsserviço de ambas são iguais, /user-service/v1/, porque estamos implantando uma alteração não importante e secundária na API.

O App Engine fornece mecanismos para migrar automaticamente o tráfego de apple para banana, marcando a nova versão de código, banana, como a versão de disponibilização padrão. Quando a nova versão de disponibilização padrão for definida, nenhuma nova solicitação será direcionada para apple, e todas as novas solicitações serão direcionadas para banana. É assim que você faz uma operação roll-forward para uma nova versão de código que implementa uma nova versão secundária ou de patch da API sem afetar os microsserviços do cliente.

No caso de um erro, pode ser feito o rollback invertendo o processo acima: redefina a versão de disponibilização padrão como a anterior, apple, no exemplo. Todas as novas solicitações serão redirecionadas para a versão do código anterior, e nenhuma solicitação nova será direcionada para banana. As solicitações em andamento podem ser concluídas.

O App Engine também permite direcionar apenas uma determinada porcentagem do tráfego para a nova versão do código. Esse processo normalmente é chamado de versão canário e o mecanismo é chamado de divisão de tráfego no App Engine. É possível direcionar 1%, 10%, 50% ou qualquer porcentagem do tráfego que você quiser para as novas versões de código e ajustar esse valor ao longo do tempo. Por exemplo, é possível implementar a nova versão de código em 15 minutos, aumentando lentamente o tráfego e observando eventuais problemas que possam identificar quando um rollback é necessário. Esse mesmo mecanismo permite fazer um teste A/B em duas versões do código: defina a divisão de tráfego para 50% e compare as características de desempenho e taxa de erros das duas versões de código para confirmar melhorias esperadas.

A seguinte imagem mostra configurações da divisão de tráfego no Console do GCP:

Configurações da divisão de tráfego no Console do Google Cloud Platform

Implantar novas versões importantes da API

Quando você implanta versões principais e importantes da API, o processo de roll-forward e rollback é o mesmo para versões secundárias e não importantes. No entanto, normalmente não há nenhuma divisão de tráfego nem testes A/B, porque a versão importante da API é um URL recém-lançado, como /user-service/v2/. Obviamente, se tiver alterado a implementação subjacente da versão principal anterior da API, será necessário usar a divisão de tráfego para testar se a versão principal anterior da API continua funcionando conforme o esperado.

Durante a implantação de uma nova versão principal da API, é importante lembrar que as anteriores também podem estar sendo disponibilizadas. Por exemplo, /user-service/v1/ ainda pode estar sendo disponibilizada quando /user-service/v2/ for lançada. Esse fato é uma parte essencial das versões independentes de código. Você só poderá desativar versões principais e anteriores da API depois que tiver verificado se nenhum outro microsserviço precisa delas, inclusive outros microsserviços que talvez precisem ser revertidos para uma versão de código anterior.

Como um exemplo concreto, imagine que você tenha um microsserviço chamado web-app que dependa de outro chamado user-service. Imagine que user-service precise alterar alguma implementação subjacente que impossibilitará a compatibilidade com a versão principal e anterior da API que web-app está usando atualmente, como recolher firstName e lastName em um único campo chamado name. Ou seja, user-service precisa desativar uma versão principal e anterior da API.

Para conseguir realizar essa alteração, três implantações separadas precisam ser feitas:

  • Primeiro, user-service precisa implantar /user-service/v2/, além de ser compatível com /user-service/v1/. Essa implantação pode exigir que o código temporário seja escrito para aceitar a compatibilidade com versões anteriores, que é uma consequência comum em aplicativos baseados em microsserviços.

  • Em seguida, web-app precisa implantar um código atualizado que altere a dependência dele de /user-service/v1/ para /user-service/v2/.

  • Por fim, depois que a equipe de user-service tiver verificado se web-app não exige mais /user-service/v1/ e se web-app não precisa de rollback, a equipe poderá implantar o código que remove o ponto de extremidade anterior /user-service/v1/ e qualquer código temporário necessário para aceitá-lo.

Toda essa atividade pode parecer onerosa, mas é um processo essencial em aplicativos baseados em microsserviços, e é precisamente o processo que permite ciclos independentes de lançamento de desenvolvimento. Esse processo parece ser bastante dependente, mas o importante é que cada etapa acima possa ocorrer em linhas do tempo independentes e as operações de roll-forward e rollback ocorram dentro do escopo de um único microsserviço. A ordem das etapas é fixa, mas elas podem ocorrer durante muitas horas, dias ou até mesmo semanas.

A seguir

Esta página foi útil? Conte sua opinião sobre:

Enviar comentários sobre…

Ambiente padrão do App Engine para Java 8