Hablar sobre la actualización en caliente de Node.js y comprender algunas fugas de memoria comunes

Dachang Technology Advanced Front-end Nodo Avanzado

Haga clic en la guía de crecimiento del programador superior, preste atención al número público

Respuesta 1, únete al grupo de intercambio de nodos avanzado

Recuerdo que cuando Node.js recién comenzaba en 2015 y 16, también me preguntaron cómo implementar la actualización en caliente de los servicios de Node.js en la entrevista de trabajo en mi antiguo empleador.

De hecho, a Noder, que fue transferido de Php-fpm / Fast-cgi en los primeros días, definitivamente le gusta este esquema de implementación que actualiza el código de lógica empresarial sin reiniciar el servidor. Sus ventajas también son muy obvias:

  • No es necesario reiniciar el servicio, lo que significa que la conexión del usuario no se interrumpirá, especialmente para aplicaciones con una gran cantidad de enlaces prolongados.

  • La caché de carga de actualización de archivos es un proceso muy rápido que puede completar las actualizaciones de la aplicación en milisegundos

También hay muchos efectos secundarios de las actualizaciones activas, como fugas de memoria comunes (fugas de recursos).Este artículo utilizará clear-module y decache, dos módulos auxiliares de actualización activa populares con descargas relativamente altas, para discutir qué actualización activa le dará a nuestra aplicación .que problemas surgen.

El principio del reemplazo térmico.

Antes de comenzar a hablar sobre actualizaciones importantes, primero debemos comprender la descripción general del mecanismo del módulo de Node.js, para que podamos tener una comprensión más profunda de los problemas que trae más adelante.

El mecanismo de carga de módulos implementado por el propio Node.js se muestra en la siguiente figura:

113d11d909ffd496d17759332f9dd259.png
En pocas palabras, los pasos para que el módulo principal A introduzca el submódulo B son los siguientes:

  • Determinar si existe caché del submódulo B

  • Si no existe, compile y analice B

    • Agregar caché del módulo B a require.cache(donde la clave es la ruta completa del módulo B)

    • Agregue la referencia del módulo B a la matriz del módulo principal childrenA

  • Si existe, juzgue si B existe en la matriz del módulo principal A children, si no, agregue la referencia del módulo B.

De hecho, en este punto, ya podemos encontrar que para lograr una actualización en caliente sin pérdida de memoria, necesitamos desconectar los siguientes enlaces de referencia del módulo que se actualizará en caliente:

698c19e1b1382cb76ba2b653d2cc4310.png
De esta forma, cuando volvamos al requiresubmódulo B, el contenido del módulo B se leerá del disco nuevamente y luego se compilará e importará a la memoria, dándose cuenta de la capacidad de actualización en caliente.

De hecho, los módulos mencionados en la primera sección clear-moduley los decachedos paquetes se implementan de acuerdo con esta idea, por supuesto, se considerarán más perfectos, como limpiar las dependencias del propio submódulo B y bucles for. la escena.

Entonces, con la ayuda de estos dos módulos, ¿la actualización en caliente de la aplicación Node.js es perfecta? Vamos a ver.

Problema 1: pérdida de memoria

La fuga de memoria es un problema muy interesante. Todos los estudiantes que ingresan al área de aguas profundas del desarrollo full-stack de Node.js básicamente se encontrarán con el problema de la fuga de memoria. Desde mi experiencia personal en la resolución de problemas y el posicionamiento, los desarrolladores no necesitan tenga miedo de las fugas de memoria, porque en comparación con otros problemas oscuros, las fugas de memoria son un tipo de falla que es 100% solucionable siempre que esté familiarizado con el código y se tome el tiempo.

Aquí, echemos un vistazo a la solución de actualización en caliente que parece borrar todas las referencias de módulos antiguos y qué tipo de fugas de memoria se producirán.

decaché

Considere construir el siguiente ejemplo de revisión, usándolo decachepara probar primero:

'use strict';

const cleanCache = require('decache');

let mod = require('./update_mod.js');
mod();
mod();

