La transición del monolito a los microservicios: historia y práctica

En este artículo, hablaré sobre cómo el proyecto en el que estoy trabajando se transformó de un gran monolito a un conjunto de microservicios.

El proyecto comenzó su historia hace bastante tiempo, a principios de 2000. Las primeras versiones se escribieron en Visual Basic 6. Con el tiempo, quedó claro que el desarrollo en este lenguaje sería difícil de mantener en el futuro, ya que el IDE y el lenguaje en sí están poco desarrollados. A finales de la década de 2000, se decidió cambiar al C#, que era más prometedor. La nueva versión se escribió en paralelo con la revisión de la anterior y gradualmente se escribió más y más código en .NET. El backend en C# se centró inicialmente en una arquitectura de servicios, pero durante el desarrollo se utilizaron bibliotecas comunes con lógica y los servicios se lanzaron en un solo proceso. El resultado fue una aplicación que llamamos "monolito de servicio".

Una de las pocas ventajas de esta combinación fue la capacidad de los servicios de llamarse entre sí a través de una API externa. Existían requisitos previos claros para la transición a una arquitectura de servicios más correcta y, en el futuro, a una arquitectura de microservicios.

Comenzamos nuestro trabajo sobre descomposición alrededor de 2015. Todavía no hemos llegado a un estado ideal: todavía hay partes de un gran proyecto que difícilmente pueden llamarse monolitos, pero tampoco parecen microservicios. Sin embargo, el progreso es significativo.
Hablaré de ello en el artículo.

La transición del monolito a los microservicios: historia y práctica

contenido

Arquitectura y problemas de la solución existente.


Inicialmente, la arquitectura se veía así: la interfaz de usuario es una aplicación separada, la parte monolítica está escrita en Visual Basic 6, la aplicación .NET es un conjunto de servicios relacionados que trabajan con una base de datos bastante grande.

Desventajas de la solución anterior

Punto único de fallo
Tuvimos un único punto de falla: la aplicación .NET se ejecutó en un solo proceso. Si algún módulo fallaba, toda la aplicación fallaba y debía reiniciarse. Dado que automatizamos una gran cantidad de procesos para diferentes usuarios, debido a una falla en uno de ellos, todos no pudieron trabajar durante un tiempo. Y en caso de un error de software, ni siquiera la copia de seguridad sirvió de ayuda.

Cola de mejoras
Este inconveniente es más bien organizativo. Nuestra aplicación tiene muchos clientes y todos quieren mejorarla lo antes posible. Antes era imposible hacer esto en paralelo y todos los clientes hacían cola. Este proceso fue negativo para las empresas porque tenían que demostrar que su tarea era valiosa. Y el equipo de desarrollo dedicó tiempo a organizar esta cola. Esto requirió mucho tiempo y esfuerzo y, al final, el producto no pudo cambiar tan rápido como les hubiera gustado.

Uso subóptimo de los recursos
Al alojar servicios en un solo proceso, siempre copiamos completamente la configuración de un servidor a otro. Queríamos colocar los servicios más cargados por separado para no desperdiciar recursos y obtener un control más flexible sobre nuestro esquema de implementación.

Difícil de implementar tecnologías modernas.
Un problema familiar para todos los desarrolladores: existe el deseo de introducir tecnologías modernas en el proyecto, pero no hay oportunidad. Con una solución monolítica grande, cualquier actualización de la biblioteca actual, sin mencionar la transición a una nueva, se convierte en una tarea nada trivial. Se necesita mucho tiempo para demostrarle al líder del equipo que esto traerá más bonificaciones que nervios desperdiciados.

Dificultad para emitir cambios.
Este era el problema más grave: publicábamos lanzamientos cada dos meses.
Cada lanzamiento se convirtió en un verdadero desastre para el banco, a pesar de las pruebas y los esfuerzos de los desarrolladores. La empresa entendió que a principios de semana algunas de sus funciones no funcionarían. Y los desarrolladores entendieron que les esperaba una semana de incidentes graves.
Todos tenían el deseo de cambiar la situación.

Expectativas de los microservicios


Entrega de componentes cuando estén listos. Entrega de componentes cuando estén listos descomponiendo la solución y separando diferentes procesos.

Pequeños equipos de productos. Esto es importante porque era difícil gestionar un gran equipo que trabajaba en el antiguo monolito. Un equipo así se vio obligado a trabajar según un proceso estricto, pero querían más creatividad e independencia. Sólo los equipos pequeños podían permitírselo.

