Contratos, direccionamiento y API para microservicios

En App Engine, los microservicios suelen llamarse entre sí mediante API de RESTful basadas en HTTP. También es posible invocar microservicios en segundo plano con la Lista de tareas en cola, y se aplican los principios de diseño de API que se describen aquí. Es importante seguir ciertos patrones para garantizar que la aplicación basada en microservicios sea estable y segura, y tenga un buen rendimiento.

Utilización de contratos sólidos

Uno de los aspectos más importantes de las aplicaciones basadas en microservicios es la capacidad de implementar microservicios que sean completamente independientes entre sí. Para lograr esa independencia, cada microservicio debe proporcionar un contrato con versiones bien definido a sus clientes, que son otros microservicios. Ningún servicio debe poner fin a estos contratos con versiones hasta tanto no se sepa que no hay microservicios basados en cierto contrato con versiones. Recuerda que puede ser necesario revertir otros microservicios a una versión anterior del código que requiera un contrato previo; por eso, es importante tener en cuenta esa posibilidad en tus políticas de baja.

Lograr una cultura en torno a contratos con versiones sólidos es probablemente el aspecto organizativo más desafiante de una aplicación estable basada en microservicios. Los equipos de desarrollo deben comprender qué implica un cambio rotundo frente a uno no rotundo. Tienen que saber cuándo se requiere una actualización general. Deben comprender cómo y cuándo se puede poner fin a un contrato antiguo. Los equipos deben implementar técnicas apropiadas de comunicación, que incluyan avisos de baja, para garantizar que se conozcan los cambios en los contratos de microservicios. Si bien lo anterior puede sonar abrumador, incorporar estas prácticas en tu cultura de desarrollo producirá grandes mejoras en cuanto a velocidad y calidad a lo largo del tiempo.

Direccionamiento a microservicios

En el trabajo con distintas versiones de código y servicios se admite el direccionamiento específico a cada una de ellas. Como resultado, puedes implementar versiones de código nuevas a la par de otras existentes y probar código nuevo antes de convertirlo en la versión predeterminada en uso.

Cada proyecto de App Engine tiene un servicio predeterminado, y cada servicio tiene una versión de código predeterminada. Para dirigirte al servicio predeterminado de la versión predeterminada de un proyecto con el ID de la aplicación my-app, utiliza la siguiente URL:

http://my-app.appspot.com

Si implementas un servicio llamado user-service, puedes acceder a su versión predeterminada en uso con la siguiente URL:

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

Si implementas una segunda versión de código no predeterminada con el nombre banana en el servicio user-service, puedes acceder directamente a ella mediante la siguiente URL:

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

Ten en cuenta que si implementas una segunda versión de código no predeterminada con el nombre cherry en el servicio default, puedes acceder a ella mediante la siguiente URL:

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

App Engine aplica la regla según la cual los nombres de las versiones de código no pueden entrar en conflicto con los nombres de los servicios en el servicio predeterminado.

El direccionamiento a versiones de código específicas solo debería utilizarse en pruebas de humo y para facilitar las pruebas A/B, la puesta al día y la reversión. El código de tu cliente solo debería dirigirse a la versión predeterminada en uso del servicio predeterminado o de uno específico:

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

Este estilo de direccionamiento permite que los microservicios implementen versiones nuevas de sus servicios, que pueden incluir la corrección de errores, sin necesidad de incorporar cambios en los clientes.

Hay equivalentes HTTPS de todos los nombres de host mencionados arriba. El certificado de App Engine incorporado que se encuentra en appspot.com no es compatible con los nombres de host profundos para los servicios y las versiones, por lo que es necesario transformar esos nombres en planos con -dot-. Por ejemplo, los nombres de host siguientes equivalen a los de los ejemplos anteriores, pero se modificaron 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

Cómo usar versiones de API

Cada API de microservicio debería tener una versión principal de API en la URL, como se ilustra a continuación:

/user-service/v1/

Esta versión principal de API identifica con claridad en los registros qué versión de API del microservicio se está llamando. Más importante aún, la versión principal de API genera diferentes URL, de modo que se pueden entregar las nuevas versiones principales de API junto con las antiguas:

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

No es necesario incluir la versión secundaria de API en la URL, ya que por definición esas versiones no producen cambios rotundos. De hecho, hacerlo daría lugar a una proliferación de URL y causaría incertidumbre sobre la capacidad de un cliente para pasar a una nueva versión secundaria de API.

