Arquitectura de microservicios vs arquitectura monolítica

 

DISCLAIMER: por claridad, el presente artículo asume que:

  • la definición de microservicio en este contexto conlleva separación física entre servicios
  • una arquitectura de microservicios se considera “propiamente” implementada, es decir:
    • en ausencia/mínimo de comunicaciones RPC request/reply entre servicios
    • cada servicio encapsula su persistencia
    • cada servicio expresa una funcionalidad. No existe “servicio de base de datos”

 

Si una de estas asunciones no subsiste, las conclusiones pueden no ser válidas.

 

Many Small Monoliths

 

Las sesiones de conferencia de la semana pasada de Martin Fowler in Barcelona dejan en evidencia algunos aspectos de los “trends tecnológicos” de estos últimos años.


En concreto, nos ha interesado mucho la parte acerca de los modelos arquitecturales a “microservicios”, la falta de definición formal que tiene el término, y los problemas que derivan de su adopción a prescindir de un atento análisis de requerimientos y de la situación del proyecto.


El problema por supuesto no reside en un patrón arquitectural en sí, estudiado para dar solución a una situación concreta, sino en la falta de matching de requerimientos a la hora de aplicarla.


La reflexión sobre la arquitectura de microservicios sugiere un conjunto de patrones/prácticas/tecnologías que han salido en estos últimos años de innovación tecnológica, y que han sido adoptados, a menudo de forma “extrema”, por los equipos de desarrollo.


Un breve listado:

  • containerización (docker, Kubernetes)
  • programación funcional
  • programación reactiva
  • CQRS
  • Event Sourcing
  • modelos a actores persistentes
  • arquitectura de microservicios
  • serverless architectures y BaaS
  • nuevos trends front-end
  • persistencia no relacional
  • polyglot programming/persistence
  • deep learning y machine learning en general
  • continuous rewrites


El propósito es iniciar una serie de posts en los que razonar retrospectivamente sobre el early adoption de lo presentado en cada punto, y nuestras consideraciones basadas en nuestras experiencias.
Tomando la ocasión del tema, refrescado por Martin Fowler, de arquitectura de microservicios, intentaremos razonar una posible respuesta sobre cuáles son los requerimientos para su adopción, explicitando a fondo los costes.

 

Los Objetivos de arquitectura de Microservicios

 

Es importante, a la hora de la valoración, tener muy claro los objetivos y los problemas que se prentende resolver con una transición a arquitectura de microservicios, que recordamos brevemente aquí:

  • builds rápìdas a nivel de componente, manejabilidad de los proyectos
  • boundaries muy claros y rígidos
  • independencia de deployment y quality of service
  • posibilidad de polyglot programming/persistence

 

Los Costes

 

El coste operacional


Se tiende a subvalorar el hecho de que cada microservicio requiere:

  • unas políticas de delivery
  • una construcción automática, con scripts repetidos/incluidos/generados
  • una pipeline de delivery/deployment, con scripts como arriba
  • una monitorización/health checking/self healing
  • una política de escalado, basada en parámetros específicos
  • una definición de dependencias comunes
  • un repositorio de control de versiones
  • unas gestiones de fallos
  • un log, preferiblemente centralizado
  • más exigencias específicas de cada servicio


No obstante sea cierto que tecnologías como docker y los DSLs de Jenkins ayudan mucho a la hora de hacer reproducibles los puntos anteriores, aún así es imposible que los costes operacionales igualen a los de un monolito:

  • más pipelines requieren más recursos hardware y de cómputo
  • para verificar una interacción, puede que haya que esperar a más de una pipeline
  • lo mismo para reproducir/ver fijado un bug


Estos puntos se ven muy afectado por el problema de los boundaries (v. más abajo).
También:

  • necesitaremos, para no repetir los aspectos de soportabilidad y operabilidad, extraer librerías comunes
  • lo mismo para todo lo que sea código común


Esto:

  • añade complejidad a las soluciones, desde el momento cero del proyecto
  • genera dependencias entre pipelines
  • genera la necesidad de repositorios privados

 

Estandarización

 

Más en general, la mitigación de los costes de operaciones requiere una cultura fuerte desde el punto de vista de estandarización de automaciones/sistemas. Esto es algo que no suele generarse en el ámbito de un solo proyecto.


La realidad que vemos es que, a pesar de los esfuerzos dedicados, muchas arquitecturas a microservicios no están respaldadas por la infraestructura operacional que requerirían.
Como también otros aspectos, el esfuerzo en estandarización es más eficiente si la migración hacia arquitectura de microservicios es incremental, y los aspectos operacionales “emergen” de forma natural de la extracción de servicios.

 

Ciclos de feedback

 

Como escrito arriba, arquitectura de microservicios no deja de ser un sistema complejo, siendo más que simplemente la suma de sus partes.
Es cierto que, asumiendo:

  • haber pagado ya los costes operacionales y de estandarización
  • una relativa estabilidad de los boundaries de un microservicio