Aislamiento de servicios en procesos separados. Lo ideal sería aislarlo en contenedores, pero una gran cantidad de servicios escritos en .NET Framework solo se ejecutan en Windows. Ahora están apareciendo servicios basados ​​en .NET Core, pero todavía quedan pocos.

Flexibilidad de implementación. Nos gustaría combinar los servicios de la manera que los necesitamos y no de la forma en que el código lo obliga.

Uso de nuevas tecnologías. Esto es interesante para cualquier programador.

Problemas de transición


Por supuesto, si fuera fácil dividir un monolito en microservicios, no habría necesidad de hablar de ello en conferencias ni escribir artículos. Hay muchos obstáculos en este proceso; describiré los principales que nos obstaculizaron.

Primer problema Típico de la mayoría de los monolitos: coherencia de la lógica empresarial. Cuando escribimos un monolito, queremos reutilizar nuestras clases para no escribir código innecesario. Y cuando se pasa a microservicios, esto se convierte en un problema: todo el código está bastante estrechamente acoplado y es difícil separar los servicios.

Al momento del inicio de las obras, el repositorio contaba con más de 500 proyectos y más de 700 mil líneas de código. Esta es una decisión bastante importante y segundo problema. No era posible simplemente tomarlo y dividirlo en microservicios.

Tercer problema — falta de infraestructura necesaria. De hecho, estábamos copiando manualmente el código fuente a los servidores.

Cómo pasar del monolito a los microservicios


Aprovisionamiento de microservicios

En primer lugar, determinamos de inmediato que la separación de microservicios es un proceso iterativo. Siempre se nos pidió que desarrolláramos problemas comerciales en paralelo. Cómo implementaremos esto técnicamente ya es nuestro problema. Por lo tanto, nos preparamos para un proceso iterativo. No funcionará de otra manera si tiene una aplicación grande e inicialmente no está lista para ser reescrita.

¿Qué métodos utilizamos para aislar microservicios?

El primer método — mover módulos existentes como servicios. En este sentido, tuvimos suerte: ya existían servicios registrados que funcionaban con el protocolo WCF. Fueron separados en asambleas separadas. Los portamos por separado y agregamos un pequeño iniciador a cada compilación. Fue escrito utilizando la maravillosa biblioteca Topshelf, que le permite ejecutar la aplicación como servicio y como consola. Esto resulta conveniente para la depuración, ya que no se requieren proyectos adicionales en la solución.

Los servicios estaban conectados según la lógica empresarial, ya que utilizaban ensamblajes comunes y trabajaban con una base de datos común. Difícilmente podrían denominarse microservicios en su forma pura. Sin embargo, podríamos proporcionar estos servicios por separado, en diferentes procesos. Esto por sí solo hizo posible reducir la influencia mutua, reduciendo el problema con el desarrollo paralelo y un único punto de falla.

El ensamblaje con el host es solo una línea de código en la clase Programa. Escondimos el trabajo con Topshelf en una clase auxiliar.

namespace RBA.Services.Accounts.Host
{
   internal class Program
   {
      private static void Main(string[] args)
      {
        HostRunner<Accounts>.Run("RBA.Services.Accounts.Host");

       }
    }
}

La segunda forma de asignar microservicios es: crearlos para resolver nuevos problemas. Si al mismo tiempo el monolito no crece, esto ya es excelente, lo que significa que vamos en la dirección correcta. Para resolver nuevos problemas, intentamos crear servicios separados. Si existiera esa oportunidad, entonces creamos servicios más "canónicos" que administran completamente su propio modelo de datos, una base de datos separada.

Nosotros, como muchos, comenzamos con servicios de autenticación y autorización. Son perfectos para esto. Son independientes y, por regla general, tienen un modelo de datos independiente. Ellos mismos no interactúan con el monolito, solo éste recurre a ellos para resolver algunos problemas. Con estos servicios, puede comenzar la transición a una nueva arquitectura, depurar la infraestructura de ellos, probar algunos enfoques relacionados con las bibliotecas de red, etc. No tenemos ningún equipo en nuestra organización que no pueda crear un servicio de autenticación.

