Contratos, endereços e APIs para microsserviços

ID da região

O REGION_ID é um código que o Google atribui com base na região selecionada ao criar o aplicativo. A inclusão de REGION_ID.r nos URLs do App Engine é opcional para aplicativos atuais e em breve será obrigatória para todos os aplicativos novos.

Para garantir uma transição tranquila, estamos atualizando lentamente o App Engine para usar IDs da região. Se ainda não tivermos atualizado seu projeto do Google Cloud, você não verá um ID da região para o aplicativo. Como o ID é opcional para os aplicativos atuais, não é necessário atualizar os URLs ou fazer outras alterações quando o ID da região está disponível para os aplicativos já existentes.

Saiba mais sobre IDs da 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, sendo aplicáveis os princípios de design da API descritos aqui. É 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:

<code>https://user-service-dot-my-app.<var><a href="#appengine-urls"
  style="border-bottom: 1px dotted #999" class="devsite-dialog-button"
  data-modal-dialog-id="regional_url" track-type="progressiveHelp"
  track-name="modalHelp"
  track-metadata-goal="regionalURL">REGION_ID</a></var>.r.appspot.com</code>

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

<code>https://banana-dot-user-service-dot-my-app.<var>REGION_ID</a></var>.r.appspot.com</code>

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:

<code>https://cherry-dot-my-app.<var>REGION_ID</a></var>.r.appspot.com</code>

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 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 implementem novas versões dos serviços deles, inclusive correções de bugs, sem exigir alterações nos clientes.

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 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, sua ramificação mestre 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 pode escolher desenvolver /user-service/v1/ e /user-service/v2/ em diferentes ramificações de código. Ou seja, 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, 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.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 podem ser removidas, 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 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. Este exemplo apresenta nossa versão de código antiga, que é a versão de exibição padrão e é denominada apple, e a implantação da nova versão de código como uma versão lado a lado, denominada banana. Observe que os URLs de microsserviço das duas são os mesmos /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 e todas as novas solicitações serão roteadas 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.

Caso ocorra um erro, a reversão é possível ao desfazer o processo acima: alterne a versão de exibição padrão para a antiga, apple em 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 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 rollforward e rollback 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 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, a versão /user-service/v1/ ainda pode estar sendo exibida quando a versão /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 exemplo concreto, imagine que você tenha um microsserviço, denominado web-app, que dependa de outro microsserviço, denominado user-service. Suponha que user-service precise alterar alguma implementação subjacente que tornará impossível a compatibilidade com a versão principal antiga da API que web-app está usando atualmente, como recolher firstName e lastName em um único campo denominado name. Ou seja, user-service precisa desativar uma antiga versão principal da API.

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

  • Primeiro, user-service precisa implantar a versão /user-service/v2/ enquanto ainda oferece suporte à versão /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 altera a dependência da versão /user-service/v1/ para a versão /user-service/v2/

  • Por fim, depois que a equipe user-service verifica que web-app não exige mais a versão /user-service/v1/ e que o web-app não precisa reverter, a equipe pode implantar o código que remove o endpoint /user-service/v1/ antigo e qualquer código temporário necessário para suportá-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 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