El enfoque más productivo para el diseño de programas / Sudo Null IT News

Cómo y por qué Shopify pasó de una arquitectura monolítica a una arquitectura modular-monolítica.

imagen

Shopify tiene una de las bases de códigos Ruby on Rails más grandes. Más de mil desarrolladores trabajaron en él durante más de diez años. Incluye muchas funciones diferentes, como facturación a comerciantes, administración de aplicaciones de terceros, actualización de información del producto, procesamiento de envíos, etc.

El sistema se construyó originalmente como un monolito, lo que significa que todas estas funcionalidades diferentes se integraron en una base de código sin ninguna demarcación entre ellas. Durante muchos años esta arquitectura funcionó bien, pero finalmente llegamos a un punto en el que las desventajas del monolito superaron las ventajas. Tuvimos que tomar una decisión sobre cómo proceder.

En los últimos años, los microservicios han ganado popularidad y están siendo aclamados como una solución integral a todos los problemas que surgen al utilizar monolitos.
Sin embargo, nuestra propia experiencia colectiva nos ha dicho que no existe una solución única para todos.y los microservicios plantean sus propios desafíos. Decidimos hacer de Shopify un monolito modular, lo que significa que mantendríamos todo el código en una base de código pero nos aseguraríamos de que los límites entre los diferentes componentes estuvieran definidos y respetados.

Cualquier arquitectura de software tiene sus propios pros y contras. Dependiendo de en qué etapa de desarrollo se encuentre una aplicación, tendrá sentido utilizar diferentes soluciones. Pasar de un monolito a un monolito modular fue el siguiente paso lógico para nosotros.

Arquitectura monolítica

Según Wikipedia, un monolito es un sistema de software en el que aspectos funcionalmente distintos se entrelazan en lugar de contener componentes arquitectónicamente distintos. En el caso de Shopify, esto significaba que el código que manejaba los cálculos de los costos de envío vivía al lado del código que manejaba el pago, sin que nada impidiera que se llamaran entre sí. Con el tiempo, esto llevó a que el código excesivamente acoplado manejara diferentes procesos comerciales.

Ventajas de los sistemas monolíticos.

La arquitectura monolítica es la más fácil de implementar. Si no aplica ninguna arquitectura, lo más probable es que el resultado sea un monolito. Esto es especialmente cierto para el marco Ruby on Rails, que fomenta la creación de monolitos debido a la disponibilidad global de todo el código a nivel de aplicación. Una arquitectura monolítica puede llevar una aplicación muy lejos porque es fácil de desarrollar e inicialmente permite que los equipos progresen muy rápidamente y, por lo tanto, hagan llegar su producto a los clientes antes.

Si mantiene todo su código base en un solo lugar y la implementación de su aplicación en un solo lugar, obtendrá muchos beneficios inmediatos. Solo necesita mantener un repositorio y puede buscar y encontrar fácilmente todas las funciones en una carpeta. También será suficiente mantener solo un proceso de prueba e implementación, lo que, dependiendo de la complejidad de su aplicación, puede ahorrarle muchos gastos generales. Crear, configurar y mantener estas canalizaciones puede ser una tarea costosa porque deben mantenerse deliberadamente consistentes. Dado que todo el código se implementa en una aplicación, todos los datos se pueden almacenar en una base de datos común. Si necesita extraer un dato en particular, simplemente haga una consulta simple a la base de datos.

Debido a que los monolitos se implementan en una ubicación, solo es necesario gestionar un conjunto de infraestructura. La mayoría de las aplicaciones Ruby vienen con una base de datos, un servidor web, la capacidad de ejecutar trabajos en segundo plano y, a menudo, otros componentes de infraestructura como Redis, Kafka, Elasticsearch, etc. Cada bloque de infraestructura adicional significa más trabajo para usted en una función de DevOps en lugar de una rol de arquitecto. Una infraestructura adicional significa más puntos posibles de falla. En este caso, la tolerancia a fallos y la seguridad de sus aplicaciones se deterioran.

