0

Patrones de migración  de Monolito a Microservicios

En este nuevo artículo, hablaremos del uso de diversos patrones para llevar a cabo la transición de un monolito a microservicios de diversas maneras y siguiendo diferentes directrices o patrones. La adopción de microservicios en arquitecturas extensas es cada vez más prevalente, en parte debido a su capacidad para aislar responsabilidades, su idoneidad para la nube, las responsabilidades únicas, la tolerancia a fallos y la facilidad de escalado, entre otras características.

Pero cuando tenemos una aplicación monolítica y queremos abordar su posible migración nos encontramos con la típica pregunta de, ¿por dónde empezamos?, por funcionalidades, servicios, bases de datos, etc.

La arquitectura monolítica convencional presenta todos los objetos y accesos a la base de datos gestionados por un código altamente acoplado. En este esquema, toda la información se almacena en una única base de datos, y la totalidad de la lógica de negocio se encuentra en la aplicación cliente y servidor. Uno de los aspectos fundamentales al migrar de una arquitectura monolítica a microservicios será la migración de la base de datos. Esta deberá llevarse a cabo en diferentes esquemas o bases de datos, conservando las mismas funcionalidades.

Analicemos ciertos patrones que son comúnmente utilizados en arquitecturas de migración de monolitos a microservicios. Además, discutiremos algunos patrones o enfoques de trabajo que podrían presentar desafíos.

Reescritura 

Aunque la idea de realizar un «Big Bang» puede resultar atractiva, como sugiere la propia palabra, esta estrategia nos llevará a una especie de explosión. En muchos casos, al iniciar la migración hacia una arquitectura de microservicios, la idea de refactorizar y reconstruir desde cero puede parecer tentadora, pero esto puede complicar la migración al no aprovechar la reutilización y la aplicación de patrones de migración. La reescritura implica una inversión significativa de tiempo en el desarrollo de código que ya existe en el monolito.

Si optamos por un «Big Bang», todo el desarrollo, junto con su lógica y funcionalidad, se recreará en nuestros microservicios. Este nuevo desarrollo conllevará un mayor tiempo de desarrollo (y, por ende, mayores gastos), además de que cada cambio en el monolito deberá replicarse en los microservicios o detener los desarrollos nuevos; algo que rara vez será aceptable.

Patrón Strangler Fig:  De Monolito a Microservicios

Cuando nos referimos a patrones para migrar de un monolito a microservicios, el «Strangler Fig» es uno de los más reconocidos y ampliamente aplicados. Desde mi experiencia, este método resulta ser el más simple de utilizar y aplicar durante una migración de monolito a microservicios.

Este patrón, también conocido como «Estrangulamiento de la Higuera» en español, toma su nombre de la analogía con una higuera real. Los higos comienzan en la rama y sus raíces descienden hasta el suelo, creciendo continuamente hasta «estrangular» al árbol padre. Si aplicamos esta idea a la migración de un monolito a una arquitectura de microservicios, la estrategia consiste en crear microservicios gradualmente hasta eliminar por completo el software original.

Para que este patrón sea efectivo, comenzaremos a desarrollar nuevos servicios en las fronteras de nuestro monolito. En otras palabras, iremos migrando a microservicios aquellos servicios externos con funcionalidades individuales. Ahora, veamos un ejemplo más ilustrativo:

En el ejemplo previo, nos enfrentamos a un monolito que engloba las cuatro funcionalidades destacadas en el diagrama. Para obtener una visión global, resulta esencial acceder a Movimientos Bancarios, Tarjetas y Préstamos. Consideramos estas tres funcionalidades finales como los bordes o el perímetro de nuestro monolito. Según el patrón Strangler Fig, podríamos iniciar la migración por los servicios más periféricos, como Movimientos Bancarios, Tarjetas y Préstamos, y crear microservicios para esas funcionalidades.

Patrón Parallel Run: De Monolito a Microservicios

Evidentemente, al desarrollar software, surgen posibilidades de fallos, y nuestro objetivo consiste en identificar y minimizar tales errores y problemas. Cuando realizamos la migración de un software que ya está en producción, la precaución debe ser aún mayor. Con el fin de evitar inconvenientes y realizar pruebas de nuestro software durante el desarrollo, podemos emplear el «Parallel Run» como uno de los patrones para migrar de un monolito a microservicios.