La tercera forma de asignar microservicios.El que utilizamos es un poco específico para nosotros. Se trata de la eliminación de la lógica empresarial de la capa de la interfaz de usuario. Nuestra aplicación de interfaz de usuario principal es de escritorio; al igual que el backend, está escrita en C#. Los desarrolladores cometían errores periódicamente y transfirieron partes de la lógica a la interfaz de usuario que deberían haber existido en el backend y haber sido reutilizadas.

Si observa un ejemplo real del código de la parte de la interfaz de usuario, puede ver que la mayor parte de esta solución contiene lógica empresarial real que es útil en otros procesos, no solo para crear el formulario de la interfaz de usuario.

La transición del monolito a los microservicios: historia y práctica

La lógica real de la interfaz de usuario solo está ahí en las últimas líneas. Lo transferimos al servidor para que pudiera reutilizarse, reduciendo así la interfaz de usuario y logrando la arquitectura correcta.

La cuarta y más importante forma de aislar microservicios.Lo que permite reducir el monolito es la eliminación de los servicios existentes con procesamiento. Cuando eliminamos los módulos existentes tal como están, el resultado no siempre es del agrado de los desarrolladores y es posible que el proceso de negocio haya quedado obsoleto desde que se creó la funcionalidad. Con la refactorización, podemos respaldar un nuevo proceso comercial porque los requisitos comerciales cambian constantemente. Podemos mejorar el código fuente, eliminar defectos conocidos y crear un mejor modelo de datos. Se obtienen muchos beneficios.

La separación de los servicios del procesamiento está indisolublemente ligada al concepto de contexto acotado. Este es un concepto de Domain Driven Design. Significa una sección del modelo de dominio en la que todos los términos de un único idioma están definidos de forma única. Veamos el contexto de los seguros y las facturas como ejemplo. Tenemos una aplicación monolítica y necesitamos trabajar con la cuenta de seguros. Esperamos que el desarrollador encuentre una clase de Cuenta existente en otro ensamblado, haga referencia a ella desde la clase de Seguro y tendremos un código de trabajo. Se respetará el principio DRY y la tarea se realizará más rápido utilizando el código existente.

Como resultado, resulta que los contextos de cuentas y seguros están conectados. A medida que surjan nuevos requisitos, este acoplamiento interferirá con el desarrollo, aumentando la complejidad de una lógica empresarial que ya es compleja. Para resolver este problema, es necesario encontrar los límites entre contextos en el código y eliminar sus violaciones. Por ejemplo, en el contexto de los seguros, es muy posible que un número de cuenta del Banco Central de 20 dígitos y la fecha de apertura de la cuenta sean suficientes.

Para separar estos contextos acotados entre sí y comenzar el proceso de separar los microservicios de una solución monolítica, utilizamos un enfoque como la creación de API externas dentro de la aplicación. Si supiéramos que algún módulo debía convertirse en un microservicio, modificado de alguna manera dentro del proceso, entonces inmediatamente hacíamos llamadas a la lógica que pertenece a otro contexto limitado a través de llamadas externas. Por ejemplo, mediante REST o WCF.

Decidimos firmemente que no evitaríamos el código que requeriría transacciones distribuidas. En nuestro caso, resultó bastante sencillo seguir esta regla. Todavía no nos hemos encontrado con situaciones en las que realmente se necesiten transacciones distribuidas estrictas: la coherencia final entre módulos es suficiente.

Veamos un ejemplo específico. Tenemos el concepto de orquestador: una canalización que procesa la entidad de la "aplicación". Crea a su vez un cliente, una cuenta y una tarjeta bancaria. Si el cliente y la cuenta se crean exitosamente, pero la creación de la tarjeta falla, la aplicación no pasa al estado "exitosa" y permanece en el estado "tarjeta no creada". En el futuro, la actividad en segundo plano lo retomará y lo finalizará. El sistema lleva algún tiempo en un estado de inconsistencia, pero en general estamos satisfechos con ello.

Si surge una situación en la que es necesario guardar constantemente parte de los datos, lo más probable es que optemos por la consolidación del servicio para procesarlos en un solo proceso.

Veamos un ejemplo de asignación de microservicios. ¿Cómo se puede llevarlo a producción con relativa seguridad? En este ejemplo, tenemos una parte separada del sistema: un módulo de servicio de nómina, una de cuyas secciones de código nos gustaría convertirla en microservicio.

La transición del monolito a los microservicios: historia y práctica