Una de las mayores ventajas de una arquitectura monolítica sobre múltiples servicios separados es que puede acceder a diferentes componentes directamente en lugar de a través de API de servicios web. Esto significa que no tiene que preocuparse por el control de versiones de la API, la compatibilidad con versiones anteriores o posibles retrasos.

Desventajas de los sistemas monolíticos.

Sin embargo, si una aplicación alcanza una determinada escala, o el equipo que la construye alcanza una determinada escala, eventualmente superará la arquitectura monolítica. Esto sucedió en Shopify en 2016 y se manifestó en la complejidad cada vez mayor de crear y probar nuevas funciones. Un par de cosas en particular fueron señales de alerta para nosotros.

La aplicación era muy frágil y el nuevo código tuvo consecuencias inesperadas. Un cambio que a primera vista parecía bastante inofensivo podría provocar una cascada de diversos fallos en las pruebas. Por ejemplo, si el código que calcula las tarifas de envío se denomina código que calcula las tasas impositivas, realizar cambios en la forma en que se calculan las tasas impositivas podría afectar el resultado del cálculo de las tarifas de envío, pero podría no ser obvio por qué. Esto se debió al fuerte acoplamiento y la falta de límites, lo que también resultó en pruebas difíciles de escribir y muy lentas de ejecutar durante la integración continua.

El desarrollo en Shopify requirió tener en cuenta un contexto amplio para realizar cambios aparentemente simples. Cuando los nuevos empleados de Shopify se unieron al equipo y conocieron el código base, se les pidió que absorbieran una gran cantidad de información antes de comenzar a trabajar. Por ejemplo, un nuevo desarrollador que se uniera al equipo de entrega solo necesitaba comprender la implementación de la lógica empresarial de entrega. Sin embargo, en realidad, un recién llegado también tenía que entender cómo se crean los pedidos, como sabemos. procesamos pagos y mucho más, ya que todo estaba estrechamente interconectado. Es demasiada información para tenerla en mente solo para enviar su primer artículo. En aplicaciones monolíticas complejas, la curva de aprendizaje es pronunciada.

Todos los problemas que encontramos surgieron directamente de la falta de límites entre las diferentes áreas funcionales de nuestro código. Estaba claro que era necesario aflojar las conexiones entre las diferentes áreas, pero la cuestión era cómo hacerlo.

Arquitectura de microservicio

Una solución que actualmente goza de gran popularidad en la industria son los microservicios. La arquitectura de microservicios es un enfoque para el desarrollo de aplicaciones en el que una aplicación grande se construye como una colección de pequeños servicios que se implementan de forma independiente unos de otros. Si bien los microservicios podrían resolver los problemas que enfrentamos, introducirían un conjunto de problemas completamente diferente.

Tendríamos que admitir varios procesos de prueba e implementación diferentes, incurrir en costos de infraestructura para cada servicio y no siempre tener acceso a los datos correctos en el momento adecuado. Debido a que cada servicio se implementa independientemente de los demás, la comunicación entre servicios significa cruzar la red, lo que aumenta la latencia y reduce la confiabilidad en cada llamada. Además, las grandes refactorizaciones que afectan a múltiples servicios pueden resultar tediosas y requieren cambios en todos los servicios dependientes y en los mecanismos de coordinación de la implementación.

monolitos modulares

Necesitábamos una solución que aumentara la modularidad sin aumentar la cantidad de unidades implementadas, lo que nos permitiera obtener los beneficios de los monolitos y los microservicios sin sus desventajas.

imagen

Monolito vs microservicios por Simon Brown.

Un monolito modular es un sistema en el que todo el código se ejecuta como una única aplicación y existen límites estrictamente definidos entre los diferentes dominios.

Implementación de un monolito modular en Shopify: componenteización

Cuando quedó claro que habíamos superado la estructura monolítica y que estaba afectando el rendimiento, se envió una encuesta a todos los desarrolladores que trabajaban en nuestro sistema central para identificar los puntos débiles clave. Sabíamos que teníamos un problema, pero al desarrollar una solución, queríamos basarnos en datos para que realmente resolviera el problema y no fuera solo anecdótico.

