Historia de la arquitectura Dodo IS: el monolito temprano

O cada empresa infeliz con un monolito es infeliz a su manera.

El desarrollo del sistema Dodo IS comenzó al mismo tiempo que la empresa Dodo Pizza, en 2011. La idea base era la digitalización completa y total de los procesos empresariales, de hecho, por nuestros propios medios, lo que ya en 2011 generaba muchas preguntas y escepticismo. Pero han pasado 9 años y seguimos este camino, con un desarrollo propio que comenzó con un monolito.

Este artículo es una "respuesta" a las preguntas: "¿Por qué reescribir la arquitectura y hacer cambios tan amplios y prolongados?" al artículo anterior "La historia de la arquitectura Dodo IS: el camino del backend".Comenzaré con cómo se inició el desarrollo de Dodo IS, cómo era la arquitectura original, cómo aparecían nuevos módulos y debido a qué problemas se realizaron cambios a gran escala.

Historia de la arquitectura Dodo IS: el monolito temprano

La serie de artículos "¿Qué es Dodo IS?" hablará sobre:

  1. El monolito temprano en Dodo IS (2011-2015). (Estás aquí)

  2. El camino del backend: bases separadas y bus..

  3. El camino de la parte del cliente: fachada sobre la base (2016-2017). (En progreso...)

  4. La historia de los verdaderos microservicios. (2018-2019). (En progreso...)

  5. La ruptura completada del monolito y estabilización de la arquitectura. (En progreso...)

Arquitectura original

En 2011, la arquitectura de Dodo IS se veía así:

Historia de la arquitectura Dodo IS: el monolito temprano

El primer módulo en la arquitectura era la recepción de pedidos. El proceso de negocio era el siguiente:

  • el cliente llama a la pizzería;

  • el gerente contesta;

  • toma el pedido por teléfono;

  • mientras introduce el pedido en la interfaz de recepción de pedidos: se considera la información sobre el cliente, los datos del pedido y la dirección de entrega. 

La interfaz del sistema de información se veía aproximadamente así...

La primera versión de octubre de 2011:

Un poco mejorada en enero de 2012.

Sistema de información Dodo Pizza Delivery Pizza Restaurant

Los recursos para desarrollar el primer módulo de recepción de pedidos eran limitados. Era necesario hacer mucho, rápidamente y con un pequeño equipo. Un equipo pequeño significa 2 desarrolladores, quienes sentaron las bases de todo el sistema futuro.

Su primera decisión determinó el futuro del stack tecnológico:

  • Backend en ASP.NET MVC, lenguaje C#. Los desarrolladores eran programadores de .NET, este stack les era familiar y agradable.

  • Frontend en Bootstrap y JQuery: interfaces de usuario con estilos y scripts personalizados. 

  • Base de datos MySQL: sin costos de licencias, fácil de usar.

  • Servidores en Windows Server, porque .NET en ese momento solo podía estar en Windows (no discutiremos Mono).

Físicamente, todo esto se tradujo en "dedicado con el proveedor de hosting". 

Arquitectura de la aplicación de recepción de pedidos

Entonces, todos ya hablaban de microservicios, y SOA se utilizó durante unos 5 años en grandes proyectos, por ejemplo, WCF se lanzó en 2006. Pero en ese momento se eligió una solución confiable y probada.

Aquí está.

Historia de la arquitectura Dodo IS: el monolito temprano

Asp.Net MVC es Razor, que devuelve una página HTML bajo demanda desde un formulario o del cliente, con renderizado en el servidor. En el cliente, ya CSS y scripts JS muestran la información y, si es necesario, realizan solicitudes AJAX a través de JQuery.

