¿Qué desacoplar y cuándo?
A medida que los sistemas monolíticos se vuelven demasiado grandes para lidiar con ellos, muchas empresas están dispuestas a dividirlos en el estilo arquitectónico de los microservicios, como se hizo en su día en el desarrollo con OOP, ahora le ha llegado el momento a los sistemas. Es un viaje que vale la pena, pero no es fácil. Hemos aprendido que para hacer esto bien, debemos comenzar con un servicio simple, pero luego extraer servicios basados en capacidades verticales que sean importantes para el negocio y estén sujetas a cambios frecuentes. Estos servicios deben ser grandes al principio y, de preferencia, no dependen del monolito restante. Debemos asegurarnos de que cada paso de la migración representa una mejora atómica de la arquitectura general que además de justificar los costes nos permite motivarnos más.
La migración de un sistema monolítico a un ecosistema de microservicios es un viaje épico. Los que se embarcan en este viaje tienen aspiraciones como aumentar la escala de operación, acelerar el ritmo del cambio y escapar del alto costo del cambio. Quieren aumentar su número de equipos al tiempo que les permite entregar valor en paralelo e independientemente uno del otro. Quieren experimentar rápidamente con las capacidades centrales de su negocio y entregar valor más rápido. También quieren escapar del alto costo asociado con hacer cambios en sus sistemas monolíticos existentes.
Para trabajar bien no hace falta cambiar de arquitectura.. quizás sus mismos servicios si fueran ‘SOLID oriented’ cumplían todas sus necesidades. Vale la pena estudiar el escenario en que aplica cada paradigma.
Decidir qué capacidad de desacoplar cuándo y cómo migrar de manera incremental son algunos de los desafíos arquitectónicos de descomponer un monolito a un ecosistema de microservicios. En este artículo, compartimos algunas técnicas que pueden guiar a los equipos de entrega (desarrolladores, arquitectos, gerentes técnicos) para tomar estas decisiones de descomposición a lo largo del viaje.
El ecosistema del microservicio
Antes de embarcarse, es fundamental que todos tengan un entendimiento común de un ecosistema de microservicios. El ecosistema de microservicios es una plataforma de servicios que encapsula una capacidad empresarial. Una capacidad comercial representa lo que hace una empresa en un dominio particular para cumplir sus objetivos y responsabilidades. Cada microservicio expone una API que los desarrolladores pueden descubrir y utilizar de forma automática. Los microservicios tienen un ciclo de vida independiente. Los desarrolladores pueden construir, probar y lanzar cada microservicio de forma independiente. El ecosistema de microservicios impone una estructura organizativa de equipos autónomos de larga vida, cada uno es responsable de uno o varios servicios. Contrariamente a la percepción general y al ‘micro’ en microservicios, el tamaño de cada servicio es lo más importante y puede variar según la madurez operativa de la organización.
Roadmap
Antes de sumergirse en la guía, es importante saber que hay un alto costo general asociado con la descomposición de un sistema existente para microservicios y puede tomar muchas iteraciones. Es necesario que los desarrolladores y arquitectos evalúen detenidamente si la descomposición de un monolito existente es el camino correcto y si los microservicios en sí son el destino correcto . Habiendo aclarado eso, vamos a repasar la guía.
Calentando motores con una capacidad simple y bastante desacoplada
Iniciar una ruta hacia los microservicios requiere un nivel mínimo de preparación operativa. Requiere acceso al entorno de implementación, la creación de nuevos canales, integración continua para construir, probar e implementar de forma independiente los servicios ejecutables, y la capacidad de proteger, depurar y monitorear una arquitectura distribuida. Se requiere madurez de preparación operacional y equipos bien formados ya sea para un nuevo proyecto o descomponiendo un sistema existente.
Nuestra sugerencia es que los desarrolladores y los equipos de operación construyan la infraestructura subyacente, los procesos de integración continua y el sistema de administración de API con el primer y segundo servicio que descomponen o construyen. Comience con las capacidades que están bastante desacopladas del monolito, no requieren cambios en muchas aplicaciones de cara al cliente que actualmente usan el monolito y posiblemente no necesitan un almacén de datos. Lo que los equipos están optimizando es validar sus enfoques de entrega, mejorar la capacidad de los miembros del equipo y desarrollar la infraestructura mínima necesaria para entregar servicios seguros de implementación independiente que exponen las API de autoservicio.
Primero recomendamos desacoplar los servicios simples. A continuación, adoptamos un enfoque diferente: capacidades de desacoplamiento profundamente integradas en el sistema monolítico. Aconsejamos realizar servicios de vanguardia primero porque al comienzo del viaje, el mayor riesgo de los equipos de entrega es no operar los microservicios correctamente. Por lo tanto, es bueno usar los servicios perimetrales para practicar los requisitos previos operativos que necesitan. Una vez que han abordado eso, pueden abordar el problema clave de dividir el monolito.
Minimiza la dependencia de regreso al monolito
Como principio fundamental, los equipos de entrega necesitan minimizar las dependencias de los microservicios recién formados para el monolito. Un beneficio importante de los microservicios es tener un ciclo de lanzamiento rápido e independiente. Tener dependencias en el monolito (datos, lógica, API) acopla el servicio al ciclo de lanzamiento del monolito, lo que prohíbe este beneficio. A menudo, la principal motivación para alejarse del monolito es el alto costo y el lento ritmo de cambio de las capacidades bloqueadas en él, por lo que queremos avanzar progresivamente en una dirección que desacople estas capacidades básicas al eliminar las dependencias del monolito. Si los equipos siguen esta guía a medida que desarrollan capacidades en sus propios servicios, lo que encuentran es, en cambio, dependencias en la dirección inversa. Del monolito a los servicios. Esta es una dirección de dependencia deseada, ya que no ralentiza el ritmo de cambio para los nuevos servicios.
Las siguientes pautas ofrecen otras formas de decidir el orden en el que los desarrolladores desacoplan los servicios. Esto significa que es posible que no siempre puedan evitar las dependencias al monolito. En los casos en que un nuevo servicio termina con una llamada al monolito, sugiero exponer una nueva API del monolito. Es mejor crear una capa en el nuevo servicio para asegurarse de que los conceptos de monolito no se filtren. Esfuércese por definir la API que refleje los conceptos y estructuras de dominio bien definidos, aunque la implementación interna del monolito podría ser diferente. En este desafortunado caso, los equipos de entrega sufragarán el costo y la dificultad de cambiar el monolito, probar y liberar los nuevos servicios junto con el lanzamiento del monolito.
Separar las funcionalidades acopladas
Estoy asumiendo que en este punto los equipos de entrega se sienten cómodos con la creación de microservicios y están listos para atacar los problemas persistentes. Sin embargo, pueden verse limitados con las capacidades que pueden desacoplar a continuación sin una dependencia del monolito. La causa principal de esto, a menudo es una capacidad dentro del monolito con fugas, no bien definida como un concepto de dominio, con muchas de las capacidades del monolito que dependen de él. Para poder progresar, los desarrolladores necesitan identificar la funcionalidad acoplada, deconstruirla en conceptos de dominio bien definidos y luego replicar esos conceptos de dominio en servicios separados.
Por ejemplo, en un monolito basado en web, la noción de ‘sesión (web)’ es uno de los factores de acoplamiento más comunes. A menos que abordemos el desacoplamiento, la deconstrucción y la replicación de la actual «sesión», nos esforzaremos por desacoplar muchas de las capacidades futuras ya que se enredarán con el monolito a través de los conceptos de la sesión con fugas.
Los desarrolladores pueden extraer microservicios de forma progresiva de las funcionalidades acopladas a un servicio a la vez. Como ejemplo, es posible refactorizar primero la ‘lista de deseos del cliente’ y extraer eso en un nuevo servicio, luego refactorizar las ‘preferencias de pago del cliente’ en otro microservicio y repítalo.
Desacoplar verticalmente y liberar los datos
El principal impulsor de las capacidades de desacoplamiento de un monolito es poder liberarlas de forma independiente. Este primer principio debería guiar todas las decisiones que los desarrolladores toman sobre cómo realizar el desacoplamiento. Un sistema monolítico a menudo está compuesto de capas estrechamente integradas o incluso de múltiples sistemas que deben liberarse juntos y tener interdependencias frágiles.
La mayoría de los intentos de desacoplamiento comienzan con la extracción de componentes orientados al usuario y algunos servicios de front-end para proporcionar API de fácil uso para las interfaces de usuario modernas, mientras que los datos permanecen bloqueados en un esquema y sistema de almacenamiento. Aunque este enfoque ofrece algunas ventajas rápidas, como cambiar la interfaz de usuario con más frecuencia, cuando se trata de capacidades básicas, los equipos de entrega solo pueden moverse tan rápido como la parte más lenta, el monolito y su almacén de datos monolítico. En pocas palabras, sin desacoplar los datos, la arquitectura no es microservicios. Mantener todos los datos en el mismo almacén de datos es contrario a la característica de administración de datos descentralizada de los microservicios.
La estrategia es mover las capacidades verticalmente, desacoplar la capacidad central con sus datos y redirigir todas las aplicaciones de front-end a las nuevas API.
Tener múltiples aplicaciones de escritura y lectura desde y hacia los datos compartidos centralmente es el principal bloqueador para desacoplar los datos junto con el servicio. Los equipos de entrega deben incorporar una estrategia de migración de datos que se adapte a su entorno en función de si son capaces de redirigir y migrar todos los lectores / escritores de datos al mismo tiempo o no. La estrategia de migración de datos de cuatro fases se aplica a muchos entornos que requieren migrar de manera incremental las aplicaciones que se integran a través de la base de datos, mientras que todos los sistemas bajo cambio deben ejecutarse continuamente.
Desacople lo que es importante para el negocio y los cambios con frecuencia
La capacidad de desacoplamiento del monolito es difícil. Extraer una capacidad implica extraer cuidadosamente los datos, la lógica, los componentes del usuario y redirigirlos al nuevo servicio. Debido a que esta es una cantidad de trabajo no trivial, los desarrolladores necesitan evaluar continuamente el costo de la disociación con los beneficios que obtienen, por ejemplo, ir más rápido o crecer en escala. Por ejemplo, si el objetivo de los equipos de entrega es acelerar las modificaciones a las capacidades existentes bloqueadas en un monolito, deben identificar la capacidad que más se está modificando para sacar. Desacople partes del código que están continuamente cambiando y recibiendo mucho cariño por parte de los desarrolladores y las están limitando para entregar valor rápidamente. Los equipos de entrega pueden analizar los patrones de compromiso de código para descubrir qué ha cambiado históricamente más, y superponer eso con la hoja de ruta y la cartera del producto para comprender las capacidades más deseadas que recibirán atención en el futuro cercano. Necesitan hablar con los gerentes de negocios y de productos para comprender las capacidades de diferenciación que realmente les interesan. y superponga eso con la hoja de ruta y la cartera del producto para comprender las capacidades más deseadas que recibirán atención en el futuro cercano. Necesitan hablar con los gerentes de negocios y de productos para comprender las capacidades de diferenciación que realmente les interesan. y superponga eso con la hoja de ruta y la cartera del producto para comprender las capacidades más deseadas que recibirán atención en el futuro cercano. Necesitan hablar con los gerentes de negocios y de productos para comprender las capacidades de diferenciación que realmente les interesan.
Capacidad de desacoplamiento sin reescribir
Cuando los desarrolladores desean extraer un servicio de un sistema existente, tienen dos formas de hacerlo: extraer código o reescribir la capacidad.
A menudo, de forma predeterminada, la extracción del servicio o la descomposición monolítica se imagina como un caso de reutilización de la implementación existente tal como está y de extraerla en un servicio separado. En parte porque tenemos un sesgo cognitivo hacia el código que diseñamos y escribimos. El trabajo de construir, no importa cuán doloroso sea el proceso o el resultado imperfecto, nos hace crecer el amor por él. De hecho, esto se conoce como el efecto IKEA . Desafortunadamente, este sesgo va a frenar el esfuerzo de descomposición del monolito. Hace que los desarrolladores y, lo que es más importante, los gerentes técnicos ignoren el alto costo y el bajo valor de extraer y reutilizar el código.
Alternativamente, los equipos de entrega tienen la opción de volver a escribir la capacidad y retirar el código anterior. La reescritura les da la oportunidad de revisar la capacidad del negocio, iniciar una conversación con negocio para simplificar el proceso heredado y desafiar las suposiciones y restricciones antiguas incorporadas con el tiempo en el sistema. También brinda la oportunidad de una actualización tecnológica, implementando el nuevo servicio con un lenguaje de programación y una pila de tecnología que es más adecuado para ese servicio en particular.
Esta capacidad es posiblemente un buen candidato para reutilización y extracción. En contraste, el ‘perfil del cliente’ es una capacidad CRUD simple que se compone principalmente de código repetitivo para la serialización, manejo de almacenamiento y configuración, por lo tanto, es un buen candidato para volver a escribir y retirarse.
En nuestra experiencia, en la mayoría de los escenarios de descomposición, es mejor que los equipos reescriban la capacidad como un nuevo servicio y retiren el código anterior. Esto está considerando el alto costo y el bajo valor de la reutilización, debido a las siguientes razones:
- Existe una gran cantidad de código repetitivo que se ocupa de las dependencias del entorno, como el acceso a la configuración de la aplicación en tiempo de ejecución, el acceso a los almacenes de datos, el almacenamiento en caché y se construye con marcos antiguos. La mayor parte de este código repetitivo debe ser reescrito. La nueva infraestructura para alojar un microservicio es muy diferente del tiempo de ejecución de la aplicación desde hace décadas y requerirá un tipo muy diferente de código repetitivo.
- Es muy probable que las capacidades existentes no se construyan alrededor de conceptos de dominio claros. Esto resulta en el transporte o almacenamiento de estructuras de datos que no reflejan los nuevos modelos de dominio y requieren una gran reestructuración.
- Un código heredado de larga duración que ha pasado por muchas iteraciones de cambio podría tener un alto nivel de toxicidad de código y un bajo valor para su reutilización.
A menos que la capacidad sea relevante, alineada con un concepto de dominio claro y con alta propiedad intelectual, recomiendo encarecidamente volver a escribir y retirar el código anterior.
Primero macro, luego micro
Encontrar los límites del dominio en un monolito heredado es tanto un arte como una ciencia. Como regla general, la aplicación de técnicas de diseño controladas por dominio para encontrar los contextos delimitados que definen los límites de los microservicios es un buen lugar para comenzar. Con demasiada frecuencia, vemos una sobrecorrección de monolito grande a servicios realmente pequeños, servicios realmente pequeños cuyo diseño está inspirado e impulsado por la vista normalizada existente de los datos. Este enfoque para identificar los límites de los servicios casi siempre conduce a un gran número de servicios idénticos para acceder a los recursos de CRUD. Para muchos nuevos en la arquitectura de microservicios, esto crea un entorno de alta fricción que, en última instancia, falla la prueba de la liberación y ejecución independientes de los servicios. Crea un sistema distribuido que es difícil de depurar, un sistema distribuido que se rompe a través de los límites transaccionales y, por lo tanto, difícil de mantener consistente, un sistema que es demasiado complejo para la madurez operativa de la organización. Aunque hay algunas pistas, sobre cómo de ‘micro’ debe ser el microservicio: el tamaño del equipo, el tiempo para volver a escribir el servicio, la cantidad de comportamiento que debe encapsular, etc. Nuestro consejo es que el tamaño depende de cuántos servicios pueden proporcionar los equipos de entrega y operación de forma independiente liberar, monitorear y operar. Comience con servicios más grandes en torno a un concepto de dominio lógico, y divida el servicio en múltiples servicios cuando los equipos estén listos para la operación.
Migrar en pasos evolutivos atómicos
La idea de desvanecer un legado monolito en el aire al disociarlo en microservicios bellamente diseñados es algo así como un mito y posiblemente indeseable. Cualquier ingeniero experimentado puede compartir historias de migraciones e intentos de modernización que se planificaron e iniciaron con un optimismo total y, en el mejor de los casos, se abandonaron en un momento suficientemente bueno. Los planes a largo plazo de tales esfuerzos se abandonan debido a que cambian las condiciones macro: el programa se queda sin dinero, la organización gira su enfoque hacia otra cosa o el liderazgo en su apoyo. Por lo tanto, esta realidad debe diseñarse en la forma en que los equipos abordan el viaje del monolito al microservicio. Yo llamo a este enfoque ‘migración en pasos atómicos de la evolución de la arquitectura’, donde cada paso de la migración debería acercar la arquitectura a su estado objetivo. Cada unidad de evolución puede ser un pequeño paso o un gran salto, pero es atómica, ya sea completa o revierte. Esto es especialmente importante ya que estamos adoptando un enfoque iterativo e incremental para mejorar la arquitectura general y los servicios de desacoplamiento. Cada incremento debe dejarnos en un lugar mejor en términos del objetivo de la arquitectura, la función de la aptitud arquitectónica después de cada paso atómico de la migración debe generar un valor más cercano al objetivo de la arquitectura.
Imagine que el objetivo de la arquitectura de microservicio es aumentar la velocidad de los desarrolladores modificando el sistema general para ofrecer valor. El equipo decide desacoplar la autenticación del usuario final en un servicio separado basado en el protocolo OAuth 2.0. Este servicio está destinado tanto a reemplazar la forma en que la aplicación cliente existente (arquitectura antigua) autentica al usuario final, como a los nuevos servicios de arquitectura que validan al usuario final. Llamemos a este incremento en la evolución, ‘Introducción al servicio de autenticación’. Una forma de presentar el nuevo servicio es seguir estos pasos primero:
- Cree el servicio Auth, implementando el protocolo OAuth 2.0.
- Agregue una nueva ruta de autenticación en el back-end monolito para llamar al servicio Auth para autenticar al usuario final en cuyo nombre está procesando una solicitud.Si el equipo se detiene aquí y gira para construir otro servicio o característica, deja la arquitectura general en un estado de mayor entropía. En este estado, hay dos formas de autenticar al usuario, la nueva ruta base de OAuth 2.0 y la ruta basada en contraseña / sesión del antiguo cliente. En este punto, los equipos están realmente más lejos de su objetivo general de hacer cambios más rápido. Cualquier nuevo desarrollador del código monolito necesita lidiar con dos rutas de código, una mayor carga cognitiva de comprensión del código y un proceso más lento para cambiarlo y probarlo.
- Reemplace la autenticación basada en contraseña / sesión del antiguo cliente con la ruta OAuth 2.0
- Retire la ruta del código de autenticación anterior del monolito
En este punto, podemos argumentar que los equipos se han acercado a la arquitectura de destino.
A menudo encontramos que los equipos finalizan la migración de una capacidad fuera del monolito y claman la victoria tan pronto como se construye la nueva capacidad sin retirar la ruta del código antiguo, el anti-patrón descrito anteriormente. Las razones principales para esto son:
- (a) El enfoque en los beneficios a corto plazo de la introducción de una nueva capacidad y
- (b) La cantidad total de esfuerzo requerido para retirar las implementaciones antiguas al tiempo que se enfrentan a prioridades competitivas para crear nuevas características.
Para hacer lo correcto, debemos esforzarnos por hacer los pasos atómicos lo más pequeños posible.
Al migrar con este enfoque, podemos dividir el viaje en viajes más cortos. Podemos detenernos, revivir y sobrevivir con seguridad en este largo viaje, matando al monolito.
Herramientas
Para llevar a cabo un desarrollo orientado a microservicios, las «nuevas» herramientas como docker y los nuevos servicios en la nube (PaaS) son un gran aliado si nuestro negocio nos permite alojar nuestros datos en una nube pública.
Para implementar de una forma escalable y sin complejas adaptaciones en nuestro departamento de sistemas podemos usar kubernetes on premise o con nuestro PaaS de confianza.
Conclusiones
Además del volumen hay que tener en cuenta el tema de la complejidad. Es decir, para nosotros, la ventaja la ves cuando tienes una plataforma empresarial donde la gente implanta soluciones, etc, y empiezas a identificar temas recurrentes como: Autenticación/Autorización, o un servicio de subida de imágenes.
No tiene sentido que cada aplicación que tenga que subir una imagen lo vuelva a implementar.
Si implementas un servicio de subida de imágenes (que además así lo tienes optimizado y que si hay cualquier cambio no tiene impacto en cada una de las soluciones que lo consumen) y de paso estandarizas, monitorizas recursos, etc.
De estas forma descargas a los proveedores que desarrollan aplicaciones de negocio de servicios cross que implican integrarse en tu infraestructura, etc, etc.
Partiendo de escenarios reales, esto lo acabas viendo en plan «arquitectura emergente».. a partir de cada solución puedes ver la funcionalidad que es candidata a ser Cross.
Pero.. si por contra quieres hacer Cross toda una solución de negocio.. por un lado puede que en sí mismo no tenga sentido (voy a poner la función de planificar una reunión as a service para toda la compañía?) lo que implica puede no ser viable por costes, tiempo y complejidad de implantación ya que:
- Por lo pronto tienes que refactorizar el código para aislarlo por dominios, subdominios, etc
- Al atomizar los componentes tienes más complejidad de operación y de trazabilidad.. a nivel de testing se complican las pruebas de integración, etc.
- Tienes que incorporar un message queue o ESB, etc, con lo que es más infraestructura que gestionar.
Las alternativas cloud han simplificado un poco esto, pero crea otros problemas en operación / TIC.
Pero volviendo al refactor de código, tienes que saber manejar:
- Patrones como Strangler para aislar dominios.
- Log Aggregators para centralizar los logs
- Patrones como SAGA para orquestar el acceso a base de datos para garantizar la transaccionalidad ya que ahora cada microservice tiene su propio repositorio y no puedes hacer cosas tan básicas como un «drop cascade»
- Un Service Registry para que cada servicio se registre y que los servicios clientes no tengan que guardar urls que pueden romperse, verificar que los servicios están activos etc.
Si todo esto no lo tienes muy claro o la gente que lo implementa no tiene experiencia esto se acaba estancando y eternizando.
Si además se plantea como un proyecto SCRUM de «vamos haciendo y mockeando que a algún sitio llegaremos» sin una definición de la arquitectura.. pues.. acabas teniendo una chapuza luego inmantenible y sobre todo que genera muchos más problemas que una aplicación clásica, cada problema es más difícil de diagnosticar, etc..
Como suele pasar, cuando no hay planificación y te consumes el presupuesto.. los tests se abandonan.. con lo que después de todo el sufrimiento tienes una fantástica aplicación Legacy en producción de nuevo cuño.
En general, en la vida real el problema es la falta de la figura del arquitecto.. tanto los «responsables» como los chavales quieren probar cosas nuevas «porque están de moda».. pero.. luego.. ¿quién paga la fiesta?
Bibliografía:
https://martinfowler.com/articles/break-monolith-into-microservices.html