Ten en cuenta que en este artículo se presupone que se trabaja con un entorno de integración y entrega continuas, donde la rama principal se implementa constantemente en App Engine. En este artículo se usan dos conceptos distintos de versión:

  • Versión de código, que guarda correspondencia directa con una versión de servicio de App Engine y representa una etiqueta de confirmación particular de la rama principal.

  • Versión de API, que guarda correspondencia directa con una URL de API y representa la forma de los argumentos de solicitud, la forma del documento de respuesta y el comportamiento de la API.

En este artículo también se presupone que con una única implementación de código se implementarán las versiones de API viejas y nuevas de una API en una versión de código común. Por ejemplo, tu rama principal implementada podría implementar /user-service/v1/ y /user-service/v2/. Cuando se implementan nuevas versiones secundarias y de parche, este enfoque te permite dividir el tráfico entre dos versiones de código más allá de las versiones de API que el código implemente en realidad.

Tu organización puede elegir desarrollar tu /user-service/v1/ y /user-service/v2/ en ramas de código diferentes; es decir, ninguna implementación de código implementará ambas al mismo tiempo. Este modelo también es viable en App Engine, pero para dividir el tráfico tendrías que trasladar la versión de API principal al nombre del servicio en sí. Por ejemplo, tus clientes usarían las URL siguientes:

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

La versión de API principal se traslada al nombre del servicio en sí, como user-service-v1 y user-service-v2. (Las partes /v1/ y /v2/ de la ruta son redundantes en este modelo y podrían quitarse, aunque posiblemente sean útiles en el análisis de registros). Este modelo requiere un poco más de trabajo porque es probable que exija hacer actualizaciones en las secuencias de comandos de tus implementaciones para implementar servicios nuevos en cambios de versiones principales de API. Además, ten en cuenta el número máximo de servicios permitidos por aplicación de App Engine.

Cambios rotundos frente a no rotundos

Es importante entender la diferencia entre los cambios rotundos y los no rotundos. Los primeros suelen ser sustractivos, es decir que eliminan parte del documento de respuesta o de la solicitud. El cambio de forma del documento o del nombre de las claves puede producir un cambio rotundo. Los argumentos obligatorios nuevos siempre son cambios rotundos. También pueden producirse este tipo de cambios si se modifica el comportamiento del microservicio.

Los cambios no rotundos suelen ser aditivos. Un argumento opcional nuevo en una solicitud o una sección adicional nueva en el documento de respuesta son cambios no rotundos. Para lograr este tipo de cambios, la elección de la serialización en línea es esencial. Muchas serializaciones, tales como las de JSON, Protocol Buffers o Thrift, son compatibles con los cambios no rotundos. Cuando se deserializan, estas ignoran de manera silenciosa la información adicional inesperada. En los lenguajes dinámicos, la información adicional simplemente aparece en el objeto deserializado.

Considera la siguiente definición en JSON para el servicio /user-service/v1/:

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

El siguiente cambio rotundo requeriría reemplazar la versión del servicio por /user-service/v2/:

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

Sin embargo, el cambio no rotundo que aparece a continuación no requiere una versión nueva:

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

Cómo implementar nuevas versiones secundarias de API con cambios no rotundos

Cuando se implementa una nueva versión secundaria de API, App Engine permite que la nueva versión de código se distribuya junto con la antigua. Aunque en esta plataforma puedes dirigirte específicamente a cualquiera de las versiones implementadas, solo una es la versión predeterminada en uso; recuerda que hay una versión predeterminada en uso para cada servicio. En este ejemplo, hay una versión de código antigua, llamada apple, que es la versión predeterminada en uso, y luego implementamos una nueva versión de código en paralelo, llamada banana. Ten en cuenta que las URL de los microservicios son las mismas para ambas, /user-service/v1/, dado que estamos implementando un cambio de API secundaria no rotundo.

App Engine proporciona mecanismos para migrar automáticamente el tráfico de apple a banana marcando la versión de código nueva, banana, como versión predeterminada en uso. Una vez que se configura la nueva versión predeterminada en uso, todas las solicitudes nuevas se enrutarán a banana en lugar de a apple. Así es como se avanza a una nueva versión de código que implementa una nueva versión de API secundaria o de parche sin causar ningún impacto en los microservicios del cliente.