Las solicitudes en el servidor llegan a las clases *Controller, donde el método procesa y genera la página HTML final. Los controladores hacen solicitudes al nivel de lógica, llamado *Services. Cada uno de los servicios se encargaba de un aspecto del negocio:

  • Por ejemplo, DepartmentStructureService proporcionaba información sobre pizzerías y departamentos. Un departamento es un grupo de pizzerías bajo la gestión de un solo franquiciado.

  • ReceivingOrdersService aceptaba y calculaba la composición del pedido.

  • Y SmsService enviaba SMS, llamando a los servicios API para el envío de SMS.

Los servicios procesaban datos de la base de datos, almacenaban lógica de negocio. Cada servicio tenía uno o varios *Repository con nombres correspondientes. Allí estaban las consultas a los procedimientos almacenados en la base y la capa de mapeadores. La lógica de negocio estaba en los procedimientos almacenados, especialmente en los que generaban datos de informe. No se utilizó ORM, todos confiaban en SQL escrito a mano. 

También había una capa de modelo de dominio y clases comunes de ayuda, por ejemplo, la clase Order, que almacenaba el pedido. Allí, en la capa, había un ayudante para la conversión de texto de visualización según la moneda seleccionada.

Todo esto se puede representar con el siguiente modelo:

Historia de la arquitectura Dodo IS: el monolito temprano

Ruta del pedido

Veamos el camino simplificado inicial para crear un pedido como ese.

Historia de la arquitectura Dodo IS: el monolito temprano

Inicialmente, el sitio era estático. Tenía precios y arriba un número de teléfono y la frase "¿Quieres pizza? Llama al número y pide". Para hacer un pedido, necesitamos implementar un flujo simple: 

  • El cliente accede al sitio estático con precios, elige productos y llama al número indicado en el sitio.

  • El cliente menciona los productos que desea agregar al pedido.

  • Dice su dirección y nombre.

  • El operador acepta el pedido.

  • El pedido se muestra en la interfaz de pedidos recibidos.

Todo comienza con la visualización del menú. Un usuario operador autenticado solo puede aceptar un pedido a la vez. Por lo tanto, el carrito en borrador puede almacenarse en su sesión (la sesión del usuario se mantiene en la memoria). Allí se encuentra el objeto Cart, que contiene los productos y la información del cliente.

El cliente menciona el producto, el operador hace clic en + junto al producto, y se envía una solicitud al servidor. Se extrae información del producto de la base de datos y se añade a la cesta.

Historia de la arquitectura Dodo IS: el monolito temprano

Nota. Sí, en este caso no es necesario extraer el producto de la base de datos, se puede enviar desde el frontend. Pero por claridad, he mostrado el camino desde la base de datos. 

A continuación, ingresamos la dirección y el nombre del cliente. 

Historia de la arquitectura Dodo IS: el monolito temprano

Al hacer clic en "Crear pedido":

  • Enviamos la solicitud a OrderController.SaveOrder().

  • Obtenemos el Cart de la sesión, donde están los productos en la cantidad que necesitamos.

  • Complementamos el Cart con la información del cliente y la enviamos al método AddOrder de la clase ReceivingOrderService, donde se guarda en la base de datos. 

  • En la base de datos hay tablas para el pedido, los productos del pedido y el cliente, todas vinculadas entre sí.

  • La interfaz de visualización del pedido obtiene y refleja los últimos pedidos.

Nuevos módulos

La aceptación del pedido era importante y necesaria. No se puede hacer negocio de venta de pizzas sin un sistema de aceptación de pedidos. Por ello, el sistema comenzó a crecer en funcionalidad — aproximadamente desde 2012 hasta 2015. Durante ese tiempo, aparecieron muchos bloques diferentes del sistema, que llamaré módulos, en contraposición a la noción de servicio o producto. 

Un módulo es un conjunto de funciones que están unidas por un objetivo comercial común. Al mismo tiempo, físicamente están en una sola aplicación.

Los módulos pueden considerarse bloques del sistema. Por ejemplo, hay un módulo de informes, interfaces de administración, un rastreador de productos en la cocina, autorización. Todas estas son diferentes interfaces para el usuario, algunas incluso tienen estilos visuales variados. Sin embargo, todas están dentro de una sola aplicación, un solo proceso operativo. 

