Contratos, endereços e APIs para microsserviços

ID da região

O REGION_ID é um código abreviado que o Google atribui com base na região que você selecionou ao criar o aplicativo. O código não corresponde a um país ou estado, ainda que alguns IDs de região sejam semelhantes aos códigos de país e estado geralmente usados. Para apps criados após fevereiro de 2020, o REGION_ID.r está incluído nos URLs do App Engine. Para apps existentes criados antes dessa data, o ID da região é opcional no URL.

Saiba mais sobre IDs de região.

Os microsserviços no App Engine normalmente chamam uns aos outros usando APIs RESTful baseadas em HTTP. Também é possível invocar microsserviços em segundo plano usando filas de tarefas, Nesse procedimento, os princípios de design da API descritos aqui são aplicáveis. É 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 interruptiva e uma não interruptiva. 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, use o seguinte URL:
https://PROJECT_ID.REGION_ID.r.appspot.com

Se você implantar um serviço denominado user-service, será possível acessar a versão de exibição padrão desse serviço usando o seguinte URL:

https://user-service-dot-my-app.REGION_ID.r.appspot.com

Se você implantar uma segunda versão de código não padrão denominada banana no serviço user-service, será possível acessar diretamente essa versão de código usando o seguinte URL:

https://banana-dot-user-service-dot-my-app.REGION_ID.r.appspot.com

Observe que, se você implantar uma segunda versão de código não padrão denominada cherry no serviço default, será possível acessar essa versão de código usando o seguinte URL:

https://cherry-dot-my-app.REGION_ID.r.appspot.com

O App Engine aplica a regra de que não permite que os nomes das versões de código no serviço padrão sejam 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 ou operações de rollback e rollforward. 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:


https://PROJECT_ID.REGION_ID.r.appspot.com

https://SERVICE_ID-dot-PROJECT_ID.REGION_ID.r.appspot.com

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

Como 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 interruptivas. 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 a ramificação principal está sempre sendo implantada 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 confirmação da ramificação 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, sua ramificação principal implantada 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.

Sua organização tem a opção de escolher desenvolver /user-service/v1/ e /user-service/v2/ em diferentes ramificações de código, isto é, nenhuma implantação de código implementará ambos ao mesmo tempo. Esse modelo também é possível no App Engine. No entanto, para dividir o tráfego, seria necessário mover a versão principal da API para o próprio nome do serviço. Por exemplo, os clientes usariam os seguintes URLs:

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

A versão principal da API é movida para o nome do serviço, como user-service-v1 e user-service-v2. (As partes /v1/ e /v2/ do caminho são redundantes nesse modelo e é possível removê-las, embora ainda possam ser úteis na análise de registros). 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 interruptivas versus não interruptivas

É necessário compreender a diferença entre uma alteração interruptiva e uma não interruptiva. As alterações interruptivas 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 interruptiva. Novos argumentos obrigatórios são sempre alterações interruptivas. Essas alterações também poderão ocorrer se o comportamento do microsserviço mudar.

As alterações não interruptivas 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 interruptivas. Para conseguir alterações não interruptivas, a escolha da serialização em trânsito é essencial. Muitas serializações são compatíveis com alterações não interruptivas: JSON, buffers de protocolo ou Thrift. Quando são 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.

Considere a seguinte definição de JSON para o serviço /user-service/v1/:

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

A seguinte alteração interruptiva exigiria um novo controle de versões 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 interruptiva 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 interruptivas 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 delas será a versão de exibição padrão. Lembre-se de que existe uma versão de exibição padrão para cada serviço. Neste exemplo, você verá nossa versão de código antiga, que é a versão de exibição padrão e é denominada apple, e será implantada a nova versão do código como uma versão lado a lado, denominada banana. Observe que os URLs de microsserviço das duas são o mesmo /user-service/v1/, já que estamos implantando uma alteração não interruptiva e secundária na API.

O App Engine oferece mecanismos para migrar automaticamente o tráfego de apple para banana ao marcar a nova versão de código banana como a versão de exibição padrão. Quando a nova versão de exibição padrão for definida, nenhuma nova solicitação será roteada para apple. Todas as novas solicitações serão roteadas para banana. É assim que se efetua o roll-forward de 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.

Caso ocorra um erro, a reversão é possível ao desfazer o processo acima: defina a versão de exibição padrão de volta para a antiga, apple no nosso exemplo. Todas as novas solicitações serão roteadas de volta para a versão de código anterior e nenhuma nova solicitação será roteada 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 imagem a seguir mostra as configurações de divisão de tráfego no console do Google Cloud:

Configurações de divisão de tráfego no console do Google Cloud

Implantar novas versões interruptivas da API

Quando você implanta versões principais e interruptivas da API, os processos de roll-forward e reversão são os mesmos de versões secundárias e não interruptivas. No entanto, você normalmente não realizará nenhuma divisão de tráfego ou testes A/B, porque a versão interruptiva da API é um URL recém-lançado, como /user-service/v2/. Obviamente, se você 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 talvez ainda estejam sendo disponibilizadas. Por exemplo, talvez a versão /user-service/v1/ ainda esteja sendo exibida quando a /user-service/v2/ for lançada. Esse fato é uma parte essencial das versões de código independentes. Você só poderá desativar versões principais anteriores da API depois que tiver verificado que elas não são necessárias para nenhum outro microsserviço, inclusive outros microsserviços que talvez precisem ser revertidos para uma versão anterior do código.

Como exemplo, imagine que você tem um microsserviço denominado web-app que depende de outro microsserviço denominado user-service. Suponha que user-service precisa alterar alguma implementação subjacente que tornará impossível a compatibilidade com a antiga versão principal da API que web-app está usando no momento, como recolher firstName e lastName em um único campo denominado name. Ou seja, é necessário que user-service desative uma antiga versão principal da API.

Para conseguir realizar essa alteração, é necessário fazer três implantações separadas:

  • Primeiro, é necessário que user-service implante a versão /user-service/v2/ enquanto ainda for compatível com a versão /user-service/v1/. Essa implantação requer a gravação de código temporário para ter compatibilidade com versões anteriores, o que é uma consequência comum em aplicativos baseados em microsserviços.

  • Em seguida, é necessário que web-app implante um código atualizado que altere sua dependência da versão /user-service/v1/ para a /user-service/v2/.

  • Por fim, depois que a equipe de user-service tiver verificado que o web-app não exige mais a versão /user-service/v1/ e que não é necessária a reversão do web-app, a equipe implantará o código que remove o antigo endpoint da /user-service/v1/ e qualquer código temporário necessário à compatibilidade.

Embora tudo isso pareça oneroso, é um processo essencial em aplicativos baseados em microsserviços, e é exatamente esse processo que permite ciclos independentes de versão 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 rollforward 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