No, los Microservicios NO son "LA SOLUCIÓN" ni "valen para todo".
Estos dos últimos años se ha desatado una “locura” en torno a la Arquitectura de Microservicios, que ha “revolucionado” el modo de desarrollar y aparenta ser el “Bálsamo de Fierabrás” que “cura todas las dolencias”. Cualquier proyecto que no utilice Microservicios se considera automáticamente una “antigualla” y totalmente obsoleto y erróneo. Me temo que debo disentir.
Como cualquier solución tecnológica, tiene sus ventajas e inconvenientes y sus ámbitos de aplicación. Hay muchos escenarios para los que es una solución válida, pero no es aplicable automáticamente a todos los proyectos, ni siquiera a la mayoría, y además creo que el “entusiasmo” está haciendo no se evalúe adecuadamente sus desventajas, solo las ventajas. Para cualquier proyecto y escenario debe evaluarse con la mente abierta diversas soluciones, ponderar y medir ventajas e inconvenientes y sólo entonces elegir la mejor opción.
Creo que algunos sentimos una sensación de “Déjà vu” que hace recordar en cierto modo el entusiasmo en su momento con los Servicios Web, ahora rechazados frente al nuevo “juguete”. Ambos coinciden en ser una Arquitectura Orientada a Servicios, que prometía desacoplar los elementos de las aplicaciones, desarrollar aplicaciones más ligeras sin repetir código, desarrollar en distintos lenguajes y tecnologías y ofrecer un abanico servicios que por medio de una especie de diccionario/directorio, permitiría a las aplicaciones dinámicamente localizar el servicio web adecuado e invocarlo. ¿Suena conocido?
Antes de reflexionar sobre la Arquitectura de Microservicios, hay que destacar que su aparición ha coincidido con dos tecnologías/paradigmas muy importantes y útiles, un incremento de las posibilidades y oferta de virtualización (por medio de servicios como Amazon o Azure o por medio de “micro-máquinas virtuales” como Docker) y los modelos de integración continua y DevOps. Hay que tener en cuenta, que puede usarse microservicios sin esos elementos y que puede hacerse desarrollo “monolítico” con ellos, por tanto hay que centrarse exclusivamente en las características de los microservicios en sí mismos.
Otro punto previo a considerar es la simplificación planteada de “aplicación monolítica” frente a “aplicación basada en microservicios”. Creo que hace muchos años que no veo una aplicación totalmente monolítica y aislada. La mayor parte de las aplicaciones están distribuidas en varios servidores, invocan Servicios Web, leen de colas de mensajería, se integran con herramientas EAI, intercambian archivos con otros servidores del mismo proceso, etc. En cuanto a los microservicios, a pesar de su nombre no son exactamente “micro”. Según recomiendan los expertos, deben tener cierta coherencia funcional, por lo que no se trata simplemente de, por ejemplo, un servicio de “alta de un elemento”, ni siquiera de un servicio de mantenimiento (alta, baja y modificación) de elementos. Cada “servicio” puede consistir en varios métodos/funciones de diversa complejidad. Por tanto no se trata de comparar opción “monolítica - gigante” frente a opción “totalmente distribuida - micro” sino opción “poco distribuida con grandes bloques” frente a opción “muy distribuida con bloques pequeños”.
Para poder comparar ventajas e inconvenientes voy a plantear un proyecto hipotético a desarrollar en Java para desplegar en un servidor de aplicaciones, comparando las dos alternativas de arquitectura (monolítica vs microservicios) y revisando hasta qué punto son reales las “ventajas” de los MicroServicios. Supongamos un proyecto que consta de 4 bloques funcionales (susceptibles de convertirse cada uno en un micro servicio) con interrelaciones entre ellos. Para este escenario, se comparan las dos soluciones en tres aspectos principales:
- Diseño - Desarrollo,
- Despliegue - Infraestructura
- Explotación - Monitorización.
1- Diseño - Desarrollo.
1-A) “Los microservicios permiten a trabajar a varios grupos de forma independientemente”.
Por supuesto, al igual que los paquetes, espacios de nombres, librerías o diversas técnicas existentes en la mayoría de los lenguajes ”serios”.
Dado que los métodos y parámetros entre bloques/servicios deben estar definidos en fases iniciales (en otro caso no podemos saber a qué se invoca ni con qué parámetros ya sea por medio de microservicios o métodos de clases) los 4 bloques funcionales podrían dividirse en 4 proyectos/paquetes/jar y trabajar en paralelo cuatro equipos, exactamente igual que 4 equipos podrían trabajar en los 4 microservicios. La diferencia principal sería que unos invocarían a otros llamando a métodos y clases y otros invocando microservicios remotos.
1-B) “Los microservicios permiten a trabajar en diversos lenguajes y tecnologías”.
Eso es cierto, aunque relativamente. Si necesitas invocar a otras tecnologías (ejemplo desde Java a servicios .Net) siempre puede utilizarse invocaciones REST, servicios Web o incluso JNI (https://es.wikipedia.org/wiki/Java_Native_Interface), aunque desde luego no siempre están disponibles todos los productos en esa forma. Además, para los principales lenguajes hay miles de librerías y APIs de productos.
Por último, el tener un equipo especializado en diversas tecnologías/lenguajes implica que si hay que mover técnicos de un servicio/bloque a otro puede no ser posible. Un equipo uniforme es más elástico.
1-C) “Los microservicios permiten tener BB.DD independientes para cada microservicio”.
Tradicionalmente las BB.DD. han incluido dos características vitales: La transaccionalidad (que nos permite realizar una serie de operaciones y, si todo es correcto, confirmarlas de forma que se haga todo o nada) y la integridad relacional (que asegura que no pueda referenciarse valores o códigos que no existen). Cuando se utiliza una BB.DD. con un modelo “completo”, la transaccionalidad e integridad está asegurada.
Si cada conjunto de tablas está separado y las operaciones se realizan invocando servicios, deberá gestionarse manualmente escenarios como que tras invocar una actualización o inserción de varias tablas, cuando uno de los servicios notifique que no se ha podido realizar la operación, deba deshacerse la operación. Para ello deberá implementarse en cada servicio una serie de métodos para deshacer cada una de las posibles operaciones del servicio (es decir, grosso modo se multiplica los métodos/servicios expuestos por 2).
Similarmente, debe gestionarse la integridad, es decir asegurar que cuando se inserta un registro, los valores referenciados (códigos de país, producto, cliente,…) existen o que antes de borrar se compruebe que nadie lo referencia. Esto no es trivial ya que, en situaciones de mucha concurrencia, entre la comprobación de existencia y la inserción, otra tarea puede borrar el registro referenciado, generando inconsistencias.
Veamos ahora los inconvenientes durante el desarrollo que en ocasiones se olvidan de los microservicios:
1-D) Control de tipos de parámetros: Cuando la comunicación entre bloques se hace utilizando un lenguaje (Java en este ejemplo), los cambios o control de tipo de dato (cadena, fecha, entero, etc.) se detectan en tiempo de compilación y son explícitos en la propia definición de los método y clases publicados. En los microservicios el intercambio de datos se hace en modo texto, por lo que debe convertirse todos los parámetros a texto y volverse a convertir al tipo físico al recibirlos. Esto no solo implica una sobrecarga de desarrollo sino un riesgo de errores, al poder depender de elementos locales a cada servidor como código de páginas (UTF-Windows 1252), formatos de fechas (dd/mm/aaaa, mm-dd-aaaa,…), decimales (123,4 – 123.4), etc.
1-E) Encapsulación: Cuando la comunicación entre bloques se hace utilizando un lenguaje (Java en este ejemplo), podemos recibir un objeto de un bloque/método, y pasarlo a otro sin necesidad de conocer la estructura, ya que la orientación a objetos nos asegura la encapsulación. Por ejemplo el bloque funcional de Contratación puede devolver en un método un objeto de clase Pedido y ese objeto pedido se transmite completo al bloque Almacen: Almacen.ActualizaInventario(Pedido1). Si se cambia la estructura (y el modelo de datos) de la clase Pedido, solo debe modificarse los componentes realmente afectados, no los intermedios. Con un modelo de MicroServicios, muchos elementos intermedios tendrían que modificarse para transmitir o leer todos los elementos de un Pedido. Esta encapsulación también afecta a otros aspectos de implementación y rendimiento. Por ejemplo, una invocación a un método Pedido.getItems() que devuelva la lista de elementos, puede implementarse para que devuelva a lista desde memoria (si estaban cargados previamente) o busque en la BB.DD. si no lo estaban). Con microservicios sería necesaria la invocación explícita al otro microservicio.
1-F) Sobrecarga de comunicaciones: La invocación a una clase o método dentro de un lenguaje es trivial. La invocación a otro microservicio, implica un código adicional (ya sea implementado directamente o utilizando un framework) que debe gestionar la conversión y emparejamiento de parámetros, la gestión de comunicaciones y timeout, reintentos, resolución de urls, etc.
1-G) Control de parámetros: Cuando la comunicación entre bloques se hace utilizando un lenguaje, si un método de un bloque funcional que admitía 2 parámetros se modifica para utilizar 3, esos cambios se detectan y saltan en tiempo de compilación. En el caso de los microservicios, hasta que no se invoque no se detecta el error.
2- Despliegue e infraestructura.
2-A) “Los microservicios son más ligeros”
Si repasamos las necesidades (obviando la alta disponibilidad, que implica duplicar todo en cualquiera de las dos arquitecturas), vemos que la aplicación monolítica requiere 1 sistema operativo + 1 máquina Java + servidor de aplicaciones + librerías comunes y de utilidad (driver jdbc, log4j, manejo XML, etc.) + cuatro módulos, lo que puede representarse como:
Bloque/Servicio A |
Bloque/Servicio B |
Bloque/Servicio C |
Bloque/Servicio D |
Librerías comunes |
Servidor Aplicaciones |
JVM |
Sistema Operativo |
La versión basada en microservicios requerirá 4 * [ 1 sistema operativo + 1 máquina Java + servidor de aplicaciones + librerías comunes y de utilidad (driver jdbc, log4j, manejo XML, etc.)+ 1 módulo + elementos adicionales comentados (comunicaciones, formateo y comprobación datos entrada/salida, cancelación transacciones) ], por lo que podría representarse:
Bloque/Servicio A | Bloque/Servicio B | Bloque/Servicio C | Bloque/Servicio D | |||
Librerías comunes | Librerías comunes | Librerías comunes | Librerías comunes | |||
Servidor Aplicaciones | Servidor Aplicaciones | Servidor Aplicaciones | Servidor Aplicaciones | |||
JVM | JVM | JVM | JVM | |||
Sistema Operativo | Sistema Operativo | Sistema Operativo | Sistema Operativo |
Es decir, en el caso muy simplificado de que todos los bloques fueran iguales (caso no real, ya que el S.O., JVM y servidor de aplicaciones son mucho más grandes que los bloques funcionales) se trata de 8 bloques/unidades de memoria (y puede equipararse a consumo de CPU para procesar y manejar esa información), frente a 24 bloques, es decir 3 veces más de infraestructura necesaria (en memoria y CPU).
Si estuviéramos hablando de, por ejemplo 10 microservicios/bloques funcionales, la comparación sería de 14 frente a 60 “unidades”.
Y si consideramos tamaños diferentes de cada bloque y un poco más reales de sistema operativo (incluso en sistemas Docker), tamaños de JVM y servidor de aplicaciones (servidores de aplicaciones miniatura como Jetty) frente a tamaño de microservicios, incluso en el caso de 4 bloques, la comparación es de:
Bloque/Servicio A | 20M |
Bloque/Servicio B | 20M |
Bloque/Servicio C | 20M |
Bloque/Servicio D | 20M |
Librerías comunes | 50M |
Servidor Aplicaciones | 100M |
JVM | 100M |
Sistema Operativo | 500M |
830M frente a 3160M o en el caso de 10 microservicios, 950M frente a 7.900M es decir un coste en infraestructura entre 4 y 8 veces mayor (y con otros tamaños de S.O y servidor de aplicaciones, incluso 10 o 15 veces mayor).
2-B) “Los microservicios son más fácilmente escalables”
Suele argumentarse que en el caso de los microservicios el escalado es más fácil, ya que puede desplegarse más instancias del servicio más usado. Es decir si el bloque A es invocado más veces que el resto, podría desplegarse dos instancias/instalaciones del microservicio A y una para cada una del resto, es decir algo como:
Bloque/Servicio A
|
Bloque/Servicio A
|
Bloque/Servicio B
|
Bloque/Servicio C
|
Bloque/Servicio D
|
Sin entrar en los costes, ya revisados, hay algo sorprendente en la afirmación. Si se despliega un servicio/bloque en un servidor, ese servidor únicamente puede ejecutar esa tarea. Es decir una instalación dimensionada para atender 4 peticiones simultáneas de servicios A, solo podrá atender esas 4 peticiones:
A
|
A
|
B
|
B
|
C
|
C
|
D
|
D
| |||
A
|
A
|
B
|
D
|
D
|
Si se recibe 6 peticiones de tipo A o 5 de tipo D, no podrán atenderse
Un servidor donde se haya desplegado todos los paquetes, dimensionado para atender 16 peticiones, atenderá cualquier petición de cualquier tipo (A, B, C o D) hasta su máximo de recursos
A | A | A | B |
A | A | A | B |
B | C | C | D |
D | D | D | D |
O lo que de forma doméstica puede expresarse como “en un cajón grande tienes más flexibilidad que en 4 cajones pequeños”. Por supuesto que pueden desplegarse más instancias para atender al servicio A, pero en la opción monolítica no sería necesario, al margen de los costes de infraestructura antes citados.
2-C) “En caso de caída de un microservicio, el resto puede seguir funcionando”
Me sorprende que tras años de existencia de sistemas de tolerancia a fallos, granjas de servidores, tecnologías cluster, alta disponibilidad Activo/Activo y Activo/pasivo, y disponibilidades predicadas del 99, 99% del tiempo, se plantee que se puede “no disponer” de una aplicación. Pero incluso si fuera el caso, plantear que “si se cae un microservicio el resto siguen funcionando” es algo bastante discutible. Si se trata, por ejemplo, de una contratación y no se puede acceder al servicio de “clientes”, o al servicio de “almacén”, o la “pasarela de pago”, no podrá realizarse esa contratación. La aplicación tiene generalmente una coherencia y unos servicios interconectados. Si uno de ellos se cae, probablemente no pueda realizarse la mayor parte de las operaciones, y en muchos casos, ninguna.
2-D) “Puede desplegarse nuevas versiones de los microservicios de forma independiente”
Habría que distinguir lo primero a que llamamos “nuevas versiones”. Si se trata de desplegar una corrección de un error o una optimización, por supuesto puede hacerse de forma independiente, al igual que puede hacerse también en una aplicación monolítica sustituyendo el fichero java jar correspondiente. Si se trata de una NUEVA versión, que expone un “contrato” nuevo (con diferentes parámetros, un comportamiento distinto, métodos adicionales,..) desde luego deberá desplegarse y probarse de forma conjunta. En otro caso, se está invocando a operaciones cuyo comportamiento es distinto (aunque no lo sea el “contrato” expuesto). El concepto de “pruebas de integración” existe hace mucho años, tanto en aplicaciones monolíticas como en las que no lo son, debido a que en cuanto hay que integrar varios sistemas, existen riesgos y comportamiento no perfectamente documentados (autenticación entre sistemas, códigos de página, tamaños de bloque, límites en cuanto a número de parámetros. Comportamiento ante excepciones, timeout, etc.)
2-E) Comunicaciones: Por último, hay algo que parece olvidarse, la comunicación entre servidores y microservicios tiene un coste de infraestructura, no es gratis. Y si se trata de un entorno en nube, podemos incluso consultar los precios de transferencias internas y externas (https://aws.amazon.com/es/ec2/pricing/on-demand/). Por tanto el uso de microservicios tiene además ese sobrecoste por el hecho de estar intercambiando información entre servidores que no se produce en la opción monolítica.
3- Administración Monitorización
Respecto a este punto, podría revisarse 3 aspectos:
A) Instalación y actualización (de Sistema Operativo, servidor de aplicaciones, utilidades y por supuesto aplicación).
B) Monitorización (debe controlarse el estado de memoria, CPU y disco de los servidores, así como el estado del servidor de aplicaciones y de la aplicación en sí).
C) Depuración y monitorización del proceso/BAM (es decir tanto depurar y analizar problemas de la aplicación como analizar en tiempo real el estado de un proceso de negocio.
3-A) En el primer caso, parece evidente que, de instalar y configurar un servidor (virtual o no) y una aplicación, hemos multiplicado el trabajo por el número de microservicios del proyecto (por supuesto la alta disponibilidad multiplica en ambos casos por dos). Lo mismo aplica para las actualizaciones y parches de seguridad o fixpack de microservicios, librerías o S.O. Desde luego, con plataformas como Docker esto se simplifica, y el número de tareas manuales se reduce, pero esto también aplica a la opción monolítica. El hecho es que sean muchas o pocas las tareas y pruebas de regresión, hay que multiplicarlas por el número de microservicios.
3-B) La monitorización de los servidores y la infraestructura (que siempre es necesaria para controlar problemas de lentitud, memoria o disco) se multiplica igualmente por el número de microservicios desplegados. No me refiero solo a caídas del sistema (que es casi lo más automatizable) sino verificar que los tiempos de respuesta son adecuados, que el nivel de uso de la CPU no sea del 99% o que la memoria libre de RAM o la de almacenamiento estén por encima de unos valores.
3-C) En cuanto a la depuración, desde luego el analizar un problema recurriendo a 4 (o N) ficheros de traza “paralelos” es mucho más complicado que disponer de un solo fichero donde se vea la secuencia y la lógica de las distintas operaciones. En cuanto a la monitorización de procesos, a nivel funcional y de cuadro de mando, también se complica más al contar con 4 (o N) elementos separados.
Conclusión.
La conclusión no es que los microservicios no sean una solución válida, la conclusión es que no son LA SOLUCIÓN a aplicar automáticamente en todos los casos ni son siempre la mejor opción.
Son una opción más a aplicar cuando las características del proyecto sean las adecuadas, a comparar en pie de igualdad con muchas otras, incluyendo, por supuesto, un desarrollo “monolítico”.