En primer lugar, creamos un microservicio reescribiendo el código. Estamos mejorando algunos aspectos con los que no estábamos contentos. Implementamos nuevos requerimientos comerciales del cliente. Agregamos una puerta de enlace API a la conexión entre la interfaz de usuario y el backend, que proporcionará desvío de llamadas.

La transición del monolito a los microservicios: historia y práctica

A continuación, ponemos en funcionamiento esta configuración, pero en estado piloto. La mayoría de nuestros usuarios todavía trabajan con procesos comerciales antiguos. Para los nuevos usuarios, estamos desarrollando una nueva versión de la aplicación monolítica que ya no contiene este proceso. Básicamente, tenemos una combinación de un monolito y un microservicio funcionando como piloto.

La transición del monolito a los microservicios: historia y práctica

Con un piloto exitoso, entendemos que la nueva configuración es realmente viable, podemos eliminar el antiguo monolito de la ecuación y dejar la nueva configuración en lugar de la solución anterior.

La transición del monolito a los microservicios: historia y práctica

En total, utilizamos casi todos los métodos existentes para dividir el código fuente de un monolito. Todos ellos nos permiten reducir el tamaño de partes de la aplicación y traducirlas a nuevas bibliotecas, mejorando el código fuente.

Trabajando con la base de datos


La base de datos se puede dividir peor que el código fuente, ya que contiene no sólo el esquema actual, sino también datos históricos acumulados.

Nuestra base de datos, como muchas otras, tenía otro inconveniente importante: su enorme tamaño. Esta base de datos se diseñó según la intrincada lógica empresarial de un monolito y las relaciones acumuladas entre las tablas de varios contextos acotados.

En nuestro caso, para colmo de problemas (base de datos grande, muchas conexiones, a veces límites poco claros entre tablas), surgió un problema que ocurre en muchos proyectos grandes: el uso de la plantilla de base de datos compartida. Los datos se tomaron de las tablas mediante visualización, replicación y se enviaron a otros sistemas donde se necesitaba esta replicación. Como resultado, no pudimos mover las tablas a un esquema separado porque se usaban activamente.

La misma división en contextos limitados en el código nos ayuda en la separación. Por lo general, nos da una idea bastante clara de cómo desglosamos los datos a nivel de base de datos. Entendemos qué tablas pertenecen a un contexto acotado y cuáles a otro.

Utilizamos dos métodos globales de partición de bases de datos: partición de tablas existentes y partición con procesamiento.

Dividir tablas existentes es un buen método si la estructura de datos es buena, cumple con los requisitos comerciales y todos están contentos con ella. En este caso, podemos separar las tablas existentes en un esquema separado.

Se necesita un departamento con procesamiento cuando el modelo de negocio ha cambiado mucho y las tablas ya no nos satisfacen en absoluto.

Dividir tablas existentes. Necesitamos determinar qué vamos a separar. Sin este conocimiento, nada funcionará, y aquí nos ayudará la separación de contextos acotados en el código. Como regla general, si puede comprender los límites de los contextos en el código fuente, quedará claro qué tablas deben incluirse en la lista del departamento.

Imaginemos que tenemos una solución en la que dos módulos monolitos interactúan con una base de datos. Necesitamos asegurarnos de que solo un módulo interactúe con la sección de tablas separadas y el otro comience a interactuar con ella a través de la API. Para empezar basta con que únicamente se realice la grabación a través de la API. Ésta es una condición necesaria para que hablemos de la independencia de los microservicios. Las conexiones de lectura pueden permanecer siempre que no haya un gran problema.

La transición del monolito a los microservicios: historia y práctica

El siguiente paso es que podemos separar la sección de código que funciona con tablas separadas, con o sin procesamiento, en un microservicio separado y ejecutarlo en un proceso separado, un contenedor. Este será un servicio separado con conexión a la base de datos Monolith y aquellas tablas que no se relacionan directamente con ella. El monolito todavía interactúa para leer con la parte desmontable.

La transición del monolito a los microservicios: historia y práctica

Posteriormente eliminaremos esta conexión, es decir, la lectura de datos de una aplicación monolítica de tablas separadas también se transferirá a la API.

La transición del monolito a los microservicios: historia y práctica

A continuación, seleccionaremos de la base de datos general las tablas con las que solo funciona el nuevo microservicio. Podemos mover las tablas a un esquema separado o incluso a una base de datos física separada. Todavía existe una conexión de lectura entre el microservicio y la base de datos monolith, pero no hay nada de qué preocuparse, en esta configuración puede durar bastante tiempo.

