Manejo eficiente de la memoria en Node.js

Los programas, en el curso del trabajo, utilizan la memoria de acceso aleatorio de las computadoras. En JavaScript, en el entorno de Node.js, puede escribir proyectos de servidor de varias escalas. La organización del trabajo con memoria es siempre una tarea difícil y responsable. Al mismo tiempo, si en lenguajes como C y C ++, los programadores están muy involucrados en la gestión de la memoria, JS tiene mecanismos automáticos que, al parecer, eliminan por completo la responsabilidad del programador para un trabajo eficiente con la memoria. Sin embargo, este no es el caso. El código mal escrito para Node.js puede interferir con el funcionamiento normal de todo el servidor en el que se ejecuta.



El material, cuya traducción publicamos hoy, se centrará en el trabajo efectivo con memoria en el entorno de Node.js. En particular, aquí se discutirán conceptos tales como flujos, memorias intermedias y el método de flujo pipe() . Node.js v8.12.0 se utilizará en los experimentos. Un repositorio con código de ejemplo se puede encontrar aquí .

Tarea: copiar un archivo enorme


Si se le pide a alguien que cree un programa para copiar archivos en Node.js, lo más probable es que escriba inmediatamente sobre lo que se muestra a continuación. basic_copy.js el archivo que contiene este código basic_copy.js .

 const fs = require('fs'); let fileName = process.argv[2]; let destPath = process.argv[3]; fs.readFile(fileName, (err, data) => {   if (err) throw err;   fs.writeFile(destPath || 'output', data, (err) => {       if (err) throw err;   });     console.log('New file has been created!'); }); 

Este programa crea controladores para leer y escribir un archivo con un nombre dado e intenta escribir datos de archivo después de leerlo. Para archivos pequeños, este enfoque está funcionando.

Supongamos que nuestra aplicación necesita copiar un archivo enorme (consideraremos archivos "enormes" de más de 4 GB) durante el proceso de copia de seguridad de datos. Por ejemplo, tengo un archivo de video de 7,4 GB de tamaño que, usando el programa descrito anteriormente, intentaré copiar desde mi directorio actual al directorio Documents . Aquí está el comando para comenzar a copiar:

 $ node basic_copy.js cartoonMovie.mkv ~/Documents/bigMovie.mkv 

En Ubuntu, después de ejecutar este comando, se mostró un mensaje de error relacionado con un desbordamiento del búfer:

 /home/shobarani/Workspace/basic_copy.js:7   if (err) throw err;            ^ RangeError: File size is greater than possible Buffer: 0x7fffffff bytes   at FSReqWrap.readFileAfterStat [as oncomplete] (fs.js:453:11) 

Como puede ver, la operación de lectura de archivos falló debido al hecho de que Node.js permite que solo 2 GB de datos se lean en el búfer. ¿Cómo superar esta limitación? Al realizar operaciones que utilizan intensivamente el subsistema de E / S (copiar archivos, procesarlos, comprimirlos), es necesario tener en cuenta las capacidades de los sistemas y las limitaciones asociadas con la memoria.

Streams y Buffers en Node.js


Para solucionar el problema descrito anteriormente, necesitamos un mecanismo con el que podamos dividir grandes cantidades de datos en pequeños fragmentos. También necesitaremos estructuras de datos para almacenar estos fragmentos y trabajar con ellos. Un búfer es una estructura de datos que le permite almacenar datos binarios. A continuación, debemos poder leer datos del disco y escribirlos en el disco. Esta oportunidad nos puede dar flujos. Hablemos de buffers e hilos.

▍Buffers


Se puede crear un búfer inicializando el objeto Buffer .

 let buffer = new Buffer(10); // 10 -    console.log(buffer); //  <Buffer 00 00 00 00 00 00 00 00 00 00> 

En las versiones de Node.js más nuevas que la 8, es mejor usar la siguiente construcción para crear buffers:

 let buffer = new Buffer.alloc(10); console.log(buffer); //  <Buffer 00 00 00 00 00 00 00 00 00 00> 

Si ya tenemos algunos datos, como una matriz o algo similar, se puede crear un búfer basado en estos datos.

 let name = 'Node JS DEV'; let buffer = Buffer.from(name); console.log(buffer) //  <Buffer 4e 6f 64 65 20 4a 53 20 44 45 5> 

Los búferes tienen métodos que le permiten "mirarlos" y descubrir qué datos hay allí; estos son los métodos toString() y toJSON() .

Nosotros, en el proceso de optimización del código, no crearemos buffers nosotros mismos. Node.js crea estas estructuras de datos automáticamente cuando trabaja con flujos o sockets de red.

▍ Streams