setInterval(() => {
  cleanCache('./update_mod.js');
  mod = require('./update_mod.js');
  mod();
}, 100);

En este ejemplo, es equivalente a limpiar continuamente ./update_mod.jsel caché de este módulo para la actualización en caliente, su contenido es el siguiente:

'use strict';

const array = new Array(10e5).fill('*');
let count = 0;

module.exports = () => {
  console.log('update_mod', ++count, array.length);
};

Para observar rápidamente el fenómeno de pérdida de memoria, aquí se construye una matriz grande para reemplazar la referencia de cierre de módulo normal.

Para la conveniencia de la observación, podemos index.jsagregar un método para imprimir el estado actual de la memoria con regularidad:

function printMemory() {
  const { rss, heapUsed } = process.memoryUsage();
  console.log(`rss: ${(rss / 1024 / 1024).toFixed(2)}MB, heapUsed: ${(heapUsed / 1024 / 1024).toFixed(2)}MB`);
}

printMemory();
setInterval(printMemory, 1000);

Finalmente, ejecute el node index.jsarchivo y podrá ver que la memoria se desborda rápidamente:

update_mod 1 1000000
update_mod 2 1000000
rss: 34.59MB, heapUsed: 11.51MB
update_mod 1 1000000
rss: 110.20MB, heapUsed: 80.09MB
update_mod 1 1000000
...

rss: 921.63MB, heapUsed: 888.99MB
update_mod 1 1000000
rss: 998.09MB, heapUsed: 965.12MB
update_mod 1 1000000
update_mod 1 1000000

<--- Last few GCs --->