Técnicamente, los módulos se estructuraron como Áreas (esta idea se ha mantenido en asp.net core). Allí había archivos separados para el frontend, modelos y sus propias clases de controladores. Como resultado, el sistema se transformó de tal manera...

Historia de la arquitectura Dodo IS: el monolito temprano

...a esta:

Historia de la arquitectura Dodo IS: el monolito temprano

Algunos módulos se implementaron como sitios separados (proyecto ejecutable), debido a funcionalidad completamente distinta y en parte por un desarrollo más enfocado y separado. Estos son:

  • Site — la primera versión del sitio dodopizza.ru.

  • Exportar: exportación de informes desde Dodo IS para 1C. 

  • Personal — área personal del empleado. Se desarrolló por separado y tiene su propio acceso y diseño.

  • fs — proyecto para el alojamiento de estáticos. Posteriormente, nos alejamos de él, trasladando todos los estáticos a CDN Akamai. 

Los demás bloques se encontraban en la aplicación BackOffice. 

Historia de la arquitectura Dodo IS: el monolito temprano

Explicación sobre los nombres:

  • Cashier — Caja del restaurante.

  • ShiftManager — interfaces para el rol de "Gerente de turno": estadísticas operativas sobre las ventas de la pizzería, opción de poner productos en la lista negra, modificar pedidos.

  • OfficeManager — interfaces para los roles de "Gerente de pizzería" y "Franquiciado". Aquí se reúnen las funciones de configuración de la pizzería, sus promociones, gestión del personal, informes.

  • PublicScreens — interfaces para televisores y tabletas en las pizzerías. En los televisores se muestra el menú, información publicitaria y el estado del pedido al ser entregado. 

Utilizaban una capa común de servicios, un bloque común de clases de dominio Dodo.Core, así como una base de datos compartida. A veces también podían intercambiar información entre sí. Incluyendo que sitios web específicos como dodopizza.ru o personal.dodopizza.ru accedían a los servicios comunes.

Al aparecer nuevos módulos, se intentaba reutilizar al máximo el código de los servicios, procedimientos almacenados y tablas en la base de datos ya creados. 

Para una mejor comprensión de la magnitud de los módulos realizados en el sistema, aquí hay un esquema de 2012 con planes de desarrollo:

Historia de la arquitectura Dodo IS: el monolito temprano

Para 2015, todo lo que está en el esquema y más ya estaba en producción.

  • La recepción de pedidos se convirtió en un bloque separado del Centro de Contacto, donde el pedido es aceptado por un operador.

  • Aparecieron pantallas públicas con el menú y la información, colgadas en las pizzerías.

  • En la cocina hay un módulo que reproduce automáticamente el mensaje de voz "Nueva pizza" cuando llega un nuevo pedido, y también imprime un recibo para el repartidor. Esto simplifica mucho los procesos en la cocina, permitiendo a los empleados no distraerse con una gran cantidad de operaciones simples.

  • El bloque de entrega se convirtió en una Caja de Entrega separada, donde el pedido se entregaba al repartidor, que previamente había comenzado su turno. Se tuvo en cuenta su tiempo de trabajo para calcular su salario. 

Entre 2012 y 2015, más de 10 desarrolladores se unieron, se abrieron 35 pizzerías, se implementó un sistema en Rumanía y se preparó la apertura de locales en EE.UU. Los desarrolladores ya no asumían todas las tareas, sino que se dividieron en equipos, cada uno especializado en su parte del sistema. 

Problemas

Incluido debido a la arquitectura (pero no solo).

Caos en la base de datos

Una sola base de datos es conveniente. Se puede lograr consistencia, además de aprovechar herramientas integradas en bases de datos relacionales. Trabajar con ella es familiar y cómodo, especialmente si hay pocas tablas y pocos datos.

