9 años en un monolito en Node.JS

monolito de https://reneaigner.deviantart.com


Hace una semana, hablé en una reunión de Node.JS, y prometí a muchos publicar una grabación de la actuación. Más tarde me di cuenta de que no podía acomodar algunos hechos interesantes en una media hora regulada. Sí, y yo prefiero leer, en lugar de mirar y escuchar, así que decidí publicar mi presentación en el formato de un artículo. Sin embargo, el video también estará al final de la publicación en la sección de enlaces.


Decidí hablarte sobre un punto doloroso: la vida en un monolito. Ya hay cientos de artículos sobre esto en el centro, miles de copias están rotas en los comentarios, la verdad hace mucho que murió en controversia, pero ... El hecho es que tenemos una experiencia muy específica en OneTwoTrip, a diferencia de muchas personas que escriben sobre ciertos patrones arquitectónicos en vacío:


  • En primer lugar, nuestro monolito ya tiene 9 años.
  • En segundo lugar, pasó toda su vida bajo alta carga (ahora son 23 millones de solicitudes por hora).
  • Y en NaN, escribimos nuestro monolito en Node.JS, que ha cambiado más allá del reconocimiento en los últimos 9 años. Sí, comenzamos a escribir en el nodo en 2010, ¡cantamos una canción para el frenesí de los valientes!

Así que tenemos mucha especificidad y experiencia real. Interesante? Vamos!


Tiempos de exención de responsabilidad


Esta presentación refleja solo la opinión privada de su autor. Puede coincidir con la posición de OneTwoTrip, o puede no coincidir. Así de afortunado. Trabajo como experto técnico de uno de los equipos de la compañía y no pretendo ser objetivo ni expresar la opinión de alguien más que la mía.

Descargo de responsabilidad dos


Este artículo describe eventos históricos y, por el momento, todo está completamente mal, así que no se alarme.

0. ¿Cómo sucedió?


La tendencia de consulta de la palabra "microservicio" en google:

Todo es muy simple: hace nueve años, nadie sabía sobre microservicios. Entonces comenzamos a escribir, como todos los demás, en un monolito.


1. Dolor en el monolito.


Aquí describiré las situaciones problemáticas que hemos tenido durante estos 9 años. Algunos de ellos fueron resueltos, otros fueron burlados por hacks, algunos simplemente perdieron relevancia. Pero el recuerdo de ellos, como las cicatrices de batalla, nunca me dejará.


1.1 Actualización de componentes conectados



Este es el caso cuando la sinergia es malvada. Debido a que cualquier componente se reutilizó varios cientos de veces, y si era posible usarlo torcidamente, entonces no se perdió. Cualquier acción puede causar efectos completamente impredecibles, y no todos ellos son rastreados por unidades y pruebas de integración. ¿Recuerdas la historia de trapeadores, un fan y un globo? Si no, búscalo en google. Ella es la mejor ilustración del código en el monolito.


1.2 Migración a nuevas tecnologías.


Quieres Express? Linter? ¿Otro marco para pruebas o simulacros? ¿Validador de actualizaciones o al menos lodash? ¿Actualizar Node.js? Lo siento Para hacer esto, debe editar miles de líneas de código.


Muchos hablan de la ventaja del monolito, que es que cualquier revisión es un compromiso atómico . Estas personas guardan silencio sobre una cosa : esta revisión nunca se hará .


¿Conoces el viejo chiste sobre las versiones semánticas?


La semántica real de las versiones semánticas:

mayor = un cambio radical
menor = un cambio de ruptura menor
parche = un pequeño cambio de ruptura

Ahora imagine que cualquier cambio de ruptura pequeño aparecerá en su código. No, es posible vivir con eso, y periódicamente reunimos fuerzas y migramos, pero fue realmente muy difícil. Muy


1.3 Lanzamientos


Aquí debo decir sobre algunos detalles de nuestro producto. Tenemos una gran cantidad de integraciones externas y varias ramas de la lógica empresarial que surgen por separado con bastante poca frecuencia. Realmente envidio los productos que realmente ejecutan todas las ramas de su código en 10 minutos en producción, pero este no es el caso aquí. Mediante prueba y error, encontramos un ciclo de lanzamiento óptimo que minimiza la cantidad de errores que llegan a los usuarios finales:


  1. el lanzamiento está en marcha y las pruebas de integración pasan en medio día
  2. entonces está bajo una cuidadosa supervisión en el escenario durante un día (para el 10% de los usuarios)
  3. luego descansa otro día en producción bajo una supervisión aún más cuidadosa.
  4. Y solo después de eso le damos luz verde en el maestro.