[50524:0x158008000]    13860 ms: Scavenge 1018.3 (1024.6) -> 1018.3 (1028.6) MB, 2.3 / 0.0 ms  (average mu = 0.783, current mu = 0.576) allocation failure 
[50524:0x158008000]    14416 ms: Mark-sweep (reduce) 1026.0 (1036.3) -> 1025.9 (1029.3) MB, 457.8 / 0.0 ms  (+ 86.6 ms in 77 steps since start of marking, biggest step 8.7 ms, walltime since start of marking 555 ms) (average mu = 0.670, current mu = 0.360

<--- JS stacktrace --->

FATAL ERROR: Reached heap limit Allocation failed - JavaScript heap out of memory

Análisis después de capturar la instantánea del montón:
3ebdec069b9bee624088333b8510a17b.png
es obvio que los resultados de la compilación de una gran cantidad de módulos de actualización en caliente repetidos en Module@39215la matriz provocan pérdidas de memoria y una mayor inspección de la información: puede ver que es la entrada .childrenupdate_mod.jsModule@39215
f13e8534897fd53a273972285c40d733.png
index.js

Después de leer decacheel código fuente de la implementación, se descubre que el motivo de la fuga es que necesitamos eliminar las tres referencias mencionadas en la sección sobre el principio de la implementación de actualizaciones en caliente, pero desafortunadamente solo el enlace de referencia más básico decacheaún está desconectado :require.cache
42a4a3201afbaf5d5194050dd84da6e8.png

Hasta ahora, decachedado que el problema de memoria de actualización en caliente más básico no se ha resuelto, el volumen de descarga mensual de 94w se ha cegado y podemos excluir directamente nuestra solución de actualización en caliente como referencia.

Referirse a:

  • La ubicación real del código fuente del problema de decaché: https://github.com/dwyl/decache/blob/main/decache.js#L35

módulo claro

A continuación, veamos cómo el volumen de descarga mensual es de 19w clear-module.

Dado que el código de prueba en la sección anterior representa el escenario de cambio en caliente del módulo más básico, y el clear-moduleuso de la API es básicamente el mismo, podemos realizar esta ronda de prueba decachereemplazando solo la referencia:cleanCache

// index.js
const cleanCache = require('clear-module');

También ejecute el node index.jsarchivo, puede ver los cambios en la memoria de la siguiente manera:

update_mod 1 1000000
update_mod 2 1000000
rss: 35.00MB, heapUsed: 11.58MB
update_mod 1 1000000
rss: 110.69MB, heapUsed: 80.10MB
update_mod 1 1000000
rss: 187.36MB, heapUsed: 156.52MB
update_mod 1 1000000
rss: 256.28MB, heapUsed: 225.26MB
update_mod 1 1000000
rss: 332.78MB, heapUsed: 301.71MB
update_mod 1 1000000
rss: 401.61MB, heapUsed: 370.38MB
update_mod 1 1000000
rss: 42.67MB, heapUsed: 11.17MB
update_mod 1 1000000
rss: 65.63MB, heapUsed: 34.15MB
update_mod 1 1000000

Se puede encontrar aquí que la clear-moduletendencia de la memoria es ondulada, lo que indica que maneja perfectamente todas las referencias de los módulos antiguos mencionados en la sección principal, de modo que los módulos antiguos antes de la actualización en caliente se pueden GCear normalmente.

Después de revisar el código fuente, se encuentra clear-moduleque la referencia del módulo principal al submódulo también se borra:
acb5a3522ede13a1b8e27e84f070ef6f.png

por lo tanto, en este ejemplo, el calor no provocará la fuga de memoria del proceso OOM.

El código detallado se puede encontrar en: https://github.com/sindresorhus/clear-module/blob/main/index.js#L25-L31

Entonces, ¿crees clear-moduleque puedes sentarte y relajarte sin tener que preocuparte por la memoria?

De hecho, no, luego index.jshacemos algunas pequeñas modificaciones a lo anterior:

'use strict';

const cleanCache = require('clear-module');

let mod = require('./update_mod.js');
mod();
mod();

require('./utils.js');

setInterval(() => {
  cleanCache('./update_mod.js');
  mod = require('./update_mod.js');
  mod();
}, 100);

Comparado con el anterior añadido utils.js, su lógica es bastante sencilla:

'use strict';

require('./update_mod.js')

setInterval(() => require('./update_mod.js'), 100);

index.jsEl escenario correspondiente es en realidad que después de limpiar el middleware update_mod.js, el módulo que también se usa utils.jstambién se vuelve requirea introducir para seguir usando la lógica del módulo de actualización en caliente más reciente.

Continúe ejecutando el node index.jsarchivo, puede ver que esta vez la memoria se desborda rápidamente nuevamente:

update_mod 1 1000000
update_mod 2 1000000
rss: 34.59MB, heapUsed: 11.51MB
update_mod 1 1000000
rss: 110.20MB, heapUsed: 80.09MB
update_mod 1 1000000
...

rss: 921.63MB, heapUsed: 888.99MB
update_mod 1 1000000
rss: 998.09MB, heapUsed: 965.12MB
update_mod 1 1000000
update_mod 1 1000000

<--- Last few GCs --->

[53359:0x140008000]    13785 ms: Scavenge 1018.5 (1025.1) -> 1018.5 (1029.1) MB, 2.2 / 0.0 ms  (average mu = 0.785, current mu = 0.635) allocation failure 
[53359:0x140008000]    14344 ms: Mark-sweep (reduce) 1026.1 (1036.8) -> 1025.9 (1029.3) MB, 462.2 / 0.0 ms  (+ 87.7 ms in 89 steps since start of marking, biggest step 7.5 ms, walltime since start of marking 559 ms) (average mu = 0.667, current mu = 0.296

<--- JS stacktrace --->

FATAL ERROR: Reached heap limit Allocation failed - JavaScript heap out of memory

Continúe tomando instantáneas del montón para el análisis:

d88bfad5f7ccc86feb3d54df2add1b5c.png
Esta vez , hay una gran cantidad de módulos calientes repetidos debajo Module@37543de la childrenmatriz que upload_mod.jscausan fugas de memoria. Echemos un vistazo a Module@37543los detalles:

f5b3bba48a2201a377f8e0ec5409bc41.png
¿No es extraño que clear-modulela referencia del módulo principal al submódulo actualizado en caliente se haya limpiado (en este ejemplo, es index.jsel módulo principal), pero utils.jsaún se conservan tantas referencias antiguas?

De hecho, esto se debe a que, en el mecanismo de implementación del módulo de Node.js, el submódulo y el módulo principal están en una relación de muchos a muchos, y debido al mecanismo de almacenamiento en caché del módulo, el submódulo solo se ejecutará cuando se carga por primera vez Inicialización del constructor:

ab9720429893aaf1776620cdac8304fa.png
Esto significa que clear-modulela supuesta eliminación de la antigua referencia del módulo principal al módulo de actualización activa es solo la primera vez que se introduce el módulo principal correspondiente al módulo de actualización activa, en este caso index.js, por lo que la matriz index.jscorrespondiente childrenestá limpia.

Cuando utils.jsse introduce un módulo de actualización activa como módulo principal, se lee la memoria caché de la última versión del módulo de actualización activa y childrense actualiza la referencia:

b2f826d171cd6a08bdca733ef2d6bf91.png
Juzgará que si el objeto almacenado en caché childrenno existe en la matriz, se agregará Obviamente update_mod.js, el objeto de memoria obtenido por las dos compilaciones antes y después de la actualización en caliente no es el mismo, por lo que hay utils.jsuna fuga en el caché.

Hasta ahora, bajo la lógica un poco complicada, clear-moduletambién ha sido derrotado. Teniendo en cuenta que la carga lógica en el desarrollo real será mucho mayor que esto, es obvio que la actualización en caliente se usa en producción, a menos que el autor tenga un control exhaustivo de la mecanismo de módulo, de lo contrario, todavía está en Cavar un hoyo para usted y las generaciones futuras.

Deje un pensamiento interesante : clear-modulela fuga en este escenario no es irresoluble. Los estudiantes interesados ​​pueden pensar en cómo evitar la fuga de memoria de actualización en caliente en este escenario con referencia al principio.

Referirse a:

  • Establecer módulo principal: https://github.com/nodejs/node/blob/v16.13.2/lib/internal/modules/cjs/loader.js#L176

  • Referencia actualizada: https://github.com/nodejs/node/blob/v16.13.2/lib/internal/modules/cjs/loader.js#L167

lodash

Algunos estudiantes pueden pensar que el ejemplo anterior no es lo suficientemente típico. Veamos un caso de fuga de memoria causado por la carga repetida de un módulo subdependiente no idempotente que es completamente incontrolable para el desarrollador debido a una actualización en caliente.

Aquí, no salimos a buscar paquetes muy parciales para construir fugas de memoria.Tomemos un módulo de herramienta muy común con un volumen de descarga semanal de hasta 3900w   lodash como ejemplo, y sigamos modificando el nuestro  uploda_mod.js:

'use strict';

const lodash = require('lodash');
let count = 0;
module.exports = () => {
  console.log('update_mod', ++count);
};

Luego  index.js elimine lo anterior en  utils.js, siga  update_mod.js repitiendo solo la actualización en caliente:

'use strict';

const cleanCache = require('clear-module');

let mod = require('./update_mod.js');
mod();
mod();

setInterval(() => {
  cleanCache('./update_mod.js');
  mod = require('./update_mod.js');
  mod();
}, 10);

function printMemory() {
  const { rss, heapUsed } = process.memoryUsage();
  console.log(`rss: ${(rss / 1024 / 1024).toFixed(2)}MB, heapUsed: ${(heapUsed / 1024 / 1024).toFixed(2)}MB`);
}

printMemory();
setInterval(printMemory, 1000);

Luego ejecute el  node index.js archivo, puede ver que esta vez la doble fuga se filtró nuevamente.Con la  update_mod.js actualización en caliente, la memoria del montón aumenta rápidamente y finalmente OOM.

En este caso, el motivo de la fuga de submódulos no idempotentes es un poco más complicado, ya que implica la  lodash compilación y ejecución repetida del módulo, lo que provocará la referencia circular de cierre.

De hecho, se encontrará que la introducción de módulos es incontrolable para el desarrollador, es decir, el desarrollador no puede confirmar si ha importado un módulo público que  lodash puede ejecutarse de manera idempotente y provocar que genere una pérdida de memoria.

Problema 2: Fuga de recursos

Después de hablar sobre los escenarios de problemas de memoria que es más probable que sean causados ​​por el calor, echemos un vistazo a otro tipo de problemas de fuga de recursos relativamente irresolubles que es más probable que cause el calor.

Todavía usamos un ejemplo simple para ilustrar, en primer lugar construir index.js:

'use strict';

const cleanCache = require('clear-module');

let mod = require('./update_mod.js');

setInterval(() => {
  cleanCache('./update_mod.js');
  mod = require('./update_mod.js');
  console.log('-------- 热更新结束 --------')
}, 1000);

Esta vez, usamos directamente clear-modulela operación de actualización en caliente e introducimos los módulos para que se actualicen en caliente update_mod.jsde la siguiente manera:

'use strict';

const start = new Date().toLocaleString();

setInterval(() => console.log(start), 1000);

En update_mod.js, creamos una tarea cronometrada para generar la hora en que se introdujo el módulo por primera vez en intervalos de 1 segundo.

Al final de la ejecución, node index.jspuede ver los siguientes resultados:

2022/1/21 上午9:37:29
-------- 热更新结束 --------
2022/1/21 上午9:37:29
2022/1/21 上午9:37:30
-------- 热更新结束 --------
2022/1/21 上午9:37:29
2022/1/21 上午9:37:30
2022/1/21 上午9:37:31
-------- 热更新结束 --------
2022/1/21 上午9:37:29
2022/1/21 上午9:37:30
2022/1/21 上午9:37:31
2022/1/21 上午9:37:32
-------- 热更新结束 --------
2022/1/21 上午9:37:29
2022/1/21 上午9:37:30
2022/1/21 上午9:37:31
2022/1/21 上午9:37:32
2022/1/21 上午9:37:33
-------- 热更新结束 --------
2022/1/21 上午9:37:29
2022/1/21 上午9:37:30
2022/1/21 上午9:37:31
2022/1/21 上午9:37:32
2022/1/21 上午9:37:33
2022/1/21 上午9:37:34

Obviamente, clear-moduleaunque las referencias antiguas del módulo intercambiable en caliente se borran correctamente, las tareas cronometradas dentro del módulo antiguo no se reciclan juntas, lo que provoca una fuga de recursos.

De hecho, la tarea programada aquí es solo uno de los recursos, y varias operaciones de recursos del sistema, incluidas socket, y fd, no se pueden reciclar automáticamente en el escenario de solo borrar las referencias del módulo anterior.

Pregunta 3: ESM Miau Miau Miau?

No importa si es decacheo es clear-module, es una integración más lógica basada en el mecanismo del módulo CommonJS implementado por Node.js.

Pero todo el front-end se ha desarrollado hasta el día de hoy. El mecanismo del módulo definido por la especificación ECMA nativa es ESModule (ESM para abreviar). Debido a que está definido por la especificación, su implementación está en el nivel del motor y la capa correspondiente a Node.js está implementado por V8. Por lo tanto, el calor actual no puede actuar sobre el módulo ESM.

Sin embargo, en mi opinión, los puntos de acceso basados ​​en CommonJS no se recomiendan para su uso en producción porque se implementan en un nivel superior y ocultarán varios pits.Sin embargo, los puntos de acceso basados ​​en ESM pueden definir un mecanismo completo de carga y descarga de módulos si la especificación puede definir un mecanismo completo de carga y descarga de módulos es el futuro de las verdaderas soluciones de actualización en caliente.

Node.js también tiene características experimentales correspondientes que se pueden usar en esta área.Para obtener más información, consulte: ESM Hooks. (https://nodejs.org/dist/latest/docs/api/esm.html#esm_hooks) Sin embargo, actualmente solo se encuentra en el estado de Estabilidad: 1 y debe continuar esperando y observando.

Problema 4: Confusión de la versión del módulo

La actualización en caliente de Node.js en realidad no es el tipo de reemplazo de módulo antiguo global que muchos estudiantes imaginaron, porque el mecanismo de almacenamiento en caché puede causar que existan varias versiones diferentes de los módulos actualizados en caliente en la memoria al mismo tiempo, lo que resulta en algunos extraños. errores que son difíciles de localizar. .

Continuemos construyendo un pequeño ejemplo para ilustrar, primero escriba el módulo que se actualizará en caliente update_mod.js:

'use strict';

const version = 'v1';

module.exports = () => {
  return version;
};

Luego agregue uno utils.jspara usar este módulo normalmente:

'use strict';

const mod = require('./update_mod.js');

setInterval(() => console.log('utils', mod()), 1000);

Luego escriba la entrada de inicio index.jspara la operación de actualización en caliente:

'use strict';

const cleanCache = require('clear-module');

let mod = require('./update_mod.js');

require('./utils.js');

setInterval(() => {
  cleanCache('./update_mod.js');
  mod = require('./update_mod.js');
  console.log('index', mod())
}, 1000);

En este punto cuando ejecutamos y node index.jsno cambiamos podemos update_mod.jsver:

utils v1
index v1
utils v1
index v1

Tenga en cuenta que la memoria es update_mod.jstodas las v1versiones.

No es necesario reiniciar el servicio en este momento, estamos update_mod.jsmodificando version:

// update_mod.js
const version = 'v2';

Luego observe que la salida se convierte en:

index v1
utils v1
index v2
utils v1
index v2
utils v1

index.jsSe realizó una operación de actualización en caliente en , por lo que volvió a requirellegar update_mod.jsa la última v2versión y utils.jsno habrá cambios en .

Tal situación de múltiples versiones de un módulo no solo aumenta la dificultad de localizar fallas en línea, sino que también causa pérdidas de memoria hasta cierto punto.

Adecuado para escenarios de actualización en caliente

Hablar del problema aparte de la escena es un hooligan. Aunque hay tantos problemas en la actualización en caliente, de hecho hay algunos escenarios de uso para la actualización en caliente del módulo. Lo discutiremos desde dos dimensiones, en línea y fuera de línea.

Para escenarios fuera de línea, los problemas menores de pérdida de memoria y recursos pueden dar paso a la eficiencia del desarrollo, por lo que la actualización en caliente es muy adecuada para la carga y descarga de un solo módulo del marco en modo de desarrollo.

Para escenarios en línea, las actualizaciones en caliente no son inútiles. Por ejemplo, está claro que los padres y los niños dependen de módulos lógicos cohesivos uno a uno y no crean atributos de recursos. La conexión en caliente se puede realizar a través de la organización de código adecuada para lograr una perfecta publicación de actualizaciones Propósito.

Al final, en general, debido al riesgo de envenenar la aplicación y los beneficios de la actualización en caliente debido a la falta de familiaridad, personalmente me opongo al uso de la tecnología de actualización en caliente en el entorno de producción en línea; y si el módulo ESM se carga más tarde con un mecanismo de descarga que claramente puede adaptarse a la especificación e implementado por el motor, puede ser el momento adecuado para que las actualizaciones en caliente se usen realmente de manera amplia y segura.

algún resumen

En el proceso de mantenimiento de AliNode en los últimos años, he lidiado con muchas fugas de memoria causadas por actualizaciones calientes. Solo aproveché la oportunidad de escribir este artículo para revisar los casos anteriores.

En la actualidad, los módulos que logran una actualización en caliente se pueden atribuir a la categoría de "magia negra". En comparación con la "tecnología negra", la "magia negra" es una espada de doble filo. Antes de usarla, debe tener cuidado de no lastimarte a ti mismo

- FINAL -

Node 社群


我组建了一个氛围特别好的 Node.js 社群,里面有很多 Node.js小伙伴,如果你对Node.js学习感兴趣的话(后续有计划也可以),我们可以一起进行Node.js相关的交流、学习、共建。下方加 考拉 好友回复「Node」即可。

如果你觉得这篇内容对你有帮助,我想请你帮我2个小忙:

1. 点个「在看」,让更多人也能看到这篇文章2. 订阅官方博客 www.inode.club 让我们一起成长

点赞和在看就是最大的支持

Supongo que te gusta

Origin blog.csdn.net/xgangzai/article/details/123861140
Recomendado
Clasificación