Pero tras 4 años de desarrollo, la base cuenta con alrededor de 600 tablas y 1500 procedimientos almacenados, muchos de los cuales incluían lógica. Lamentablemente, los procedimientos almacenados no ofrecen ventajas significativas cuando se trabaja con MySQL. No son almacenados en caché por la base de datos, y almacenar lógica en ellos complica el desarrollo y la depuración. También dificulta la reutilización del código.

Muchas tablas carecían de índices adecuados.Por otro lado, había tablas con demasiados índices, lo que dificultaba la inserción. Era necesario modificar aproximadamente 20 tablas; la transacción para crear un pedido podría tardar entre 3 y 5 segundos. 

Los datos en las tablas no siempre estaban en la forma más adecuada.En algunos casos era necesario desnormalizar. Parte de los datos que se recibían regularmente se almacenaba en una columna como estructura XML, lo que aumentaba el tiempo de ejecución, alargaba las consultas y complicaba el desarrollo.

Se realizaban muy diversas consultas a las mismas tablas.Las tablas populares, como la mencionada orders o la tabla pizzeria, se utilizaban para mostrar interfaces operativas en la cocina y para análisis. También las consultaba el sitio web (dodopizza.ru), donde, en cualquier momento, podían llegar repentinamente muchas solicitudes. 

Los datos no estaban agregados y muchos cálculos se realizaban en tiempo real a través de la base de datos. Esto generaba cálculos innecesarios y una carga adicional. 

A menudo, el código accedía a la base de datos cuando no era necesario. En algunos casos faltaban operaciones por lotes, y en otros era necesario descomponer una consulta en varias a través del código para acelerar y aumentar la confiabilidad. 

Conexión y enredo en el código.

Los módulos que debían encargarse de su área de negocio no cumplían su función de manera honesta.. Algunos de ellos tenían funciones duplicadas para roles. Por ejemplo, un marketero local, que es responsable de la actividad de marketing de la red en su ciudad, tenía que usar tanto la interfaz de "Admin" (para crear promociones) como la interfaz de "Office Manager" (para ver el impacto de las promociones en el negocio). Claro, dentro ambos módulos utilizaban un único servicio que gestionaba las promociones.

Los servicios (clases dentro de un gran proyecto monolítico) podían llamarse entre sí para enriquecer sus datos.

Con las propias clases modelo, que almacenan los datos, el trabajo en el código se realizaba de diversas maneras. En algunos lugares había constructores a través de los cuales se podían especificar campos obligatorios. En otros, esto se hacía a través de propiedades públicas. Por supuesto, la obtención y transformación de datos de la base de datos era variada. 

La lógica estaba ya sea en los controladores o en las clases de servicios. 

Esto parecen ser problemas menores, pero ralentizaban mucho el desarrollo y disminuían la calidad, lo que llevaba a inestabilidad y errores. 

La complejidad de un gran desarrollo

Las dificultades también surgieron durante el desarrollo. Era necesario crear diferentes bloques del sistema, y además, de manera paralela. Conciliar las necesidades de cada componente en un único código se volvía cada vez más difícil. No era fácil llegar a un acuerdo y satisfacer a todos los componentes al mismo tiempo. Además, se agregaban limitaciones en las tecnologías, especialmente en lo que respecta a la base y el frontend. Se debía abandonar JQuery en favor de marcos de alto nivel, especialmente en partes de servicios del cliente (sitio web).

En algunas partes del sistema podrían haberse utilizado bases de datos más adecuadas para esto. Por ejemplo, más tarde tuvimos un caso de transición de Redis a CosmosDB para el almacenamiento del carrito de pedidos. 

Los equipos y desarrolladores que se ocupaban de su área claramente deseaban una mayor autonomía para sus servicios, tanto en términos de desarrollo como en la implementación. Conflictos durante la fusión, problemas en los lanzamientos. Si para 5 desarrolladores este problema no era significativo, para 10, y mucho más en el crecimiento planificado, todo se volvió más serio. Y se avecinaba el desarrollo de una aplicación móvil (comenzó en 2017, y en 2018 hubo una gran caída). 