Para mitigar riesgos en nuestra migración, ejecutaremos de manera simultánea nuestro nuevo microservicio junto con la funcionalidad del monolito. El monolito actuará como la fuente de verdad y resolverá las peticiones, pero también se enviará la información al microservicio, y la respuesta se comparará con la del monolito. Una medida posible para verificar la corrección es almacenar ambas respuestas y contrastarlas con un proceso batch, lo que nos permitirá identificar y corregir posibles errores en nuestro microservicio.

Patrón Branch By Abstraction

El patrón «Branch By Abstraction» nos brinda la capacidad de retirar funcionalidades mientras el sistema está en pleno uso. Esta estrategia se revela como ideal en situaciones en las que la funcionalidad en cuestión depende estrechamente de otras funcionalidades o módulos.

La premisa fundamental de este patrón es posibilitar que tanto la funcionalidad existente como la nueva implementación de dicha funcionalidad coexistan dentro del mismo proceso en ejecución de manera simultánea. La implementación de este patrón implica la creación de una capa de abstracción sobre la funcionalidad que estamos buscando desarrollar.

Este proceso lo podríamos dividir en varias etapas:

  • Crear la capa de abstracción: Creamos una capa de abstracción sobre el módulo al que vamos a llamar.
  • Refactorización: Se ha de modificar el código para que se llame a esta nueva capa en lugar de directamente a la funcionalidad antigua.
  • Creación del microservicio: Una vez conocemos la funcionalidad a reemplazar y tenemos creada la capa de abstracción es momento de crear un microservicio con esa funcionalidad.
  • Migración: Una vez hemos creado el nuevo microservicio, y ha sido testeado podremos ir poco a poco conectando el nuevo microservicio para que resuelva todas las peticiones.

Iniciemos el examen de estos procedimientos partiendo de la representación anterior, donde tenemos la intención de ejecutar la transferencia de Transacciones Financieras.

En la imagen vemos la apa de  abstracción y ahora  veremos el microservicios.

La conexión:

Se elimina la funcionalidad antigua

Comúnmente, un monolito se caracteriza por contar con una única base de datos que alberga todas las funcionalidades. No obstante, al migrar hacia una arquitectura de microservicios, se adopta la práctica de disponer de un esquema o base de datos exclusiva para cada microservicio. A continuación, exploramos diversos patrones refinados de migración de bases de datos al realizar la transición de un monolito a microservicios.

Patrones de Bases de Datos: Database View

Cuando nos movemos desde una estructura en la cual compartimos nuestra base de datos entre todos los servicios y aspiramos a mantener una Base de Datos compartida en nuestra arquitectura de microservicios, la estrategia de generar vistas emerge como la más pertinente. A través de una vista, un servicio puede acceder a la Base de Datos de manera análoga a un esquema, posibilitando la observación exclusiva de la información asociada a ese servicio. Este enfoque podría ser una valiosa táctica al migrar de un monolito a Microservicios, ya que nos permitirá aislar nuestra base de datos y de manera gradual desentrañar nuestro monolito.

Al hacer uso de vistas, logramos gestionar el acceso a datos para nuestro microservicio, permitiéndonos ocultar la información o datos que resulten superfluos.

Aunque esta alternativa pueda ser una elección apropiada y digna de consideración al emprender la migración a microservicios, es esencial reconocer que presenta ciertas limitaciones:

  • Una vista es solo de lectura, lo que ya limita su utilidad.
  • La gran mayoría de Bases de Datos NoSQL no permiten vistas.
  • Acoplamiento debido a que se usa una misma Base de Datos.
  • El escalado se tiene que realizar a toda la Base de Datos.
  • Único punto de fallo.

Patrones de Bases de Datos: Database Wrapping Service

Cuando en alguna fase de nuestras migraciones identifiquemos que la creación de vistas o el manejo de información específica en la Base de Datos se complica de manera significativa, o que el DBA o el equipo encargado de los Datos no permite llevar a cabo migraciones o modificaciones sobre la Base de Datos, podemos recurrir al uso de DataBase Wrapping Service como uno de los modelos de migración de monolito a Microservicios. Este modelo nos proporciona la capacidad de crear un servicio que envuelve nuestra Base de Datos y nos entrega lo necesario.