Dado que amamos a nuestros colegas y no lanzamos los viernes, al final esto significa que el lanzamiento va al maestro aproximadamente 1.5-2 veces por semana. Lo que lleva al hecho de que el lanzamiento puede tener 60 tareas o más. tal cantidad causa conflictos de fusión, efectos sinérgicos repentinos, carga de trabajo completa de control de calidad en el análisis de registros y otras penas. En general, fue muy difícil para nosotros liberar un monolito.


1.4 Solo mucho código


Parece que la cantidad de código no debería ser de importancia fundamental. Pero ... no realmente. En el mundo real es:


  • Umbral de entrada más alto
  • Enormes artefactos de construcción para cada tarea
  • Procesos de CI largos, que incluyen pruebas de integración, pruebas unitarias e incluso código linting
  • Trabajo lento de IDE (en los albores del desarrollo de Jetbrains los sorprendimos con nuestros registros más de una vez)
  • Búsqueda contextual sofisticada (no lo olvide, no tenemos escritura estática)
  • Dificultad para encontrar y eliminar código no utilizado

1.5 Propietarios de códigos faltantes


Muy a menudo, las tareas surgen con una esfera de responsabilidad incomprensible, por ejemplo, en bibliotecas relacionadas. Y el desarrollador original ya podría mudarse a otro equipo, o incluso abandonar la empresa. La única forma de encontrar responsables en este caso es la arbitrariedad administrativa: tomar y nombrar a una persona. Lo que no siempre es agradable tanto para el desarrollador como para el que lo hace.


1.6 Dificultad de depuración


¿Está fluyendo la memoria? ¿Mayor consumo de CPU? ¿Quería construir gráficos de llamas? Lo siento En un monolito, suceden tantas cosas al mismo tiempo que se vuelve increíblemente difícil localizar un problema. Por ejemplo, para comprender cuál de las 60 tareas cuando se implementa en producción produce un mayor consumo de recursos (aunque esto no se reproduce localmente en entornos de prueba y preparación) es casi irreal.


1.7 Una pila


Por un lado, es bueno cuando todos los desarrolladores "hablan" el mismo idioma. En el caso de JS, resulta que incluso los desarrolladores de Backend with Frontend se entienden entre sí. Pero ...


  • No hay una bala de plata, y para algunas tareas a veces quieres usar otra cosa. Pero tenemos un monolito y no tenemos dónde adherir a otros desarrolladores.
  • No podemos simplemente tomar un buen equipo con la recomendación, que nos llegó por consejo de amigos, no tenemos dónde ponerla.
  • Con el tiempo, descansamos en el hecho de que el mercado simplemente no tiene suficientes desarrolladores en la pila correcta.

1.8 Muchos equipos con diferentes ideas sobre la felicidad.



Si tiene dos desarrolladores, entonces ya tiene dos ideas diferentes sobre cuál es el mejor marco, qué estándares deben respetarse, usar bibliotecas, etc.
Si tiene diez equipos, cada uno de los cuales tiene varios desarrolladores, entonces esto es solo un desastre.
Y solo hay dos maneras de resolverlo: “democrático” (cada uno hace lo que quiere) o totalitario (los estándares se imponen desde arriba). En el primer caso, la calidad y la estandarización sufren, en el segundo, personas a las que no se les permite realizar su idea de felicidad.


2. Las ventajas del monolito.


Por supuesto, hay algunas ventajas en el monolito que pueden ser diferentes para diferentes pilas, productos y equipos. Por supuesto, hay mucho más que tres, pero no seré responsable de todo lo posible, solo de aquellos que fueron relevantes para nosotros.


2.1 Fácil de implementar


Cuando tiene un servicio, aumentarlo y probarlo es mucho más fácil que una docena de servicios. Es cierto que esta ventaja solo es relevante en la etapa inicial; por ejemplo, puede elevar el entorno de prueba y utilizar todos los servicios, excepto los desarrollados a partir de él. O de contenedores. O lo que sea.


2.2 Sin sobrecarga de transferencia de datos


Toda una ventaja dudosa, si no tienes mucha carga. Pero tenemos un caso así, por lo tanto, el costo de transporte entre microservicios es notable para nosotros. No importa cómo intente hacer esto rápidamente, almacene y transfiera todo en RAM más rápido que cualquier otra cosa; esto es obvio.


2.2 Una asamblea