Si se produce un error, es posible revertir el proceso. Para ello, configura la versión antigua, apple en nuestro ejemplo, como versión predeterminada en uso. Se enrutarán las solicitudes nuevas a la versión de código antigua en lugar de enrutarse a banana. Ten en cuenta que se permite que se completen las solicitudes en curso.

App Engine también brinda la capacidad de dirigir solo un porcentaje del tráfico a tu versión de código nueva. Este proceso se conoce como lanzamiento canary, y el mecanismo se denomina división del tráfico en App Engine. Puedes dirigir el 1%, el 10%, el 50% o cualquier porcentaje del tráfico que desees a tus versiones de código nuevas, y ajustar ese valor a lo largo del tiempo. Por ejemplo, podrías implementar tu nueva versión de código a lo largo de 15 minutos, incrementar el tráfico de a poco y prestar atención a situaciones que pudieran indicar la necesidad de ejecutar una reversión. Este mecanismo también te permite hacer pruebas A/B entre dos versiones de código: divide el tráfico al 50% y compara las características de tasa de error y rendimiento de ambas versiones de código para confirmar las mejoras esperadas.

La imagen que sigue muestra la configuración de la división del tráfico en GCP Console:

Configuración de la división del tráfico en Google Cloud Platform Console

Cómo implementar nuevas versiones principales de API con cambios rotundos

Cuando implementas versiones principales de API con cambios rotundos, el proceso de puesta al día y reversión es el mismo que en el caso de versiones secundarias de API con cambios no rotundos. Sin embargo, por lo general no tendrás que dividir el tráfico ni hacer pruebas A/B porque la versión de API con cambios rotundos es una URL de lanzamiento reciente, como /user-service/v2/. Por supuesto, si cambiaste la implementación subyacente de tu antigua versión principal de API, tal vez quieras recurrir a la división del tráfico de todos modos para probar que esa versión aún funcione según lo esperado.

Cuando implementes una nueva versión principal de API, debes recordar que tal vez las antiguas sigan entregando solicitudes. Por ejemplo, podría suceder que /user-service/v1/ siga entregando solicitudes cuando se lance /user-service/v2/. Este es un elemento esencial del trabajo con versiones de código independientes. Solo deberías poder dar de baja versiones principales de API antiguas tras verificar que ningún otro microservicio las necesite, incluidos los que puedan requerir que se los revierta a una versión de código anterior.

Como ejemplo concreto, imagina que tienes un microservicio, llamado web-app, que depende de otro, llamado user-service. Imagina que user-service necesita cambiar alguna implementación subyacente que volverá imposible la compatibilidad con la antigua versión principal de API que web-app usa en la actualidad, incluida la posibilidad de contraer firstName y lastName en un mismo campo llamado name. Es decir, user-service necesita dar de baja una antigua versión principal de API.

Para realizar ese cambio, se deben hacer tres implementaciones por separado:

  • Primero, user-service debe implementar /user-service/v2/ manteniendo la compatibilidad con /user-service/v1/. Esta implementación tal vez requiera la programación de código temporal para permitir la compatibilidad con versiones anteriores, una consecuencia común en las aplicaciones basadas en microservicios

  • Luego, web-app debe implementar código actualizado que reemplace su dependencia de /user-service/v1/ por la correspondiente a /user-service/v2/

  • Por último, luego de que el equipo de user-service verifique que web-app ya no requiere /user-service/v1/ y que web-app no necesita reversiones, el equipo puede implementar código que quite el extremo /user-service/v1/ antiguo y cualquier código temporal que fuera necesario para ofrecerle compatibilidad.

Si bien toda esta actividad puede parecer ardua, es un proceso esencial para las aplicaciones basadas en microservicios que permite trabajar con ciclos de versiones de desarrollo independientes. Este proceso parece implicar un alto grado de dependencia, pero es importante destacar que cada paso puede llevarse a cabo según cronogramas independientes, y la puesta al día y la reversión se producen dentro del alcance de un microservicio. Solo es fijo el orden de los pasos, que pueden efectuarse a lo largo de muchas horas, días o incluso semanas.

Próximos pasos

¿Te ha resultado útil esta página? Enviar comentarios:

Enviar comentarios sobre...

Entorno estándar de App Engine para Go