Las corrientes, si nos dirigimos al lenguaje de la ciencia ficción, se pueden comparar con los portales de otros mundos. Hay cuatro tipos de transmisiones:

  • Una secuencia para leer (los datos se pueden leer de ella).
  • Secuencia para grabación (se le pueden enviar datos).
  • Flujo dúplex (está abierto para leer datos de él y para enviarle datos).
  • Transformación de flujo (un flujo dúplex especial que le permite procesar datos, por ejemplo, comprimirlos o verificar su corrección).

Necesitamos flujos porque el objetivo vital de la API de flujo en Node.js, y en particular el método stream.pipe() , es limitar el almacenamiento en búfer de datos a niveles aceptables. Esto se hace para que trabajar con fuentes y receptores de datos que difieren en diferentes velocidades de procesamiento no desborde la memoria disponible.

En otras palabras, para resolver el problema de copiar un archivo grande, necesitamos algún tipo de mecanismo que nos permita no sobrecargar el sistema.


Secuencias y almacenamientos intermedios (según la documentación de Node.js)

El diagrama anterior muestra dos tipos de transmisiones: transmisiones legibles y transmisiones grabables. El método pipe() es un mecanismo muy simple que le permite adjuntar hilos para leer a hilos para escribir. Si el esquema anterior no es particularmente claro para usted, entonces está bien. Después de analizar los siguientes ejemplos, puede manejarlo fácilmente. En particular, ahora consideraremos ejemplos de procesamiento de datos utilizando el método pipe() .

Solución 1. Copiar archivos usando flujos


Considere la solución al problema de copiar un archivo enorme, del que hablamos anteriormente. Esta solución se puede basar en dos hilos y se verá así:

  • Esperamos que la siguiente pieza de datos aparezca en la secuencia para leer.
  • Escribimos los datos recibidos en la secuencia para su grabación.
  • Monitoreamos el progreso de la operación de copia.