La transición del monolito a los microservicios: historia y práctica

El último paso es eliminar por completo todas las conexiones. En este caso, es posible que necesitemos migrar datos desde la base de datos principal. En ocasiones queremos reutilizar algunos datos o directorios replicados desde sistemas externos en varias bases de datos. Esto nos pasa periódicamente.

La transición del monolito a los microservicios: historia y práctica

Departamento de procesamiento. Este método es muy similar al primero, sólo que en orden inverso. Inmediatamente asignamos una nueva base de datos y un nuevo microservicio que interactúa con el monolito a través de una API. Pero al mismo tiempo, queda un conjunto de tablas de bases de datos que queremos eliminar en el futuro. Ya no lo necesitamos; lo reemplazamos en el nuevo modelo.

La transición del monolito a los microservicios: historia y práctica

Para que este plan funcione, probablemente necesitaremos un período de transición.

Hay entonces dos enfoques posibles.

primero: duplicamos todos los datos en las bases de datos nuevas y antiguas. En este caso tenemos redundancia de datos y pueden surgir problemas de sincronización. Pero podemos tomar dos clientes diferentes. Uno funcionará con la nueva versión y el otro con la antigua.

Segundo: dividimos los datos según algunos criterios comerciales. Por ejemplo, teníamos 5 productos en el sistema que estaban almacenados en la base de datos anterior. Colocamos el sexto dentro de la nueva tarea empresarial en una nueva base de datos. Pero necesitaremos una puerta de enlace API que sincronice estos datos y le muestre al cliente dónde y de qué obtener.

Ambos enfoques funcionan, elija según la situación.

Una vez que estemos seguros de que todo funciona, se puede desactivar la parte del monolito que funciona con estructuras de bases de datos antiguas.

La transición del monolito a los microservicios: historia y práctica

El último paso es eliminar las estructuras de datos antiguas.

La transición del monolito a los microservicios: historia y práctica

En resumen, podemos decir que tenemos problemas con la base de datos: es difícil trabajar con ella en comparación con el código fuente, es más difícil de compartir, pero se puede y se debe hacer. Hemos encontrado algunas formas que nos permiten hacer esto de manera bastante segura, pero aún es más fácil cometer errores con los datos que con el código fuente.

Trabajando con código fuente


Así es como se veía el diagrama del código fuente cuando comenzamos a analizar el proyecto monolítico.

La transición del monolito a los microservicios: historia y práctica

Se puede dividir aproximadamente en tres capas. Esta es una capa de módulos, complementos, servicios y actividades individuales lanzados. De hecho, estos eran puntos de entrada dentro de una solución monolítica. Todos ellos estaban herméticamente sellados con una capa común. Tenía una lógica empresarial de que los servicios se compartían y muchas conexiones. Cada servicio y complemento utilizaba hasta 10 o más ensamblajes comunes, según su tamaño y la conciencia de los desarrolladores.

Tuvimos suerte de contar con bibliotecas de infraestructura que podían usarse por separado.

A veces surgía una situación en la que algunos objetos comunes en realidad no pertenecían a esta capa, sino que eran bibliotecas de infraestructura. Esto se resolvió cambiando el nombre.

La mayor preocupación eran los contextos acotados. Sucedió que se mezclaron 3 o 4 contextos en un ensamblaje común y se utilizaron entre sí dentro de las mismas funciones comerciales. Era necesario entender dónde se podría dividir esto y a lo largo de qué límites, y qué hacer a continuación para mapear esta división en ensamblajes de código fuente.

Hemos formulado varias reglas para el proceso de división del código.

primero: Ya no queríamos compartir la lógica empresarial entre servicios, actividades y complementos. Queríamos independizar la lógica empresarial dentro de los microservicios. Los microservicios, por otro lado, se consideran idealmente servicios que existen de forma completamente independiente. Creo que este enfoque es un poco derrochador y es difícil de lograr porque, por ejemplo, los servicios en C# estarán conectados en cualquier caso mediante una biblioteca estándar. Nuestro sistema está escrito en C#, aún no hemos utilizado otras tecnologías. Por lo tanto, decidimos que podíamos permitirnos el lujo de utilizar conjuntos técnicos comunes. Lo principal es que no contienen ningún fragmento de lógica empresarial. Si tiene un contenedor conveniente sobre el ORM que está utilizando, copiarlo de un servicio a otro es muy costoso.