el ciclo de feedback durante el desarrollo es entonces comparable, o incluso mejor, al sistema monolítico, a nivel de componente.
Es pero de grande importancia explicitar que existe un balance entre la facilidad del feedback a nivel de componente y el feedback sobre la composición de componentes, que resultará mucho más complejo:

  • cualquier problema a nivel de sistema hay que buscarlo en mensajería, con dificultad de debugging
    • la herramienta necesaria es frecuentemente de bajo nivel
    • este problema se hace más grave en caso de problemas de rendimiento
  • frecuentemente los mensajes no son tipados, moviendo los problemas de compile-time a runtime
  • un arreglo a un problema, para ser probado, puede requerir de recompilación/reconstrucción de uno o más componentes

 

La complejidad por interacción (smart service, dumb pipes)

 

Como Michael Feathers justamente resalta, si complejidad no está en los componentes, está necesariamente en la interacción entre componentes.


A esto se refiere Martin Fowler subrayando el principio “smart services, dumb pipes”.
La idea básica es minimizar las dependencias a la hora de poder reemplazar/actualizar una instancia de microservicio.


Más en general, la heurística responde, por una vez más, al concepto de encapsulación: cuanto más distribuida la “inteligencia”, más modular será el sistema.
A pesar de nuestro apoyo incondicional en relación al uso de tecnologías que faciliten la comunicación asíncrona (message brokers), no podemos negar que diferente es el coste entre una sesión de debug de dos líneas de código y de una interacción mediada por algún tipo de bus.

 

Many Small Monoliths

 

Para quien escribe, el punto central de los costes es la modelación de los “service boundaries”.


Una redefinición (o incluso reescritura) de un microservicio tiene un coste oculto mucho más alto que un cambio normal en un código base, entre otras cosas:

  • aplicación de los costes operacionales (librerías, policies, etc.)
  • migraciones de esquemas/datos en subsistemas con diferente ciclo de vida
  • redefinición de contratos, inestabilidad de las APIs públicas
  • testabilidad sólo a nivel de integración


El problema es que, como bien comentó Eric Evans en la última edición de DDD Europe, la definición de los boundaries es algo de lo más complicado, sobre todo al principio de la evolución de un sistema, tanto que llega a parecerse a algo “elástico”.


Probablemente, la única certeza que tenemos es que el primer diseño estará equivocado con respecto de los requerimientos posteriores del proyecto. Además, el concepto no es nuevo: es una piedra angular de las metodologías ágiles.


Usar un acercamiento a microservicios desde el principio proporciona prácticamente la garantía de que habrá que cambiar los boundaries de uno o (más probable) más subsistemas a lo largo del proyecto.


Esto conlleva que el “microservices premium” se pagará, inevitablemente, de forma:

  • a cada cambio de boundaries, como coste del cambio (ver arriba)
  • si se decide minimizar los cambios de boundary, a nivel de multiplicación de “conversaciones” entre servicios, causando problemas de mantenibilidad y incluso de rendimiento/escalabilidad


Consideramos la cantidad y calidad de interacción entre servicios como el punto central de las arquitecturas orientadas a servicio, hasta el punto que si pudiéramos remplazaríamos con “many small monoliths” la expression “microservices”.

 

Esperanza a… arquitectura de microservicios!

 

Quality of Service con monolito

Hablamos de “quality of service” porque, realmente, es una de las cualidades arquitecturales que esperamos de una transición a microservicios.


Entendemos, de hecho, que el término “escalabilidad” es impropio (por poco específico) en este contexto (a meno que no se interprete como escalabilidad funcional): los límites a la genérica “escalabilidad” no dependen tanto de una distribución monolito vs microservicios, sino de otras propiedades de carácter más general, como la ausencia de estado compartido en los servicios o la distribución de datos.


Aún así, como ejercicio mental, es posible imaginar una situación en la que el desarrollo es sobre un código base único (“monolito”), pero la distribución es diversificada.


Es decir, en las máquinas donde distribuir el servicio “usuario”, se despliega el monolito entero, pero se publican sólo las APIs de usuario.


Esto permite el despliegue de diferentes versiones del mismo monolito.


Minimizaría los costes de la separación física, pero consiguiendo la independencia de deployment, que es una de las características que buscamos de una arquitectura a microservicio.


Consideramos que, sin ser una arquitectura particularmente “comunicativa”, este modelo puede ser viable en un momento de transición entre monolito y microservicios.
Incluso, puede sacar de “apuros” en momentos dónde se necesita urgentemente escalabilidad a nivel de componente.

 

Si hay tantos problemas, ¿para qué arquitectura de microservicios?

 

Es importante expresar de forma clara nuestra posición:


Nosotros no estamos “en contra”, ni mucho menos, de una arquitectura de microservicios.


Intentamos pero llevar a consciencia los costes que esta conlleva, para tomar una decisión razonada.