Llamaremos al programa que implementa esta idea streams_copy_basic.js . Aquí está su código:

 /*         . : Naren Arya */ const stream = require('stream'); const fs = require('fs'); let fileName = process.argv[2]; let destPath = process.argv[3]; const readable = fs.createReadStream(fileName); const writeable = fs.createWriteStream(destPath || "output"); fs.stat(fileName, (err, stats) => {   this.fileSize = stats.size;   this.counter = 1;   this.fileArray = fileName.split('.');     try {       this.duplicate = destPath + "/" + this.fileArray[0] + '_Copy.' + this.fileArray[1];   } catch(e) {       console.exception('File name is invalid! please pass the proper one');   }     process.stdout.write(`File: ${this.duplicate} is being created:`);     readable.on('data', (chunk)=> {       let percentageCopied = ((chunk.length * this.counter) / this.fileSize) * 100;       process.stdout.clearLine();  //          process.stdout.cursorTo(0);       process.stdout.write(`${Math.round(percentageCopied)}%`);       writeable.write(chunk);       this.counter += 1;   });     readable.on('end', (e) => {       process.stdout.clearLine();  //          process.stdout.cursorTo(0);       process.stdout.write("Successfully finished the operation");       return;   });     readable.on('error', (e) => {       console.log("Some error occurred: ", e);   });     writeable.on('finish', () => {       console.log("Successfully created the file copy!");   });  }); 

Esperamos que el usuario ejecute este programa para proporcionarle dos nombres de archivo. El primero es el archivo fuente, el segundo es el nombre de su futura copia. Creamos dos secuencias: una secuencia para leer y una secuencia para escribir, transfiriendo datos del primero al segundo. También hay algunos mecanismos auxiliares. Se utilizan para supervisar el proceso de copia y para enviar la información correspondiente a la consola.

Usamos el mecanismo de eventos aquí, en particular, estamos hablando de suscribirnos a los siguientes eventos:

  • data : se llama cuando se lee un dato.
  • end : se llama cuando se leen datos de la secuencia de lectura.
  • error : se llama si se produce un error al leer los datos.

Con este programa, se copia un archivo de 7,4 GB sin mensajes de error.

 $ time node streams_copy_basic.js cartoonMovie.mkv ~/Documents/4kdemo.mkv 

Sin embargo, hay un problema. Se puede identificar observando datos sobre el uso de los recursos del sistema por diversos procesos.


Datos de uso de recursos del sistema

Tenga en cuenta que el proceso de node , después de copiar el 88% del archivo, ocupa 4,6 GB de memoria. Esto es mucho, tal manejo de la memoria puede interferir con el trabajo de otros programas.

▍ Razones para el consumo excesivo de memoria


Preste atención a la velocidad de lectura de datos del disco y escritura de datos en el disco de la ilustración anterior (columnas de lectura de disco y escritura de disco). A saber, aquí puede ver los siguientes indicadores:

 Disk Read: 53.4 MiB/s Disk Write: 14.8 MiB/s 

Tal diferencia en las velocidades de lectura del registro de datos significa que la fuente de datos los produce mucho más rápido de lo que el receptor puede recibirlos y procesarlos. La computadora tiene que almacenar en la memoria los fragmentos de datos leídos hasta que se escriben en el disco. Como resultado, vemos tales indicadores de uso de memoria.

En mi computadora, este programa se ejecutó durante 3 minutos y 16 segundos. Aquí hay información sobre el progreso de su implementación:

 17.16s user 25.06s system 21% cpu 3:16.61 total 

Solución 2. Copiar archivos usando flujos y con ajuste automático de la velocidad de lectura y escritura de datos


Para hacer frente al problema anterior, podemos modificar el programa para que durante la copia de archivos, las velocidades de lectura y escritura se configuren automáticamente. Este mecanismo se llama contrapresión. Para usarlo, no necesitamos hacer nada especial. Es suficiente, utilizando el método pipe() , conectar la secuencia de lectura a la secuencia de escritura, y Node.js ajustará automáticamente las velocidades de transferencia de datos.

Llame a este programa streams_copy_efficient.js . Aquí está su código:

 /*          pipe(). : Naren Arya */ const stream = require('stream'); const fs = require('fs'); let fileName = process.argv[2]; let destPath = process.argv[3]; const readable = fs.createReadStream(fileName); const writeable = fs.createWriteStream(destPath || "output"); fs.stat(fileName, (err, stats) => {   this.fileSize = stats.size;   this.counter = 1;   this.fileArray = fileName.split('.');     try {       this.duplicate = destPath + "/" + this.fileArray[0] + '_Copy.' + this.fileArray[1];   } catch(e) {       console.exception('File name is invalid! please pass the proper one');   }     process.stdout.write(`File: ${this.duplicate} is being created:`);     readable.on('data', (chunk) => {       let percentageCopied = ((chunk.length * this.counter) / this.fileSize) * 100;       process.stdout.clearLine();  //          process.stdout.cursorTo(0);       process.stdout.write(`${Math.round(percentageCopied)}%`);       this.counter += 1;   });   readable.on('error', (e) => {       console.log("Some error occurred: ", e);   });     writeable.on('finish', () => {       process.stdout.clearLine();  //          process.stdout.cursorTo(0);       process.stdout.write("Successfully created the file copy!");   });     readable.pipe(writeable); //  !  }); 

La principal diferencia entre este programa y el anterior es que el código para copiar fragmentos de datos se reemplaza con la siguiente línea:

 readable.pipe(writeable); //  ! 

En el corazón de todo lo que sucede aquí está el método pipe() . Controla las velocidades de lectura y escritura, lo que lleva al hecho de que la memoria ya no está sobrecargada.

Ejecute el programa

 $ time node streams_copy_efficient.js cartoonMovie.mkv ~/Documents/4kdemo.mkv 

Estamos copiando el mismo archivo enorme. Ahora veamos cómo funciona el trabajo con la memoria y con el disco.


Al usar pipe (), las velocidades de lectura y escritura se configuran automáticamente

Ahora vemos que el proceso de node consume solo 61.9 MB de memoria. Si observa los datos sobre el uso del disco, puede ver lo siguiente:

 Disk Read: 35.5 MiB/s Disk Write: 35.5 MiB/s 

Gracias al mecanismo de contrapresión, las velocidades de lectura y escritura ahora son siempre iguales entre sí. Además, el nuevo programa funciona 13 segundos más rápido que el anterior.

 12.13s user 28.50s system 22% cpu 3:03.35 total 

Al usar el método pipe() , pudimos reducir el tiempo de ejecución del programa y el consumo de memoria en un 98.68%.

En este caso, 61,9 MB es el tamaño del búfer creado por el flujo de lectura de datos. Podemos establecer este tamaño nosotros mismos, utilizando el método read() de la secuencia para leer datos:

 const readable = fs.createReadStream(fileName); readable.read(no_of_bytes_size); 

Aquí copiamos el archivo en el sistema de archivos local, sin embargo, se puede usar el mismo enfoque para optimizar muchas otras tareas de entrada y salida de datos. Por ejemplo, esto funciona con flujos de datos, cuya fuente es Kafka, y el receptor es la base de datos. De acuerdo con el mismo esquema, es posible organizar la lectura de datos de un disco, comprimiéndolos, como dicen, "sobre la marcha", y volviéndolos a escribir en el disco ya en forma comprimida. De hecho, hay muchos otros usos para la tecnología aquí descrita.

Resumen


Uno de los objetivos de este artículo era demostrar lo fácil que es escribir programas malos en Node.js, a pesar de que esta plataforma proporciona excelentes API para el desarrollador. Con un poco de atención a esta API, puede mejorar la calidad de los proyectos de software del lado del servidor.

Estimados lectores! ¿Cómo trabaja con buffers e hilos en Node.js?

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


All Articles