Nuestro equipo es un fanático del diseño basado en dominios, por lo que la arquitectura cebolla fue una excelente opción para nosotros. La base de nuestros servicios no es la capa de acceso a datos, sino un conjunto con lógica de dominio, que contiene solo lógica de negocios y no tiene conexiones con la infraestructura. Al mismo tiempo, podemos modificar de forma independiente el ensamblaje del dominio para resolver problemas relacionados con los marcos.

En esta etapa nos encontramos con nuestro primer problema grave. El servicio tenía que referirse a un ensamblaje de dominio, queríamos que la lógica fuera independiente y el principio DRY nos obstaculizó mucho aquí. Los desarrolladores querían reutilizar clases de ensamblados vecinos para evitar la duplicación y, como resultado, los dominios comenzaron a vincularse nuevamente. Analizamos los resultados y decidimos que quizás el problema también esté en el área del dispositivo de almacenamiento del código fuente. Teníamos un gran repositorio que contenía todo el código fuente. La solución para todo el proyecto fue muy difícil de montar en una máquina local. Por lo tanto, se crearon pequeñas soluciones separadas para partes del proyecto, y nadie prohibió agregarles algún conjunto común o de dominio y reutilizarlos. La única herramienta que no nos permitió hacer esto fue la revisión de código. Pero a veces también fracasó.

Luego comenzamos a pasar a un modelo con repositorios separados. La lógica empresarial ya no fluye de un servicio a otro, los dominios se han vuelto verdaderamente independientes. Los contextos acotados se admiten más claramente. ¿Cómo reutilizamos las bibliotecas de infraestructura? Los separamos en un repositorio separado y luego los colocamos en paquetes Nuget, que luego colocamos en Artifactory. Ante cualquier cambio, el montaje y publicación se produce de forma automática.

La transición del monolito a los microservicios: historia y práctica

Nuestros servicios comenzaron a hacer referencia a los paquetes de infraestructura internos de la misma manera que a los externos. Descargamos bibliotecas externas de Nuget. Para trabajar con Artifactory, donde colocamos estos paquetes, utilizamos dos administradores de paquetes. En repositorios pequeños también utilizamos Nuget. En repositorios con múltiples servicios, utilizamos Paket, que proporciona más coherencia de versiones entre módulos.

La transición del monolito a los microservicios: historia y práctica

Así, trabajando en el código fuente, cambiando ligeramente la arquitectura y separando los repositorios, conseguimos que nuestros servicios sean más independientes.

Problemas de infraestructura


La mayoría de las desventajas de pasar a microservicios están relacionadas con la infraestructura. Necesitará una implementación automatizada y nuevas bibliotecas para ejecutar la infraestructura.

Instalación manual en ambientes.

Inicialmente, instalamos la solución para entornos manualmente. Para automatizar este proceso, creamos una canalización de CI/CD. Elegimos el proceso de entrega continua porque la implementación continua aún no es aceptable para nosotros desde el punto de vista de los procesos comerciales. Por lo tanto, el envío para funcionamiento se realiza mediante un botón y para prueba, de forma automática.

La transición del monolito a los microservicios: historia y práctica

Usamos Atlassian, Bitbucket para el almacenamiento del código fuente y Bamboo para la construcción. Nos gusta escribir scripts de compilación en Cake porque es lo mismo que C#. Los paquetes listos para usar llegan a Artifactory y Ansible llega automáticamente a los servidores de prueba, después de lo cual se pueden probar inmediatamente.

La transición del monolito a los microservicios: historia y práctica

Registro separado


Hubo un tiempo en que una de las ideas del monolito era proporcionar tala compartida. También necesitábamos comprender qué hacer con los registros individuales que se encuentran en los discos. Nuestros registros se escriben en archivos de texto. Decidimos utilizar una pila ELK estándar. No escribimos a ELK directamente a través de los proveedores, pero decidimos modificar los registros de texto y escribir en ellos el ID de seguimiento como identificador, agregando el nombre del servicio, para que estos registros pudieran analizarse más adelante.

La transición del monolito a los microservicios: historia y práctica

Al usar Filebeat, tenemos la oportunidad de recopilar nuestros registros de los servidores, luego transformarlos, usar Kibana para crear consultas en la interfaz de usuario y ver cómo se realizó la llamada entre servicios. Trace ID ayuda mucho con esto.

Servicios relacionados con pruebas y depuración