Según los resultados de esta encuesta, se decidió dividir nuestra base de código. A principios de 2017, se formó un equipo pequeño pero fuerte para resolver este problema. El proyecto originalmente se llamaba “Romper el núcleo en varias piezas” y, con el tiempo, cambiamos el nombre de este proceso a “componentización”.

Organización del código

El primer problema que nos propusimos resolver fue la organización del código. Por el momento nuestro código es fue diseñado como una aplicación Rails típica: por conceptos de software (modelos, vistas, controladores). El objetivo era reorganizarlo en torno a conceptos del mundo real (como pedidos, envíos, inventario y facturación) para que fuera más fácil encontrar el código, encontrar personas que comprendan el código y comprender las partes individuales por su cuenta. Cada componente se estructurará como su propia miniaplicación sobre rieles, con el objetivo de denominarlos eventualmente como módulos Ruby. Esperábamos que la nueva organización resaltara áreas que estaban innecesariamente conectadas.

imagen

Reorganización realista: antes y después.

La compilación de la lista de componentes inicial requirió mucha investigación y aportes de las partes interesadas de toda la empresa. Hicimos esto enumerando todas las clases de Ruby (alrededor de 6000 en total) en una extensa hoja de cálculo y anotando manualmente a qué componente pertenecía. Aunque no se cambió ningún código durante este proceso, el trabajo aún afectó a todo el código base y era potencialmente muy riesgoso si hacíamos algo mal. Logramos esto con una gran solicitud de extracción creada mediante scripts automatizados.

Dado que los cambios que hicimos se limitaron a mover archivos, podrían ocurrir fallas potenciales porque nuestro código no “sabía” dónde encontrar las definiciones de objetos, lo que provocaría errores en tiempo de ejecución. Nuestro código base está bien probado, por lo que al ejecutar nuestras pruebas localmente y en CI sin fallas, y al ejecutar la mayor cantidad de funcionalidad posible localmente y en el entorno de prueba, pudimos asegurarnos de que no se perdiera nada. Decidimos hacer todo esto en un solo PR para molestar lo menos posible a los desarrolladores. Desafortunadamente, como resultado de este cambio, perdimos una gran parte del historial de Git en Github cuando los movimientos de archivos se contaron incorrectamente como eliminaciones y creaciones en lugar de cambios de nombre. Todavía podemos rastrear la procedencia usando la opción git -follow, que rastrea el historial de movimientos de archivos, pero Github no entiende cuál es el movimiento.

Aislamiento de dependencia

El siguiente paso fue aislar las dependencias separando los dominios comerciales entre sí. Cada componente definió una interfaz limpia y especializada con límites de dominio expresados ​​a través de una API pública. Cada componente recibió la propiedad exclusiva de los datos asociados a él.

Aunque el equipo no pudo implementar este pedido en todo el código base de Shopify porque requería expertos de cada dominio, el equipo sí definió patrones y proporcionó herramientas para completar la tarea.

Hemos desarrollado una herramienta llamada Wedge que rastrea el progreso de cada componente para lograr el aislamiento. Detecta cualquier violación de los límites del dominio (cuando se accede a otro componente a través de algo que no sea su API definida públicamente), así como el acoplamiento de datos entre componentes. Para hacer esto, escribimos una herramienta que se conecta a los puntos de seguimiento de Ruby durante la CI para obtener el gráfico de llamadas completo. Luego clasificamos a las personas que llaman y a los destinatarios por componente, seleccionando solo aquellas llamadas que cruzan los límites de los componentes y las enviamos a Wedge. Junto con estas llamadas, enviamos algunos datos adicionales del análisis de código, como asociaciones y herencia de ActiveRecord. Luego, Wedge determina cuáles de estas cosas de componentes cruzados (llamadas, asociaciones, herencia) son normales y cuáles no funcionan. Generalmente:

  • Las asociaciones entre componentes siempre rompen la componenteidad.
  • Las llamadas sólo son posibles a cosas que son claramente públicas.