El problema que intentamos atacar es que la percepción de sencillez que da al principio la arquitectura a microservicio es falsa y distorsionada.


Es fácil pensar que un proyecto de cero, reducido en su tamaño, sea lo más sencillo para empezar un desarrollo extendiendo un sistema.


El problema, en ese momento, es olvidarse de todos los costes que quedan escondidos relativos al hecho de que un microservicio no deja de ser una parte de un todo.


Nos es difícil en general proponer reglas en relación a la adopción de estilos arquitecturales.


Intentamos proponer un acercamiento que nos es “cómodo”, en base a nuestra (limitada) experiencia, que es lo único que podemos hacer.


Si aplicamos la filosofía ágil al problema, la idea es quizás empezar con una situación de coste mínimo (monolito), y, escuchando atentamente a los pain-points en la evolución del proyecto, dejar que la partición física “emerga” de forma natural.


Nos imaginamos un proceso de este tipo:

  • empezar con una sóla base de código, intentar, en la medida de lo posible, estructurar a microservicios “lógicos” y separar por “contextos”
  • las piezas muy “técnicas”, como proxies, o piezas de alto rendimiento poco o nada relacionadas con el resto del sistema, deberían constituir desde cero otros “microservicios”
  • escuchar atentamente a los pain-points que puedan sugerir un inicio de transición. Típicamente:
    • las compilaciones/construcciones duran mucho
    • los ciclos de vida de diferentes areas del sistema empiezan a colisionar de forma continua
    • el código base empieza a ser inmanejable por su tamaño, o tras razones de tipo “físico”
    • cambios triviales conllevan despliegue de alto riesgo
    • existen necesidades comprobadas de cambios tecnológicos en alguna area del sistema
    • la empresa crece y se forman naturalmente equipos de desarrollo organizados por área funcional

 

Si hay uno o más pain-points demostrados, antes de simplemente “partir el sistema”, hacer un análisis de los contextos (context-map), cosa que ayudará a minimizar el cambio de boundaries.


Puede no ser necesario partir el monolito en macro-contextos, pero si se extrae un microservicio, se aconseja hacerlo basándose en el análisis previo.


Si se extrae un microservicio que en base al análisis previo pertenece a un contexto de otro microservicio, plantear un análisis de ventajas y inconvenientes de unir los códigos base.


Con continuidad con los orígenes de la cultura de microservicios, las razones y las modalidades de transición no son muy diferentes que las de transiciones a SOA.


Se diferencian por el hecho de que es posible extraer servicios de pequeño tamaño.


Nos gusta referirnos a este proceso con el término “microservices mitosis”, para subrayar el carácter progresivo, incremental y “natural” del proceso por el que se genera por división un microservicio cuando los boundaries hayan demostrado madurez suficiente.


Vemos imprescindible añadir una nota sobre las tecnologías “serverless”: el hecho que estas tecnologías típicamente distribuyen “por defecto” no influye sobre las presentes consideraciones, ya que nada impide que el código base sea único, y que cambien simplemente los códigos de activación.

 

Microservicios lógicos

Mientras tenemos las dudas expuestas sobre la adopción up-front de microservicios con separación física, pocas tenemos con respecto a microservicios en separación lógica.


Un microservicio en contexto DDD puede ser simplemente un domain service en su bounded context, y así lo expresa con frecuencia Vaughn Vernon en sus posts sobre DDD y modelo a actores.


No es esta la sede donde profundizar estos aspectos, ya que DDD es un conjunto muy avanzado de patrones de modelación.


Sólo nos importa subrayar que los microservicios “lógicos” son consecuencia necesaria de una buena aplicación de DDD.

 

Un posible Win-Win

Simplemente como hipótesis, nos gustaría imaginar tener una tecnología que pueda habilitar las ventajas de los microservicios a nivel de quality of service y independencia de deployment, con los costes mínimos de un monolito en su estado inicial.

Para que esto suceda, deberíamos tener un solo (o unos pocos) artefactos deployable físicos, que pero permitieran mover en runtime el número de instancias de los microservicios lógicos contenidos.

Un sistema de este tipo nos permitiría tratar los demás problemas (tiempos de build, organización de equipos) con más distancia, sin la necesidad de tomar decisiones up-front.

Esperamos que, tecnologías que aporten location transparency, como por ejemplo los sistemas de actores en Akka, puedan aplanar las brechas tán extremas entre monolitos y microservicios.

Si quieres enviarnos tu proyecto para que le echemos un ojo a la arquitectura, hazlo aquí.

Si este artículo te gustó, te puede interesar: 

 

Principio de responsabilidad única 

Por qué Kotlin?

Patrón MVP en iOS

F-bound en Scala

Sobre Dioses y procrastinación

Arquitectura de microservicios

Fibers en Nodejs

Simular respuestas del servidor con Nodejs

 

Suscríbete a nuestro newsletter para estar al día de los eventos, meet ups y demás artículos!