Si necesita retroceder en algún momento de la historia, hacerlo con un monolito es realmente simple: tómelo y retroceda. En el caso de los microservicios, es necesario seleccionar versiones compatibles de servicios que se utilizaron entre sí en un momento determinado, lo que no siempre puede ser simple. Es cierto que esto también se resuelve con la ayuda de la infraestructura.


3. Ventajas imaginarias de un monolito


Aquí tomé todas esas cosas que generalmente se consideran ventajas, pero desde mi punto de vista no lo son.


3.1 Código: esta es la documentación


A menudo escuchó esta opinión. Pero generalmente es seguido por desarrolladores novatos que no han visto archivos en decenas de miles de líneas de código escritas por personas que se fueron hace años. Bueno, por alguna razón, la mayoría de las veces este elemento se agrega a favor de los partidarios del monolito: dicen que no necesitamos documentación, no tenemos ningún transporte o API, todo está en el código, es fácil y claro. No discutiré con esta declaración, solo diré que no creo en ella.


3.2 No existen versiones diferentes de bibliotecas, servicios y API. No hay repositorios diferentes.


Si Pero no Porque a segunda vista entiendes que el servicio no existe en el vacío. Y entre una gran cantidad de otros códigos y productos con los que se integra, comenzando desde bibliotecas de terceros, continuando con versiones de software de servidor y no terminando con integraciones externas, versiones IDE, herramientas de CI, etc. Y tan pronto como comprenda cuántas cosas versionadas diferentes incluye su servicio indirectamente, inmediatamente queda claro que esta ventaja es solo demagogia.


3.3 monitoreo más fácil


Más fácil Porque tienes, aproximadamente, un tablero de instrumentos, en lugar de unas pocas docenas. Pero es más complicado, y a veces incluso imposible, porque no puede descomponer sus gráficos en diferentes partes del código, y solo tiene la temperatura promedio en el hospital. En general, ya dije todo en el párrafo sobre la complejidad de la depuración, solo aclararé que la misma complejidad se aplica al monitoreo.


3.4 Es más fácil cumplir con estándares comunes


Si Pero, como ya escribí en el párrafo sobre muchos equipos con la idea de la felicidad, los estándares se imponen de forma totalitaria o se debilitan casi en ausencia de ellos.


3.5 Menos posibilidades de duplicación de código


La extraña opinión es que el código no está duplicado en el monolito. Pero lo conocí con bastante frecuencia. En mi práctica, resulta que la duplicación de código depende únicamente de la cultura de desarrollo en la empresa. Si es así, el código general se asigna a todo tipo de bibliotecas, módulos y microservicios. Si no está allí, habrá una copia y pegar veinte veces en el monolito.


4. Pros de microservicios


Ahora escribiré sobre lo que obtuvimos después de la migración. Nuevamente, estas son conclusiones reales de una situación real.


4.1 Puedes hacer una infraestructura heterogénea


Ahora podemos escribir código en una pila que sea óptima para resolver un problema específico. Y es racional utilizar los buenos desarrolladores que vinieron a nosotros. Por ejemplo, aquí hay una lista de muestra de tecnologías que tenemos actualmente:


4.2 Puedes hacer muchos lanzamientos frecuentes


Ahora podemos hacer muchos lanzamientos independientes pequeños, y son más simples, más rápidos y no son dolorosos. Una vez teníamos un solo equipo, pero ahora ya hay 18. Hubiera habido un descanso si todos permanecieran en el monolito. O las personas responsables de ello ...


4.3 Más fácil de hacer pruebas independientes


Hemos reducido el tiempo para las pruebas de integración, que ahora solo prueban lo que realmente ha cambiado, y al mismo tiempo no tenemos miedo de los efectos de la sinergia repentina. Por supuesto, tuve que comenzar a caminar alrededor del rastrillo, por ejemplo, aprender a hacer API compatibles con versiones anteriores, pero con el tiempo, todo se calmó.


4.4 Más fácil de implementar y probar nuevas características


Ahora estamos abiertos a la experimentación. Cualquier marco, pila, biblioteca: puede probarlo todo y, si tiene éxito, seguir adelante.


4.5 Puedes actualizar cualquier cosa


Puede actualizar la versión del motor, las bibliotecas, ¡pero cualquier cosa! Como parte de un pequeño servicio, encontrar y corregir todos los cambios importantes es cuestión de minutos. Y no semanas, como era antes.


4.6 Y no puedes actualizar


Por extraño que parezca, esta es una de las mejores características de los microservicios. Si tiene un código de trabajo estable, puede congelarlo y olvidarse de él. Y nunca tendrá que actualizarlo, por ejemplo, para ejecutar el código del producto en un nuevo motor. El producto en sí funciona en un nuevo motor, y el microservicio continúa viviendo como vivía. Las moscas con chuletas finalmente se pueden comer por separado.