Luego, Wedge calcula la puntuación general y también enumera las infracciones de cada componente.

imagen

Shopify's Wedge realiza un seguimiento del progreso hacia el objetivo de cada componente.

El siguiente paso será graficar cómo han cambiado las puntuaciones a lo largo del tiempo y mostrar las diferencias significativas para que las personas puedan ver por qué y cuándo ha cambiado la puntuación.

Aplicación de límites

De cara al futuro, nos gustaría dar un paso más y hacer cumplir estos límites de forma programática. En este artículo de Dan Mangs proporciona un ejemplo detallado de cómo un equipo de desarrollo de aplicaciones alcanzó los límites. Si bien todavía estamos explorando el enfoque que queremos usar, en términos generales planeamos que cada componente cargue solo los componentes de los que depende explícitamente. Esto provocará errores en tiempo de ejecución si intenta acceder al código de un componente al que no se le ha declarado una dependencia. También podemos causar errores de tiempo de ejecución o fallas en las pruebas cuando no se accede a los componentes a través de su API pública.

También queremos desentrañar gráfico de dependencia de dominioeliminando dependencias aleatorias y circulares. Lograr un aislamiento completo es un desafío continuo, pero es algo en lo que todos los desarrolladores de Shopify están invirtiendo y ya estamos viendo algunos de los beneficios esperados. Por ejemplo, teníamos un motor fiscal obsoleto que ya no satisfacía las necesidades de nuestros comerciantes. Antes de que se llevaran a cabo los esfuerzos descritos en esta publicación, reemplazar el sistema antiguo por uno nuevo habría sido una tarea casi imposible. Sin embargo, debido a que pusimos tanto esfuerzo en aislar la dependencia, pudimos reemplazar nuestro motor de impuestos con un sistema de cálculo de impuestos completamente nuevo.

En conclusión, ninguna arquitectura suele ser la mejor arquitectura en las primeras etapas de un sistema. Esto no significa que no debas implementar buenas prácticas de software, pero no deberías pasar semanas o meses intentando diseñar un sistema complejo que aún no conoces. Hipótesis sobre la sostenibilidad de la arquitectura Martin Fowler ilustra bien esta idea al explicar que en las primeras etapas de la mayoría de las aplicaciones se puede avanzar muy rápidamente sin tener que realizar ningún trabajo de diseño. Tiene sentido buscar un equilibrio entre la calidad del diseño y el tiempo de comercialización. Una vez que el ritmo al que se agregan características y funcionalidades comienza a disminuir, es hora de invertir en un buen diseño.

Las mejores etapas para refactorizar y reconstruir son lo más tardías posible, ya que continuamente aprende más sobre su sistema y dominio comercial a lo largo del proceso de desarrollo. Diseñar un sistema complejo de microservicios antes de investigar a fondo el dominio es un paso arriesgado con el que muchas personas tropiezan. Según Martina Fowler“En casi todos los casos que he oído hablar de un sistema que fue construido como un microservicio desde cero, terminó con serios problemas… No es una buena idea comenzar un nuevo proyecto con microservicios, incluso si estás Asegúrese de que su aplicación sea lo suficientemente grande como para justificar ese enfoque “

Una buena arquitectura de software es un desafío continuo y la escala a la que opere determinará qué solución es la adecuada para su aplicación. Los monolitos, los monolitos modulares y las arquitecturas orientadas a servicios evolucionan en una escala evolutiva a medida que aumenta la complejidad de su aplicación. Cada arquitectura es adecuada para un equipo/aplicación de su tamaño, y pasar de una capa a otra será muy doloroso. Cuando comienza a abordar muchos de los puntos débiles descritos en este artículo, sabrá que su solución actual se le ha quedado pequeña y que es hora de pasar a la siguiente.

PD: Tenga en cuenta que estamos teniendo una oferta en nuestro sitio web.

Publicaciones Similares

Deja una respuesta

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