El modelo Database Wrapping Service nos brinda varias ventajas, tales como:

  • Una única vista de Base de Datos, lo que nos permitirá escrituras y lecturas.
  • Uso de un servicio intermedio que nos permitirá devolver los datos que deseemos.
  • Permite la creación de un API de consumo de la Base de Datos.
  • Proporciona tiempo para reajustar o dividir la Base de Datos en Schemas o en más Bases de Datos.

Patrones de Bases de Datos: Database as a Service Interface

En determinadas circunstancias dentro de nuestra arquitectura, puede ocurrir que solo contemos con una Base de Datos destinada a consultas. Por ende, en estas instancias, tiene sentido permitir la visualización de los datos administrados por el servicio. La única precaución a tener en cuenta en estos casos es la separación entre las Bases de Datos que exponemos y aquellas que se encuentran en los límites de nuestro servicio.

Para implementar el patrón Database as a Service Interface, podemos adoptar la estrategia de crear una Base de Datos dedicada como solo lectura, accesible desde fuera de los límites de nuestro servicio mediante una API expuesta, y actualizarla cuando la Base de Datos interna experimente cambios; además de contar con una Base de Datos interna en nuestro servicio. Para el proceso de actualización de la Base de Datos de solo lectura, dispondremos de un motor encargado de poblarla.

Veamos una representación gráfica:

En el esquema anterior, se aprecia la existencia de dos Bases de Datos: la interna en nuestro servicio, habilitada para operaciones de lectura y escritura, y la externa dedicada exclusivamente a operaciones de lectura. Además, resaltamos un componente esencial en este patrón: el servicio de actualización o mapping engine, actuando como una capa de abstracción. Su función primordial es facilitar la actualización de nuestra base de datos ante cambios, asegurando la coherencia de datos en nuestro sistema.

Aspectos positivos y desafíos asociados a la interfaz de Database-as-a-Service:

  • Presenta mayor complejidad que otros patrones de Bases de Datos compartida.
  • Ofrece una mayor flexibilidad que el uso de vistas.
  • Aislamos las lecturas de las escrituras.
  • Es perfecto para aquellos clientes que únicamente necesitan lecturas.
  • Requiere un mayor esfuerzo y por tanto inversión de tiempo mayor a otros patrones.

Hasta ahora, hemos revisado cómo aliviar o minimizar el esfuerzo mediante directrices o patrones durante la transición de un monolito a microservicios, especialmente cuando compartimos Bases de Datos. Ahora, examinemos cómo podemos fragmentar nuestra Base de Datos de acuerdo con nuestros microservicios.

Sincronización de datos con múltiples Bases de Datos para migrar De Monolito a Microservicios

En el proceso de migrar nuestra arquitectura hacia microservicios, al utilizar, por ejemplo, la técnica Strangler Fig, coexistirán tanto microservicios como nuestro monolito. En esta situación, la Base de Datos compartida y las Bases de Datos de nuestros microservicios compartirán espacio.

Durante el período en el que ambas arquitecturas deben convivir, es esencial mantener sincronizadas todas las Bases de Datos.

Para abordar este desafío y encontrar la mejor solución posible, es necesario analizar nuestras necesidades. Si necesitamos una Base de Datos compartida, podríamos considerar la creación de vistas. En caso de enfrentarnos a un enfoque tipo Big Bang, podríamos emplear un Batch o un flujo continuo de datos, utilizando, por ejemplo, Kafka y KSQL para la migración, entre otras opciones.

Patrón Trace Writer

Este enfoque nos brinda la posibilidad de llevar a cabo una migración gradual de la Base de Datos del monolito, que actúa como nuestra fuente primaria, hacia la nueva Base de Datos de nuestro microservicio de manera progresiva. En este momento, coexistirán dos bases de datos, por lo que será fundamental considerar la sincronización para evitar inconsistencias en los datos. De esta manera, el nuevo código utilizará la nueva Base de Datos como fuente primaria, siendo este nuevo código desarrollado en el recién creado microservicio.

La esencia del patrón trace writer radica en realizar lanzamientos incrementales, evitando realizar un cambio completo que abarque todas las funcionalidades, ya que esto podría generar diversos problemas e inconsistencias. Con cada lanzamiento, incorporaremos nuevas funcionalidades, permitiéndonos desconectar de manera gradual a los clientes de la antigua fuente primaria y conectarlos a la nueva fuente primaria.