5 Contras de microservicios


Por supuesto, una mosca en la pomada no estaba completa, y una solución perfecta para simplemente sentarse y recibir el pago no funcionó. ¿Qué encontramos?


5.1 Necesita un bus para el intercambio de datos y el registro claro.


La interacción de los servicios a través de HTTP es un modelo clásico y, en general, incluso uno funcional, siempre que haya capas de registro y equilibrio entre ellos. Pero es mejor tener un neumático más distintivo. Además, debe pensar en cómo recolectar y combinar registros entre ellos; de lo contrario, solo tendrá gachas en sus manos.


5.2 Mantenga un registro de lo que están haciendo los desarrolladores.


En general, esto siempre debe hacerse, pero en los microservicios, los desarrolladores obviamente tienen más libertad, lo que a veces puede dar lugar a cosas de las que Stephen King se pone nervioso. Aunque exteriormente parezca que el servicio está funcionando, no olvide que debe haber una persona que controle lo que hay dentro de él.


5.3 Necesita un buen equipo de DevOps para gestionarlo todo.


Casi cualquier desarrollador puede implementar un monolito de una forma u otra y cargar sus versiones (por ejemplo, a través de FTP o SSH, lo vi). Pero con los microservicios, hay todo tipo de servicios centralizados para recopilar registros, métricas, paneles, cocineros para administrar configuraciones, voltios, jenkins y otras cosas buenas, sin las cuales generalmente puede vivir, pero es malo e incomprensible por qué. Entonces, para administrar microservicios, necesita tener un buen equipo de DevOps.


5.4 Puedes tratar de atrapar una exageración y dispararte en el pie.


Quizás este sea el principal inconveniente de la arquitectura y su peligro. Muy a menudo, las personas siguen ciegamente las tendencias y comienzan a introducir arquitectura y tecnología sin entenderlo. Después de lo cual todo cae, se confunden en el desorden resultante y escriben un artículo sobre el Habr "cómo pasamos de microservicios a un monolito", por ejemplo. En general, muévase solo si sabe por qué está haciendo esto y qué problemas resolverá. Y cuáles obtienes.


6 caqui en monolito


Algunos de los hacks que nos permitieron vivir en un monolito son un poco mejores y un poco más largos.


6.1 Linting


La introducción de un linter en un monolito no es una tarea tan simple como parece a primera vista. Por supuesto, puede establecer reglas estrictas, agregar una configuración y ... Nada cambiará, todos simplemente apagan el linter, porque la mitad del código se vuelve rojo.


Para introducir gradualmente el linting, escribimos un complemento simple para eslint, slowlint , que le permite hacer una cosa simple: contener una lista de archivos ignorados temporalmente. Como resultado:


  • Todo el código incorrecto se resalta en el IDE
  • Los nuevos archivos se crean de acuerdo con las reglas de linting, de lo contrario, CI no los perderá
  • Los viejos gradualmente gobiernan y se alejan de las excepciones.

A lo largo del año, fue posible incorporar aproximadamente la mitad del código monolítico en un solo estilo, es decir, casi todo el código agregado activamente.


6.2 Mejoras de prueba de unidad


Una vez que se realizaron pruebas unitarias con nosotros durante tres minutos. Los desarrolladores no querían esperar tanto tiempo, por lo que todo se verificó solo en CI en el servidor. Después de un tiempo, el desarrollador descubrió que las pruebas cayeron, maldijo, abrió una rama, volvió al código ... En general, sufrió. ¿Qué hemos hecho con esto?


  1. Para empezar, comenzamos a ejecutar pruebas multiproceso. Yandex tiene una variante de moca multihilo, pero con nosotros no despegó, por lo que ellos mismos escribieron un contenedor simple. Las pruebas comenzaron a realizarse una vez y media más rápido.
  2. Luego pasamos de 0.12 nodos al octavo (sí, el proceso mismo se basa en un informe separado). Por extraño que parezca, no dio una ganancia fundamental en productividad en la producción, pero las pruebas comenzaron a realizarse un 20% más rápido.
  3. Y luego nos sentamos para depurar las pruebas y optimizarlas individualmente. Lo que dio el mayor aumento de velocidad.

En general, en este momento, las pruebas unitarias se ejecutan en el gancho de preparación y funcionan en 10 segundos, lo cual es bastante cómodo y le permite ejecutarlas sin interrupción de la producción.


6.3 Aligerando el peso del artefacto