Inicialmente, no entendíamos completamente cómo depurar los servicios que se estaban desarrollando. Todo fue simple con el monolito; lo ejecutamos en una máquina local. Al principio intentaron hacer lo mismo con los microservicios, pero a veces para iniciar completamente un microservicio es necesario iniciar varios otros, lo cual resulta inconveniente. Nos dimos cuenta de que necesitamos pasar a un modelo en el que dejemos en la máquina local solo el servicio o servicios que queremos depurar. Los servicios restantes se utilizan desde servidores que coinciden con la configuración con prod. Después de la depuración, durante la prueba, para cada tarea, solo se envían al servidor de prueba los servicios modificados. De este modo, la solución se prueba en la forma en que aparecerá en producción en el futuro.

Hay servidores que solo ejecutan versiones de producción de los servicios. Estos servidores son necesarios en caso de incidencias, para comprobar la entrega antes del despliegue y para formación interna.

Hemos agregado un proceso de prueba automatizado utilizando la popular biblioteca Specflow. Las pruebas se ejecutan automáticamente usando NUnit inmediatamente después de la implementación desde Ansible. Si la cobertura de la tarea es completamente automática, no es necesario realizar pruebas manuales. Aunque a veces todavía se requieren pruebas manuales adicionales. Usamos etiquetas en Jira para determinar qué pruebas ejecutar para un problema específico.

Además, ha aumentado la necesidad de realizar pruebas de carga, que antes sólo se realizaban en casos excepcionales. Usamos JMeter para ejecutar pruebas, InfluxDB para almacenarlas y Grafana para crear gráficos de procesos.

¿Qué hemos logrado?


En primer lugar, nos deshicimos del concepto de "liberación". Atrás quedaron los monstruosos lanzamientos de dos meses cuando este coloso se implementó en un entorno de producción, interrumpiendo temporalmente los procesos comerciales. Ahora implementamos servicios en promedio cada 1,5 días, agrupándolos porque entran en funcionamiento después de la aprobación.

No hay fallas fatales en nuestro sistema. Si lanzamos un microservicio con un error, la funcionalidad asociada a él se interrumpirá y el resto de funciones no se verán afectadas. Esto mejora enormemente la experiencia del usuario.

Podemos controlar el patrón de implementación. Podrás seleccionar grupos de servicios por separado del resto de la solución, si es necesario.

Además, hemos reducido significativamente el problema con una gran cola de mejoras. Ahora contamos con equipos de productos independientes que trabajan con algunos de los servicios de forma independiente. El proceso Scrum ya encaja bien aquí. Un equipo específico puede tener un propietario de producto independiente que le asigna tareas.

Resumen

  • Los microservicios son muy adecuados para descomponer sistemas complejos. En el proceso, comenzamos a comprender qué hay en nuestro sistema, qué contextos limitados existen, dónde se encuentran sus límites. Esto le permite distribuir correctamente las mejoras entre los módulos y evitar confusión de código.
  • Los microservicios proporcionan beneficios organizacionales. A menudo se habla de ellos sólo como arquitectura, pero cualquier arquitectura es necesaria para resolver las necesidades comerciales, y no por sí sola. Por lo tanto, podemos decir que los microservicios son muy adecuados para resolver problemas en equipos pequeños, dado que Scrum es muy popular ahora.
  • La separación es un proceso iterativo. No se puede tomar una aplicación y simplemente dividirla en microservicios. Es poco probable que el producto resultante sea funcional. Al dedicar microservicios, es beneficioso reescribir el legado existente, es decir, convertirlo en un código que nos guste y que satisfaga mejor las necesidades del negocio en términos de funcionalidad y velocidad.

    Una pequeña advertencia: Los costos de pasar a microservicios son bastante significativos. Tomó mucho tiempo resolver el problema de infraestructura por sí solo. Entonces, si tiene una aplicación pequeña que no requiere un escalamiento específico, a menos que tenga una gran cantidad de clientes compitiendo por la atención y el tiempo de su equipo, entonces es posible que los microservicios no sean lo que necesita hoy. Es bastante caro. Si comienza el proceso con microservicios, los costos inicialmente serán más altos que si comienza el mismo proyecto con el desarrollo de un monolito.

    PD: Una historia más emotiva (y como para ti personalmente), según enlace.
    Aquí está la versión completa del informe.

Fuente: habr.com

Añadir un comentario