Diferentes partes del sistema requerían diferentes indicadores de estabilidad, pero debido a la fuerte interconexión del sistema, no pudimos garantizarlo. Un error al desarrollar una nueva función en el panel de administración podría haber influido en la recepción de pedidos en el sitio web, ya que el código es común y reutilizable, y la base y los datos también son los mismos.

Probablemente, en el marco de una arquitectura monolítica-módular se podrían haber evitado estos errores y problemas: separando responsabilidades, realizando refactorizaciones tanto del código como de la base de datos, separando claramente las capas, y controlando la calidad todos los días. Sin embargo, las decisiones arquitectónicas elegidas y el enfoque en la rápida expansión de las funcionalidades del sistema llevaron a problemas de estabilidad.

Cómo el blog La Fuerza de la Mente afectó los ingresos en los restaurantes.

Si el crecimiento de la cadena de pizzerías (y la carga) hubiera continuado al mismo ritmo, en algún momento las caídas habrían sido tan severas que el sistema no se habría recuperado. Una historia que ilustra bien los problemas que empezamos a enfrentar en 2015. 

En el blog “La Fuerza de la Mente” había un widget que mostraba datos sobre los ingresos anuales de toda la red. El widget accedía a la API pública de Dodo, que proporciona estos datos. Ahora, esta estadística está disponible en http://dodopizzastory.com/. El widget se mostraba en cada página y hacía solicitudes cada 20 segundos. La solicitud iba a api.dodopizza.ru y solicitaba:

  • el número de pizzerías en la cadena;

  • los ingresos totales de la cadena desde el inicio del año;

  • los ingresos del día de hoy.

La solicitud de estadísticas de ingresos iba directamente a la base de datos y comenzaba a solicitar datos sobre los pedidos, agregando información en tiempo real y proporcionando la suma. 

En esta misma tabla de pedidos, las cajas de los restaurantes descargaban la lista de pedidos aceptados del día y se añadían los nuevos pedidos. Las cajas hacían sus solicitudes cada 5 segundos o con la actualización de la página.

El esquema era el siguiente:

Historia de la arquitectura Dodo IS: el monolito temprano

Una vez en otoño, Fyodor Ovchinnikov escribió una larga y popular entrada en su blog. Mucha gente visitó el blog y comenzó a leer atentamente todo. Mientras cada persona que llegó leía el artículo, el widget de ingresos funcionaba correctamente y solicitaba la API cada 20 segundos.

La API invocó un procedimiento almacenado para calcular la suma de todos los pedidos desde el inicio del año en todas las pizzerías de la red. La agregación se realizó sobre la tabla orders, que es muy popular. En ella ingresan todas las cajas de todos los restaurantes abiertos en ese momento. Las cajas dejaron de responder, no se aceptaban pedidos. También, no se aceptaban desde el sitio web, no aparecían en el rastreador, el gerente de turno no podía verlos en su interfaz. 

Esta no es la única historia. Para el otoño de 2015, la carga del sistema era crítica todos los viernes. Varias veces apagamos la API pública, y una vez, incluso tuvimos que desconectar el sitio, porque ya nada funcionaba. Existía incluso una lista de servicios con un orden de desconexión en caso de cargas graves.

Desde este momento comienza nuestra lucha contra las cargas y por la estabilización del sistema (desde el otoño de 2015 hasta el otoño de 2018). Fue entonces cuando ocurrió el “Gran colapso”. Luego, también ocurrieron fallos de vez en cuando, algunos fueron bastante sensibles, pero el período general de inestabilidad ahora se puede considerar superado.

Crecimiento acelerado del negocio

¿Por qué no se pudo "hacerlo bien desde el principio"? Basta con mirar los siguientes gráficos.

Historia de la arquitectura Dodo IS: el monolito temprano

