El costo de los microservicios (2020)
(robertovitillo.com)- El backend suele empezar como un monolito, pero cuando varios equipos de funcionalidades comienzan a tocar la misma base de código, el acoplamiento puede aumentar y la velocidad de cambio puede disminuir
- Los microservicios consisten en dividir los servicios por función de negocio para obtener despliegue independiente y autonomía para equipos pequeños
- Si los servicios se vuelven demasiado pequeños, la carga operativa crece más que los beneficios, y el nombre “micro” incluso puede nublar el juicio
- Una adopción total trae consigo un amplio costo: estandarización, aprovisionamiento de recursos, defensas para llamadas remotas, CI/CD, operación e incluso consistencia eventual
- Por lo general, es más seguro aprender bien los límites dentro del monolito y luego separar componentes uno por uno cuando los dolores de crecimiento se vuelven grandes; un enfoque de microservicios primero solo conviene cuando ya existe experiencia y preparación de plataforma
Los límites que aparecen cuando crece el monolito
- El backend de una aplicación moderna de JavaScript de página única normalmente comienza como un único servicio web sin estado que ofrece una API HTTP RESTful y usa una base de datos relacional
- Este servicio está compuesto por componentes o bibliotecas que implementan distintas funciones de negocio
- A medida que aumenta la cantidad de equipos de funcionalidades que participan en la misma base de código, crece el acoplamiento entre componentes y se vuelve más fácil que las áreas de trabajo entre equipos se traslapen
- La productividad baja y se vuelve difícil predecir el alcance del impacto de un cambio
- Cuando la base de código se vuelve lo suficientemente compleja, nadie logra entenderla por completo, así que implementar nuevas funciones y corregir errores toma más tiempo
- Aunque el backend esté dividido en bibliotecas por equipo, los cambios en una biblioteca igual terminan llevando a volver a desplegar el servicio
- Si entra un bug como una fuga de memoria, puede afectar a todo el servicio
- Si se revierte una compilación equivocada, eso afecta la velocidad no solo del equipo que introdujo el bug, sino de todos los equipos
La autonomía que dan los límites de servicio
- Una forma de reducir los dolores de crecimiento de un backend monolítico es dividirlo en un conjunto de servicios desplegables de forma independiente
- Los servicios se comunican mediante APIs, y estas crean límites más difíciles de invadir que los límites entre componentes dentro del mismo proceso
- A esta arquitectura se le llama arquitectura de microservicios, pero la expresión
micropuede prestarse a confusión- Los servicios no necesariamente tienen que ser pequeños
- Un servicio con demasiado poca funcionalidad puede generar más carga operativa que beneficios
- Un nombre más apropiado podría ser service-oriented architecture, aunque ese nombre también arrastra cargas históricas
La velocidad que crean los equipos pequeños y los servicios independientes
- Si se divide el backend en servicios por función de negocio, cada servicio puede ser desarrollado y operado por un solo equipo pequeño
- Los equipos pequeños son más efectivos porque el costo de comunicación aumenta al cuadrado según el tamaño del equipo
- Si cada equipo define su propio calendario de lanzamientos y controla su base de código, se reduce la coordinación entre equipos y las decisiones se toman más rápido
- Si la base de código del servicio es pequeña, a los desarrolladores les resulta más fácil entenderla y también se reduce el tiempo de adaptación de quienes recién ingresan
- Una base de código pequeña también ayuda a la productividad de los desarrolladores al reducir la lentitud del IDE
- Gracias a límites de servicio fuertes, un desarrollador solo necesita entender una parte pequeña del backend, y no todo, cuando cambia algo
- Cada servicio puede escalar de manera independiente y elegir el stack tecnológico que mejor se adapte a sus propias necesidades
- A quienes consumen la API no les importa cómo se implementa internamente la funcionalidad
- Es más fácil experimentar y evaluar nuevas tecnologías sin afectar otras áreas del sistema
- Cada microservicio puede tener su propio modelo de datos y su propio almacén de datos
- Los desarrolladores pueden cambiar el esquema sin afectar a otros servicios
Los costos reales que agregan los microservicios
- Los microservicios agregan muchas más piezas móviles a todo el sistema, y ese costo no es gratis
- El costo de adoptar microservicios por completo solo vale la pena cuando puede amortizarse de forma distribuida entre decenas de equipos de desarrollo
-
Experiencia de desarrollo
- Es posible usar distintos lenguajes, bibliotecas y almacenes de datos por servicio, pero eso puede dificultar el mantenimiento de la aplicación
- Si el stack de software es completamente distinto, a los desarrolladores les cuesta más moverse de un equipo a otro
- Para ofrecer funciones comunes necesarias en todos los servicios, como el logging, hay que mantener bibliotecas para cada lenguaje adoptado
- Se necesita cierto nivel de estandarización
- Se puede ofrecer una buena experiencia de desarrollo a los equipos que sigan un portafolio recomendado de lenguajes y tecnologías, fomentando así de manera flexible el uso de ciertas tecnologías
-
Aprovisionamiento de recursos
- Para soportar muchos servicios independientes, debe ser fácil crear servidores, almacenes de datos y otros recursos genéricos
- No se debe permitir que cada equipo invente su propia forma de crear recursos
- Los recursos aprovisionados luego tienen que reconfigurarse, y para manejar eso se necesita una cantidad considerable de automatización
-
Comunicación
- Las llamadas remotas tienen un costo alto e introducen nuevas formas en las que el sistema puede caerse
- Para prepararse ante fallas, se necesitan mecanismos de defensa como timeouts, reintentos y circuit breakers
- Para reducir la pérdida de rendimiento de la comunicación por red, también hay que usar procesamiento asíncrono y lotes
- Estos elementos aumentan la complejidad del sistema
- Un monolito también puede sufrir problemas parecidos por el acceso de clientes remotos y el uso de APIs de terceros, pero en una escala menor
-
Integración continua, entrega y despliegue
- La integración continua hace que los cambios de código se fusionen a la rama principal después de ejecutar compilaciones y pruebas automáticas
- Una vez fusionado un cambio de código, debe publicarse y desplegarse automáticamente en un entorno similar a producción
- En ese entorno deben ejecutarse pruebas de integración y pruebas end-to-end para verificar que el microservicio no rompa a otros servicios de los que depende
- Probar un microservicio individual no es más difícil que probar un monolito, pero las pruebas de integración de todos los microservicios son mucho más difíciles
- Cuando los servicios individuales interactúan, pueden aparecer comportamientos muy sutiles e inesperados
-
Operación
- A diferencia de un monolito, asignar un equipo de operaciones separado para cada equipo responsable de un servicio resulta mucho más costoso
- Normalmente, el equipo que desarrolla el servicio también se encarga de su guardia on-call
- En cada sprint, el equipo debe decidir qué priorizar entre el trabajo de desarrollo y la carga operativa, y ahí aparece fricción
- Depurar fallas del sistema también se vuelve más difícil
- No se puede levantar toda la aplicación en una máquina local y recorrerla paso a paso con un depurador
- Como hay más piezas móviles, también hay más formas de fallar
- El buen logging y el monitoreo se vuelven importantes en todos los niveles
-
Consistencia eventual
- Si se divide la aplicación en varios servicios, el modelo de datos deja de existir solo en un único almacén de datos
- Actualizar de forma atómica registros que están en distintos almacenes de datos y garantizar consistencia fuerte es lento, costoso y difícil de implementar
- Este tipo de arquitectura normalmente tiene que aceptar la consistencia eventual
Cuándo separar
- Dividir una aplicación en servicios aumenta de forma importante la complejidad de todo el sistema
- En general, conviene empezar con un monolito y dividirlo solo cuando haya una razón suficiente para hacerlo
- Como es difícil definir correctamente los límites entre servicios, suele ser más fácil mover esos límites dentro del monolito hasta encontrar el punto adecuado
- Cuando el monolito ya maduró lo suficiente y los dolores de crecimiento empiezan a hacerse grandes, se pueden ir extrayendo microservicios uno por uno
- Un enfoque de microservicios primero solo es adecuado si ya se tiene experiencia y si ya se construyó la plataforma necesaria, o si al menos se tomó en cuenta el tiempo que llevará construirla
1 comentarios
Comentarios de Hacker News
En general estoy de acuerdo con el texto, pero viéndolo al revés también hay casos en los que vale la pena asumir el costo: puedes separar como un servicio aparte los problemas de compatibilidad con dependencias de terceros antiguas para evitar refactorizaciones dolorosas o reimplementaciones propias; puedes hacer que las restricciones de despliegue de un sistema central de alta disponibilidad no bloqueen el despliegue de otros sistemas; y puedes aislar restricciones de diseño que un gran cliente no puede cambiar
No significa que sea gratis, sino que es un costo que puede valer la pena pagar para no convertir un monolito simple en un basurero, no sacudirlo con despliegues riesgosos, o no quedar atado a un socio de negocio con una pila tecnológica peor
¿No se supone que los microservicios son un enfoque donde los componentes internos se construyen por defecto como servicios en vez de bibliotecas o clases?
Si escribes un servicio de inferencia de aprendizaje automático en Python, una UI de renderizado del lado del servidor en Rails, y un servicio con mucha entrada/salida y concurrencia en Go, la carga adicional de separarlos en servicios distintos puede estar justificada
Me gustan los microservicios, pero la optimización prematura y el escalado prematuro pueden ser tan malos como la deuda técnica que aparece cuando después tienes que optimizar en una dirección completamente distinta
El momento de extraer algo como un servicio aparte es cuando aparece un problema que no puede resolverse de una forma manejable sin hacerlo
Aumentar la complejidad arquitectónica para imponer límites no puede ser la solución a la falta de disciplina organizacional, pero las empresas tecnológicas medianas lo tratan así una y otra vez. Si los problemas surgen porque los límites del dominio son débiles, es muy probable que extraer servicios tampoco los resuelva bien
Cita: https://www.oreilly.com/library/view/software-architects-han...
Incluso CI/CD, que bloquea los PR hasta que pasen las pruebas, es una solución técnica costosa para resolver un problema de disciplina organizacional
Todos aceptan que la resolución de nombres de dominio la haga un servicio externo, y casi nadie integra un resolvedor DNS recursivo y caché dentro de un monolito. Pero este tipo de separación de responsabilidades, que existe desde hace tanto tiempo, no suele aceptarse como ejemplo
El problema parece ser que, cuanto “mejor” es un empleado, más ganas tiene de demostrar valor, eso lleva al sobrediseño y al final todo se vuelve un infierno
Microservicios
grug se pregunta por qué un cerebro grande tomaría el problema más difícil de descomponer correctamente un sistema y además le metería llamadas de red
A grug le parece muy confuso
https://grugbrain.dev/#grug-on-microservices
“Queremos modernizar el enfoque de acceso a FOO_TABLE por motivos de SCALE_REASONS, pasar de MySQL a DynamoDB, pero 32 de 59 equipos acceden directamente a FOO_TABLE o llaman directamente métodos privados de nuestra clase. Como tienen prioridades distintas, esos equipos no pueden migrar a usar FOO_SERVICE ni cambiar la forma de consultar para usar tablas fragmentadas. Ahora escalar FOO_TABLE se vuelve un trabajo de varios trimestres, y hay que dejar que los equipos se actualicen lentamente. En 1 o 2 años quizá podamos retirar el enfoque antiguo que se está incendiando en este momento. Mientras tanto, disfruta las guardias”
Con microservicios, el equipo nota que la tabla no escala, pero los datos se exponen por API. Planifican y ejecutan la migración en el siguiente sprint, y los usuarios de la API sienten que todo va mucho más rápido
Si abres el REPL de Ruby moderno, puedes obtener buena parte de lo que esperarías de un IDE para lenguaje con tipado estático en términos de autocompletado y revisión de sintaxis
El aislamiento no debe tomarse a la ligera; es una de las capacidades más poderosas que se pueden incorporar al software. Puede mejorar el rendimiento, crear límites de falla y proporcionar límites de seguridad
Es el mismo concepto base del modelo de actores. En vez de que dos componentes compartan y modifiquen la memoria del otro, dos sistemas aislados como actores o microservicios solo se comunican mediante un protocolo definido
Creo que la gente suele entender mal la modularidad. La modularidad es importante, pero he llegado a pensar que hay otro principio arquitectónico importante: el principio del remolino único
En un sistema de software, un remolino es un bucle en el flujo de datos. Si envías datos a algún lugar y luego recibes de vuelta el resultado procesado, o si de alguna manera te ves afectado por esos datos, hay un remolino. Una variable mutable es un ejemplo de un remolino muy pequeño
El principio del remolino único significa que, idealmente, un sistema de software debería tener un solo remolino o, dicho de otro modo, que todos los componentes deberían saber en qué dirección gira su remolino
Cuando unes en uno los módulos que forman dos remolinos, si las direcciones coinciden la composición es fácil; si van en direcciones opuestas, se vuelve complicado porque hay que decidir cuál será la nueva dirección del remolino. Por eso, lo mejor es que los remolinos de todos los módulos vayan en la misma dirección y formen un solo remolino
Este principio es una generalización de ideas como el patrón Flux, CQRS, event sourcing e inmutabilidad
O también puede pasar algo inesperado en A y bloquear a C. Idealmente, el padre debería llamar solo a sus hijos y no depender del padre; si eso falla, la arquitectura también falló
Al ver un diagrama de arquitectura, la mitad de la batalla es entender qué es asíncrono o síncrono, qué es push o pull, y cuál es la dirección y el orden del flujo
El enterprise service bus y las colas de mensajes parecen una solución para crear remolinos donde las direcciones de ambos lados no coinciden
Hay una parte que dice que el problema es la “cantidad de bibliotecas que hay que soportar, una por cada lenguaje adoptado, para ofrecer funciones comunes necesarias en todos los servicios, como el logging”
Esa fue la principal razón por la que nosotros abandonamos los microservicios. Con las herramientas de 2023, especialmente en entornos puramente cloud-native o FaaS de escalado infinito, gastar ancho de banda mental en esa preocupación es un desperdicio total. Más todavía cuando tu base de clientes incluye bancos e instituciones financieras, que revisan con brutal detalle cada dependencia de terceros
Ahora operamos con una sola distribución binaria monolítica de .NET de unos 250MB comprimida con gzip, y no hay ni una sola señal de grietas. Si tienes una distribución SaaS de 10~100MB y te estás preocupando meticulosamente por cosas como “mi exe ya no cabe en L2”, puedes quedarte tranquilo. Tu viaje con el software monolítico ni siquiera ha empezado
Si llegara el momento de reescribir uno de estos montones de basura, ¿no sería mucho más fácil que todos los commits estuvieran en un solo lugar y fueran globalmente consistentes?
Tener microservicios no significa que todo sea tierra de nadie y que cada equipo pueda actuar sin considerar al resto de la organización. Puede y debe haber reglas para mantener cierto grado de consistencia entre equipos
El mejor código es el que se puede tirar y reescribir fácilmente. En la práctica, el código viejo pasa por más manos y se vuelve peor y, sobre todo, nadie quiere mantenerlo
Quienes prefieren los microservicios dicen que es más fácil cambiar o reescribir porque hay un contrato claro sobre cómo debe funcionar el servicio y porque el codebase de ese servicio es mucho más pequeño
Quienes prefieren el monolito dicen que es más fácil cambiar o reescribir porque todo es una gran maraña de hilos, así que para cambiar un hilo solo necesitas saber dónde se cruza con los demás
No sé quién tiene razón. Para alguien que lleva años mirando el codebase con lupa, probablemente el monolito sea mejor; para los demás, probablemente los microservicios
Desde la perspectiva de la gestión de releases, pasar de un monolito a microservicios muchas veces se hace por las razones equivocadas
La única razón realmente válida para cambiar parece ser un problema de escalabilidad causado por un cuello de botella de rendimiento. Todo lo demás no es más que trasladar la complejidad del desarrollo de software al mantenimiento del sistema
Claro, a los desarrolladores les va a gustar porque reduce mucho la carga de tener que coordinarse con otros equipos. Pero dejar claro cuándo y cómo se implementa una funcionalidad, cómo se prueba correctamente a través de todos los microservicios, cómo se activa en producción y cómo se gestiona de forma centralizada se vuelve mucho más difícil si la comunicación entre equipos de desarrollo no es lo bastante madura. Y muchas veces esa falta de madurez es justamente la razón real por la que se termina rompiendo el monolito
En cambio, con pocos desarrolladores y un contexto bien definido, lidiar con varios microservicios es un dolor
La frase “todo lo demás no es más que trasladar la complejidad del desarrollo de software al mantenimiento del sistema” suena razonable si el software está en desarrollo activo. Desarrollar es caro. Puede que el costo de mantener un sistema distribuido para un equipo grande sea menor que el de desarrollar un monolito enorme; al final, depende de la situación
¿Dónde está el texto de “el costo del monolito”? Aquí no se ve. Lo único que pasa es que todo el mundo está implementando microservicios tontamente, así que solo se ve ese problema.
Si todo el mundo estuviera implementando monolitos tontamente, veríamos una enorme cantidad de textos de “los monolitos son malos”.
La gente no entiende que ambos sistemas terminan generando la misma cantidad de problemas. Es la diferencia entre poner a trabajar a un elefante o a 1000 ratones. Ambos tienen problemas; solo que son problemas distintos.
Elegir uno de los dos no hace que los problemas desaparezcan. Habrá problemas de cualquier manera, y hay que elegir uno y darle solución. Si la mayor barrera en una empresa es “qué arquitectura usar”, me gustaría trabajar ahí.
Si un monolito está bien descompuesto, ¿en qué se diferencia de un microservicio desplegado en la misma ubicación? Probablemente solo en la interfaz. Una llamada de función se vuelve RPC, y se aceptan ciertas ventajas de tratar los componentes por separado, por ejemplo un poco de sobrecarga para aplicar parches.
¿Y en qué se diferencia un microservicio distribuido de uno ubicado en la misma posición? El despliegue es más complejo, pero se pueden ubicar procesos de forma inteligente en hardware más adecuado. Aumentan los modos de falla, pero se obtiene mayor tolerancia a fallos.
Aquí no hay una respuesta universal. Si necesitas los beneficios, pagas el costo. Creo que este debate de microservicios vs. monolitos surge de aplicar mal algún patrón, de no tener herramientas que te faciliten la vida o, la mayoría de las veces, de seguir arrastrando software de 10 años sin refactorizarlo ni rediseñarlo, hasta dejarlo doloroso de mantener sin importar la arquitectura inicial.
Decir que “una llamada de función se vuelve RPC” suena simple, pero en el momento en que eso pasa, la app cliente tiene que manejar rutas de ejecución como: no se puede acceder al servidor DNS, falla la resolución del hostname, no se puede acceder al host, conexión rechazada, autenticación rechazada, certificado TLS no confiable, se excedió el rate limit, 301 Moved Permanently, resultado aún no listo, timeout, etc.
Incluso una app bien descompuesta probablemente necesite rediseñarse con colas asíncronas, limitación de tasa de solicitudes, caché de resultados, reintentos con backoff de registro, credenciales de cliente / trust store / URLs de recursos configurables, y registro de fallas, para manejar todo esto con solidez.
También hacen falta funciones RPC y parámetros adicionales para proveer datos que antes estaban disponibles en el contexto de una llamada local. Además, la UI del monolito podría tener que exponer al usuario latencias y fallas de red que antes no existían.
Incluso el mejor monolito requiere un esfuerzo considerable para refactorizarse hacia microservicios.
¿Se puede reintentar esta llamada RPC de forma segura? ¿Con backoff exponencial? ¿El load balancer de la API realmente funciona bien? Entonces también hace falta monitoreo. ¿Cómo se hará el tracing entre microservicios? Ahora ya necesitas algo como OpenTelemetry.
¿Qué tan más difícil es depurar con breakpoints en una arquitectura de microservicios? ¿Cómo se revierte una transacción de base de datos entre dos microservicios? Una simple llamada de función de pronto se vuelve muchísimo más compleja.
Cada vez que se defiende esta idea, se deja fuera del cálculo la fragmentación que produce exigir que los servicios sean muy pequeños. Más concretamente, si tienes un monolito y un microservicio, eso no son microservicios. El término microservicios implica que todo está dividido en servicios pequeños y que no hay monolito.
La mayor parte de la argumentación a favor de los microservicios se derrumba en cuanto te das cuenta de que necesariamente tienen que ser “micro”. Y si abandonas ese requisito, no queda nada nuevo ni profundo en la propuesta.
Desde la perspectiva operativa, decir que “el despliegue es más complejo” es un eufemismo enorme. Cada paquete es una cosa aparte que hay que administrar, con sus propias características y problemas.
Algo parecido a cuando desarrolladores frontend creen que no pueden hacer una “web app moderna” sin frameworks o librerías frontend como React/Vue/Svelte: hoy parece circular la idea de que “monolito” equivale a una enorme masa de alquitrán aterradora corriendo como una sola instancia, y por eso “no escala”, pero eso no tiene sentido.
Otra cosa que se ve es que la cantidad total de código crece muchísimo, y que la mayoría de los servicios termina siendo 20% código de negocio/dominio y 80% manejo de envío y recepción de mensajes por red. Por más que lo escondas, eso sigue ahí, y de una forma u otra hay que lidiar con la red.
Igual que con la locura del frontend, este culto de los microservicios solo va a terminar cuando la economía se hunda y ya no haya dinero para sostener estas torres de Babel condenadas.
Dicho eso, los microservicios sí tienen su lugar. Son adecuados dentro de algunas empresas donde aportan más valor que costo.
Hay ventajas técnicas, como escalar servicios de forma independiente, pero el mayor beneficio es tener límites claros de ownership y evitar acoplamientos excesivos e innecesarios.
Si un equipo pequeño está construyendo un solo producto, es muy posible que todavía no tenga los problemas organizacionales que los microservicios resuelven, así que quizá no los necesite. En ese caso, los microservicios son optimización prematura, y se está pagando un costo para resolver un problema que aún no existe.
Más aún, toda la complejidad del frontend está en la cabeza de la gente: https://bower.sh/front-end-complexity
El texto parece partir de la premisa de que la organización de desarrollo está completamente rota y fuera de control. Que no pueden decidir qué hacer durante el sprint, que meten a escondidas librerías de terceros y lenguajes desconocidos, y que despliegan silenciosamente a producción cambios incompatibles
Si los desarrolladores no fueran tontos, parece que los microservicios también serían más fáciles
Estoy seguro de que, si trabajas en un equipo formado por desarrolladores inteligentes y capaces, los microservicios serán más fáciles. Claro, en ese caso todo sería más fácil