El artefacto monolítico finalmente tomó 400 megabytes. Teniendo en cuenta el hecho de que se crea para cada confirmación, los volúmenes totales resultaron ser bastante grandes. El módulo de diarrea , un tenedor del módulo modclean , nos ayudó con esto. Eliminamos las pruebas unitarias del artefacto y lo limpiamos de varios tipos de basura, como archivos léame, pruebas dentro de paquetes, etc. ¡La ganancia fue de aproximadamente el 30% del peso!


6.4 Almacenamiento en caché de dependencias


Anteriormente, instalar dependencias usando npm tomaba tanto tiempo que no solo se podía tomar café, sino también, por ejemplo, hornear pizza. Por lo tanto, al principio usamos el módulo npm-cache , que estaba bifurcado y un poco terminado. Le permitía almacenar dependencias en una unidad de red compartida, de la cual todas las otras compilaciones lo tomarían.


Luego pensamos en la reproducibilidad de los ensamblajes. Cuando tienes un monolito, entonces el cambio de dependencias transitivas es el azote de Dios. Dado el hecho de que estábamos muy por detrás de la versión del motor en ese momento, cambiar alguna dependencia de quinto nivel fácilmente rompió todo nuestro ensamblaje. Entonces comenzamos a usar npm-shrinkwrap. Ya era más fácil con él, aunque fusionar sus cambios era un placer para los fuertes de espíritu.


Y luego finalmente apareció package-lock y el excelente comando npm ci , que se ejecutó a una velocidad ligeramente menor que la instalación de dependencias desde la caché de archivos. Por lo tanto, comenzamos a usarlo solo y dejamos de almacenar ensambles de dependencia. En este día traje a trabajar varias cajas de donas.


6.5 Distribución del orden de los lanzamientos.


Y esto es más un truco administrativo, no técnico. Inicialmente, estaba en contra de él, pero el tiempo demostró que el segundo experto técnico tenía razón y, en general, estaba bien hecho. Cuando los lanzamientos se distribuyeron por turnos entre varios equipos, quedó claro dónde aparecían exactamente los errores y, lo que es más importante, cada equipo sintió su responsabilidad por la velocidad e intentó resolver los problemas y desplegarse lo más rápido posible.


6.6 Eliminar código muerto


En un monolito, es muy aterrador eliminar el código; nunca se sabe dónde podría quedar atrapado en él. Por lo tanto, la mayoría de las veces solo queda acostarse de lado. Con los años E incluso el código muerto debe ser compatible, sin mencionar la confusión que presenta. Por lo tanto, con el tiempo, comenzamos a usar require-analyse para una búsqueda superficial de código muerto, y pruebas de integración más el lanzamiento en el modo de verificación de cobertura para una búsqueda más profunda.


7 corte monolítico


Por alguna razón, muchas personas piensan que para cambiar a microservicios, debe abandonar su monolito, escribir un montón de microservicios desde cero, comenzar todo de una vez, y habrá felicidad. Pero este modelo ... Hmm ... está cargado con el hecho de que no harás nada, y solo gastarás mucho tiempo y dinero en escribir el código que tienes que tirar.


Propongo otra opción, que me parece más funcional, y que se implementó con nosotros:


  1. Comenzamos a escribir nuevos servicios en microservicios. Ejecutamos la tecnología, nos subimos al rastrillo, entendemos si queremos hacerlo.
  2. Extraemos el código en módulos, bibliotecas o lo que se use allí.
  3. Distinguimos los servicios de un monolito.
  4. Distinguimos los microservicios de los servicios. Sin prisas y uno a la vez.

8 y finalmente


foto tomada de https://fvl1-01.livejournal.com/


Al final, decidí dejar lo más importante.


Recuerda:


  • No eres google
  • No eres microsoft
  • No eres facebook
  • No eres Yandex
  • No eres netflix
  • No eres OneTwoTrip

Si algo funciona en otras compañías, no es un hecho que lo beneficie. Si intenta copiar ciegamente la experiencia de otras compañías con las palabras "funciona para ellos", entonces esto probablemente terminará mal. Cada empresa, cada producto y cada equipo es único. Lo que funciona para algunos no funcionará para otros. No me gusta decir cosas obvias, pero muchas personas comienzan a construir un culto de carga alrededor de otras compañías, copiando ciegamente enfoques y enterrándose bajo falsas decoraciones de árboles de Navidad. No lo hagas. Experimente, intente, desarrolle aquellas soluciones que sean óptimas para usted. Y solo entonces todo saldrá bien.


Enlaces utiles:


Source: https://habr.com/ru/post/459206/


All Articles