También en 2014-2015 se abrió en Rumanía y se preparaba la apertura en EE. UU.

La red creció muy rápido, se abrieron nuevos países, aparecieron nuevos formatos de pizzerías, por ejemplo, se abrió una pizzería en un food court. Todo esto requería una atención significativa a la expansión de las funciones de Dodo IS. Sin todas estas funciones, sin rastreo en la cocina, control de productos y pérdidas en el sistema, visualización del pedido en el área del food court, difícilmente estaríamos hablando ahora de una arquitectura "correcta" y un enfoque "adecuado" para el desarrollo.

Otro obstáculo para la revisión oportuna de la arquitectura y, en general, para la atención a los problemas técnicos, fue la crisis de 2014. Estas cosas afectan dolorosamente las oportunidades de crecimiento de los equipos, especialmente para un negocio joven como lo era Dodo Pizza.

Soluciones rápidas que ayudaron

Los problemas requerían soluciones. Condicionalmente, las soluciones se pueden dividir en 2 grupos:

  • Rápidas, que apagan el fuego y dan un pequeño margen de supervivencia, y nos ganan tiempo para hacer cambios.

  • Sistémicas y, por lo tanto, largas. Reingeniería de varios módulos, separación de la arquitectura monolítica en servicios separados (la mayoría de ellos no son micro, sino más bien macroservicios y sobre esto hay informe de Andrey Morevsky). 

La lista de cambios rápidos es la siguiente:

Escalar maestra de la base

Por supuesto, lo primero que se hace para combatir las cargas es aumentar la potencia del servidor. Esto se realizó tanto para la maestra de la base como para los servidores web. Lamentablemente, esto solo es posible hasta cierto límite, después se vuelve demasiado costoso.

Desde 2014, nos mudamos a Azure, sobre esto también escribimos en aquel momento en el artículo «Cómo Dodo Pizza entrega pizza con la nube de Microsoft Azure». Pero después de una serie de aumentos en los servidores de la base, nos encontramos con límites de costos. 

Réplicas de la base para lectura

Se hicieron dos réplicas para la base:

ReadReplica para consultas sobre directorios. Se utiliza para la lectura de directorios, como ciudades, calles, pizzerías, productos (dominio de cambios lentos), y en aquellas interfaces donde se permite un pequeño retraso. Estas réplicas eran 2, y aseguramos su disponibilidad igual que la de la maestra.

ReadReplica para consultas sobre informes. Esta base tenía menor disponibilidad, pero en ella se realizaban todos los informes. Aunque tienen consultas pesadas sobre enormes recálculos de datos, no afectan a la base principal ni a las interfaces operativas. 

Cachés en el código

No había cachés en el código en ninguna parte (en absoluto). Esto llevaba a consultas adicionales, no siempre necesarias, en la base sobrecargada. Las cachés estaban inicialmente tanto en memoria como en un servicio de caché externo, que era Redis. Todo se invalidaba por tiempo, y las configuraciones se indicaban en el código.

Varios servidores para el backend

El backend de la aplicación también necesitaba ser escalado para soportar las cargas incrementadas. Era necesario convertir de un servidor IIS a un clúster. Migramos la sesión de aplicaciones de la memoria a RedisCache, lo que permitió establecer varios servidores detrás de un simple equilibrador de carga con round robin. Inicialmente se utilizó el mismo Redis que para las cachés, luego se distribuyeron en varios. 

En última instancia, la arquitectura se complicó...

Historia de la arquitectura Dodo IS: el monolito temprano

...pero se logró aliviar parte de la tensión.

Y luego había que rehacer los componentes sobrecargados, lo que nosotros afrontamos. Hablaremos de esto en la siguiente parte.

Fuente: habr.com

Compra un hosting fiable para sitios web con protección contra DDoS, servidores VPS VDS 🔥 Compra un hosting fiable para sitios web con protección contra DDoS, servidores VPS VDS | ProHoster