Una vez hayamos concluido con todas las funcionalidades, ya no será necesario mantener los dos orígenes de datos, y la antigua fuente principal podrá ser desconectada una vez que todos los consumidores se conecten a la nueva Base de Datos.

Es crucial tener en cuenta que contaremos con dos fuentes principales, lo que implica que, por un lado, debemos evitar inconsistencias, y por otro lado, será necesario mantener los datos sincronizados, incluso si eso conlleva duplicados.

Otro aspecto a considerar con este patrón es la consistencia eventual de los datos. Durante las sincronizaciones, por ejemplo, mediante un sistema de CDC (Change Data Capture), podríamos enfrentarnos a períodos en los que nuestros datos sean inconsistentes, por lo que es necesario evaluar y tener en cuenta estos lapsos.

Dividir la Base de Datos de Monolito a Microservicios

Cuando nos enfrentamos a la migración de una arquitectura monolítica a microservicios, es probable que deseemos abordar la división y ruptura de nuestra Base de Datos del monolito, fragmentándose para que cada microservicio tenga su propia Base de Datos. En situaciones como estas, donde adentrarse en esa ruptura puede resultar complicado, existen algunos patrones, pasos y recomendaciones que podemos seguir para intentar simplificar la tarea, aunque es importante tener en cuenta que dividir una Base de Datos siempre será bastante complejo.

Al llevar a cabo la división de la Base de Datos, lo primero que debemos considerar es si queremos comenzar por la división del código y crear nuestros microservicios, o si preferimos iniciar por la división de la Base de Datos para luego avanzar hacia la división del código.

Divisón del código

La ventaja de comenzar primero por el código primero es que vamos a tener más claro cuales son los datos que vamos a necesitar en nuestro microservicio. De esta manera luego podremos abordar esa separación con las ideas más claras. Para poder abordar esta tarea podemos usar el patrón Monolith as data access layer el cual se basa en crear un API delante del monolito para la obtención de datos. De esta manera podremos seguir desarrollando nuestro microservicio y a continuación «romper» nuestra Base de Datos. O también podemos utilizar la aproximación Multischema Storage, en la que nuevas tablas o funcionalidades podemos añadirlas a nuevos schemas.

Dividir la Base de Datos

Cuando mantenemos nuestro código, es decir, nuestro monolito, y optamos por dividir la Base de Datos primero, será necesario realizar modificaciones en nuestro código, ya que ahora tendremos que realizar consultas contra varias Bases de Datos o esquemas. De esta manera, lo que anteriormente logramos con una única consulta ahora requerirá varias consultas, y deberemos realizar las respectivas uniones en memoria. A pesar de este desafío, podremos verificar los cambios que realizamos sin afectar a los clientes conectados, lo que nos permitirá retroceder más fácilmente ante cualquier error.

Por otro lado, nos enfrentaremos a problemas como la transaccionalidad, que deberán ser manejados con múltiples transacciones. Para abordar esta aproximación, podemos hacer uso del Patrón Repository per bounded context, que sugiere la idea de agregar una capa de repositorio con algún framework como Hibernate para mapear objetos de la Base de Datos de manera más eficiente.

Ambos a la vez

Indudablemente, podemos abordar la división tanto del código como de la Base de Datos de manera simultánea, ¿por qué no hacerlo?, pero esta enfoque requerirá más esfuerzo y tiempo. Además, revertir cambios en caso de problemas o errores también será más complejo.

Conclusiones

Estas han sido algunas sugerencias y patrones clave para abordar la migración de un monolito a microservicios. No obstante, la versatilidad de este proceso permite la creación de enfoques híbridos al combinar diferentes patrones según las necesidades específicas del proyecto.

Es esencial reconocer que la migración no estará exenta de desafíos, como el manejo de la transaccionalidad en la base de datos. Situaciones como esta podrían requerir la implementación de patrones específicos de microservicios, como Sagas, o la fragmentación de tablas a través de múltiples servicios.

Dada la longitud y complejidad potencial de una migración hacia una arquitectura de microservicios, es imperativo realizar un análisis exhaustivo antes de comenzar. Comprender cómo abordar cada fase y anticipar posibles obstáculos permitirá una transición más fluida y exitosa. La planificación cuidadosa y la selección inteligente de patrones son fundamentales para garantizar el éxito a largo plazo de la migración.

Fernando Sonego

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *