So schreiben Sie speichereffiziente Anwendungen mit Node.js

So schreiben Sie speichereffiziente Anwendungen mit Node.js

Vorwort

Softwareanwendungen werden im Hauptspeicher des Computers ausgeführt, der als Direktzugriffsspeicher (RAM) bezeichnet wird. JavaScript, insbesondere Nodejs (serverseitiges JS), ermöglicht es uns, kleine bis große Softwareprojekte für Endbenutzer zu schreiben. Der Umgang mit Programmspeicher ist immer eine heikle Angelegenheit, da eine fehlerhafte Implementierung alle anderen Anwendungen blockieren kann, die auf einem bestimmten Server oder System ausgeführt werden. C- und C++-Programmierer legen großen Wert auf die Speicherverwaltung, da sich in jeder Ecke des Codes schreckliche Speicherlecks verbergen. Aber haben Sie als JS-Entwickler sich wirklich um dieses Problem gekümmert?

Da JS-Entwickler die Webserverprogrammierung normalerweise auf dedizierten Servern mit hoher Kapazität durchführen, bemerken sie Verzögerungen beim Multitasking möglicherweise nicht. Wenn wir beispielsweise einen Webserver entwickeln, werden wir auch mehrere Anwendungen ausführen, wie beispielsweise einen Datenbankserver (MySQL), einen Cache-Server (Redis) und je nach Bedarf weitere Anwendungen. Wir müssen uns darüber im Klaren sein, dass sie auch den verfügbaren Hauptspeicher verbrauchen. Wenn wir unsere Anwendung nachlässig schreiben, verschlechtern wir wahrscheinlich die Leistung anderer Prozesse oder verweigern ihnen sogar die Speicherzuweisung vollständig. In diesem Artikel lösen wir ein Problem, um NodeJS-Konstrukte wie Streams, Puffer und Pipes zu verstehen und zu sehen, wie jedes von ihnen das Schreiben speichereffizienter Anwendungen unterstützt.

Problem: Kopieren großer Dateien

Wenn jemand aufgefordert wird, ein Programm zum Kopieren von Dateien mit NodeJS zu schreiben, wird er schnell den folgenden Code schreiben:

const fs = erfordern('fs');

let Dateiname = Prozess.argv[2];
let destPath = process.argv[3];

fs.readFile(Dateiname, (Fehler, Daten) => {
    wenn (err) throw err;

    fs.writeFile(Zielpfad || 'Ausgabe', Daten, (Fehler) => {
        wenn (err) throw err;
    });
    
    console.log('Neue Datei wurde erstellt!');
});

Dieser Code übernimmt einfach den Eingabedateinamen und -pfad und schreibt ihn nach dem Versuch, die Datei zu lesen, in den Zielpfad, was bei kleinen Dateien kein Problem darstellt.

Nehmen wir nun an, wir haben eine große Datei (größer als 4 GB), die wir mit diesem Programm sichern müssen. Nehmen wir als Beispiel meinen 7,4G Ultra-High-Definition-4K-Film. Ich verwende den obigen Programmcode, um ihn aus dem aktuellen Verzeichnis in ein anderes Verzeichnis zu kopieren.

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

Dann bekam ich diese Fehlermeldung unter Ubuntu (Linux):

/home/shobarani/Arbeitsbereich/basic_copy.js:7

wenn (err) throw err;

^

RangeError: Dateigröße ist größer als möglicher Puffer: 0x7fffffff Bytes

bei FSReqWrap.readFileAfterStat [als oncomplete] (fs.js:453:11)

Wie Sie sehen, tritt der Fehler beim Lesen der Datei auf, da NodeJS nur das Schreiben von maximal 2 GB Daten in seinen Puffer zulässt. Um dieses Problem zu lösen, sollten Sie bei E/A-intensiven Vorgängen (Kopieren, Verarbeiten, Komprimieren usw.) die Speichersituation berücksichtigen.

Streams und Puffer in NodeJS

Um das obige Problem zu lösen, benötigen wir eine Möglichkeit, große Dateien in viele Dateiblöcke aufzuteilen, und wir benötigen eine Datenstruktur zum Speichern dieser Dateiblöcke. Ein Puffer ist eine Struktur zum Speichern binärer Daten. Als Nächstes benötigen wir eine Möglichkeit zum Lesen und Schreiben von Dateiblöcken, und Streams bietet diese Möglichkeit.

Puffer

Mit dem Buffer-Objekt können wir ganz einfach einen Puffer erstellen.

let buffer = new Buffer(10); # 10 ist das Volumen des Puffers console.log(buffer); # druckt <Buffer 00 00 00 00 00 00 00 00 00 00>

In neueren Versionen von NodeJS (>8) können Sie auch so schreiben.

Lassen Sie den Puffer = neuer Puffer.alloc(10);
console.log(Puffer); # druckt <Puffer 00 00 00 00 00 00 00 00 00 00>

Wenn wir bereits über Daten wie ein Array oder einen anderen Datensatz verfügen, können wir einen Puffer dafür erstellen.

let name = "Node JS DEV";
Lassen Sie den Puffer = Puffer.from(Name);
console.log(Puffer) # druckt <Puffer 4e 6f 64 65 20 4a 53 20 44 45 5>

Puffer verfügen über einige wichtige Methoden wie buffer.toString() und buffer.toJSON(), mit denen Sie einen Drilldown in die von ihnen gespeicherten Daten durchführen können.

Um den Code zu optimieren, werden wir keine Rohpuffer direkt erstellen. NodeJS und die V8-Engine implementieren dies bereits, indem sie beim Verarbeiten von Streams und Netzwerk-Sockets interne Puffer (Warteschlangen) erstellen.

Streams

Einfach ausgedrückt sind Streams wie beliebige Türen auf NodeJS-Objekten. In der Computervernetzung ist Ingress eine Eingabeaktion und Egress eine Ausgabeaktion. Wir werden diese Begriffe im Folgenden weiterhin verwenden.

Es gibt vier Arten von Streams:

  • Lesbarer Stream (zum Lesen von Daten)
  • Schreibbarer Stream (zum Schreiben von Daten)
  • Duplex-Stream (kann sowohl zum Lesen als auch zum Schreiben verwendet werden)
  • Transformationsstream (ein benutzerdefinierter Duplexstream zur Datenverarbeitung, z. B. Komprimieren, Überprüfen von Daten usw.)

Der folgende Satz erklärt deutlich, warum wir Streams verwenden sollten.

Ein wichtiges Ziel der Stream-API (und insbesondere der Methode stream.pipe()) besteht darin, die Datenpufferung auf ein akzeptables Maß zu begrenzen, sodass Quellen und Ziele mit unterschiedlichen Geschwindigkeiten den verfügbaren Speicher nicht verstopfen.

Wir müssen die Aufgabe irgendwie erledigen, ohne das System zu überlasten. Dies haben wir am Anfang des Artikels erwähnt.

Im obigen Diagramm haben wir zwei Arten von Streams: lesbare Streams und beschreibbare Streams. Die Methode .pipe() ist eine sehr einfache Methode, um einen lesbaren Stream mit einem beschreibbaren Stream zu verbinden. Wenn Sie das obige Diagramm nicht verstehen, machen Sie sich keine Sorgen. Nachdem Sie sich unsere Beispiele angesehen haben, können Sie zum Diagramm zurückkehren und alles wird einen Sinn ergeben. Pfeifen sind ein faszinierender Mechanismus, und wir werden sie anhand von zwei Beispielen veranschaulichen.

Lösung 1 (Verwenden Sie einfach Streams zum Kopieren von Dateien)

Lassen Sie uns eine Lösung für das oben erwähnte Problem des Kopierens großer Dateien entwerfen. Zuerst erstellen wir zwei Flows und führen dann die nächsten Schritte aus.

1. Auf Datenblöcke aus einem lesbaren Stream warten

2. Schreiben Sie den Datenblock in den beschreibbaren Stream

3. Verfolgen Sie den Fortschritt des Dateikopierens

Wir haben diesen Code streams_copy_basic.js genannt

/*
    Eine Dateikopie mit Streams und Ereignissen - Autor: Naren Arya
*/

const stream = erfordern('stream');
const fs = erfordern('fs');

let Dateiname = Prozess.argv[2];
let destPath = process.argv[3];

const readabale = fs.createReadStream(Dateiname);
const beschreibbar = fs.createWriteStream(destPath || "Ausgabe");

fs.stat(Dateiname, (err, stats) => {
    diese.Dateigröße = stats.size;
    dieser.Zähler = 1;
    dies.fileArray = Dateiname.split('.');
    
    versuchen {
        this.duplicate = Zielpfad + "/" + this.fileArray[0] + '_Copy.' + this.fileArray[1];
    } Fang(e) {
        console.exception('Der Dateiname ist ungültig! Bitte geben Sie den richtigen ein.');
    }
    
    process.stdout.write(`Datei: ${this.duplicate} wird erstellt:`);
    
    readabale.on('Daten', (Block)=> {
        let Prozentsatz kopiert = ((Chunklänge * dieser Zähler) / diese Dateigröße) * 100;
        process.stdout.clearLine(); // aktuellen Text löschen
        verarbeiten.stdout.cursorTo(0);
        verarbeiten.stdout.write(`${Math.round(percentageCopied)}%`);
        beschreibbar.schreiben(Block);
        dieser.Zähler += 1;
    });
    
    readabale.on('Ende', (e) => {
        process.stdout.clearLine(); // aktuellen Text löschen
        verarbeiten.stdout.cursorTo(0);
        process.stdout.write("Vorgang erfolgreich abgeschlossen");
        zurückkehren;
    });
    
    readabale.on('Fehler', (e) => {
        console.log("Es ist ein Fehler aufgetreten: ", e);
    });
    
    beschreibbar.on('fertig', () => {
        console.log("Dateikopie erfolgreich erstellt!");
    });
    
});

In diesem Programm erhalten wir zwei vom Benutzer übergebene Dateipfade (Quelldatei und Zieldatei) und erstellen dann zwei Streams, um Datenblöcke vom lesbaren Stream in den beschreibbaren Stream zu übertragen. Anschließend definieren wir einige Variablen, um den Fortschritt des Dateikopiervorgangs zu verfolgen und ihn dann auf der Konsole (in diesem Fall „Konsole“) auszugeben. Gleichzeitig abonnieren wir auch einige Veranstaltungen:

Daten: Wird ausgelöst, wenn ein Datenblock gelesen wird

Ende: Wird ausgelöst, wenn ein Datenblock vom lesbaren Stream gelesen wird

Fehler: Wird ausgelöst, wenn beim Lesen eines Datenblocks ein Fehler auftritt

Durch Ausführen dieses Programms können wir die Aufgabe des Kopierens einer großen Datei (hier 7,4 GB) erfolgreich abschließen.

$ Zeitknoten streams_copy_basic.js cartoonMovie.mkv ~/Documents/4kdemo.mkv

Wenn wir jedoch den Speicherstatus des Programms während des Betriebs über den Taskmanager beobachten, besteht weiterhin ein Problem.

4,6 GB? Der Speicherverbrauch unseres Programms während der Ausführung ist hier unsinnig und kann mit hoher Wahrscheinlichkeit andere Anwendungen blockieren.

was ist passiert?

Wenn Sie sich die Lese- und Schreibraten in der obigen Abbildung genau ansehen, werden Sie einige Hinweise finden.

Festplattenlesezugriff: 53,4 MiB/s

Festplattenschreibzugriff: 14,8 MiB/s

Dies bedeutet, dass die Hersteller immer schneller produzieren und die Verbraucher nicht mithalten können. Um den gelesenen Datenblock zu speichern, speichert der Computer die überschüssigen Daten im Arbeitsspeicher des Geräts. Aus diesem Grund kommt es zu einem Anstieg des RAM.

Der obige Code läuft auf meinem Rechner in 3 Minuten und 16 Sekunden ...

17,16 s Benutzer 25,06 s System 21 % CPU 3:16,61 gesamt

Lösung 2 (Dateikopieren basierend auf Streams und automatischem Gegendruck)

Um die oben genannten Probleme zu beheben, können wir das Programm so ändern, dass die Lese- und Schreibgeschwindigkeit der Festplatte automatisch angepasst wird. Dieser Mechanismus ist Gegendruck. Wir müssen nicht viel tun, sondern nur den lesbaren Stream in den beschreibbaren Stream importieren und NodeJS kümmert sich um den Gegendruck.

Nennen wir dieses Programm streams_copy_efficient.js

/*
    Eine Dateikopie mit Streams und Piping - Autor: Naren Arya
*/

const stream = erfordern('stream');
const fs = erfordern('fs');

let Dateiname = Prozess.argv[2];
let destPath = process.argv[3];

const readabale = fs.createReadStream(Dateiname);
const beschreibbar = fs.createWriteStream(destPath || "Ausgabe");

fs.stat(Dateiname, (err, stats) => {
    diese.Dateigröße = stats.size;
    dieser.Zähler = 1;
    dies.fileArray = Dateiname.split('.');
    
    versuchen {
        this.duplicate = Zielpfad + "/" + this.fileArray[0] + '_Copy.' + this.fileArray[1];
    } Fang(e) {
        console.exception('Der Dateiname ist ungültig! Bitte geben Sie den richtigen ein.');
    }
    
    process.stdout.write(`Datei: ${this.duplicate} wird erstellt:`);
    
    readabale.on('Daten', (Block) => {
        let Prozentsatz kopiert = ((Chunklänge * dieser Zähler) / diese Dateigröße) * 100;
        process.stdout.clearLine(); // aktuellen Text löschen
        verarbeiten.stdout.cursorTo(0);
        verarbeiten.stdout.write(`${Math.round(percentageCopied)}%`);
        dieser.Zähler += 1;
    });
    
    readabale.pipe(writeable); // Autopilot EIN!
    
    // Falls es beim Kopieren zu einer Unterbrechung kommt
    beschreibbar.auf('unpipe', (e) => {
        process.stdout.write("Das Kopieren ist fehlgeschlagen!");
    });
    
});

In diesem Beispiel haben wir den vorherigen Datenblock-Schreibvorgang durch eine Codezeile ersetzt.

readabale.pipe(writeable); // Autopilot EIN!

Die ganze Magie geschieht in diesem Rohr. Es steuert die Geschwindigkeit der Lese- und Schreibvorgänge auf der Festplatte, um den Hauptspeicher (RAM) nicht zu verstopfen.

Führen Sie es aus.

$ Zeitknoten streams_copy_efficient.js cartoonMovie.mkv ~/Documents/4kdemo.mkv

Wir haben dieselbe große Datei (7,4 GB) kopiert und schauen uns die Speicherauslastung an.

Schock! Jetzt belegt das Node-Programm nur noch 61,9 MiB Speicher. Wenn Sie die Lese- und Schreibraten beobachten:

Festplattenlesezugriff: 35,5 MiB/s

Festplattenschreibzugriff: 35,5 MiB/s

Aufgrund des Gegendrucks bleiben die Lese- und Schreibraten jederzeit konstant. Noch überraschender ist, dass dieser optimierte Programmcode 13 Sekunden schneller ist als der vorherige.

12,13 s Benutzer 28,50 s System 22 % CPU 3:03,35 gesamt

Dank NodeJS-Streams und -Pipes wurde die Speicherlast um 98,68 % reduziert und auch die Ausführungszeit verkürzt. Aus diesem Grund ist die Pipeline eine starke Präsenz.

61,9 MiB ist die Größe des Puffers, der durch den lesbaren Stream erstellt wird. Wir können dem Pufferblock auch eine benutzerdefinierte Größe zuweisen, indem wir die Lesemethode für den lesbaren Stream verwenden.

const readabale = fs.createReadStream(Dateiname);
lesbar.lesen(Anzahl_Bytes_Größe);

Zusätzlich zum lokalen Kopieren von Dateien kann diese Technik auch verwendet werden, um viele E/A-Vorgangsprobleme zu optimieren:

  • Verarbeiten des Datenflusses von Kafka zur Datenbank
  • Verarbeitet Datenströme aus dem Dateisystem, komprimiert sie im laufenden Betrieb und schreibt sie auf die Festplatte
  • Mehr……

abschließend

Die Motivation für das Schreiben dieses Artikels besteht hauptsächlich darin, zu veranschaulichen, dass wir versehentlich Code mit schlechter Leistung schreiben können, selbst wenn NodeJS eine gute API bereitstellt. Wenn wir den integrierten Tools mehr Aufmerksamkeit schenken würden, könnten wir den Programmablauf besser optimieren.

Oben finden Sie detaillierte Informationen zur Verwendung von Node.js zum Schreiben speichereffizienter Anwendungen. Weitere Informationen zu Node.js finden Sie in den anderen verwandten Artikeln auf 123WORDPRESS.COM!

Das könnte Sie auch interessieren:
  • Detaillierte Erklärung des Speicherplatzes, der Zuweisung und der tiefen und flachen Kopien von JavaScript
  • Ein Artikel zum Verständnis von Javascript-Speicherlecks
  • Fehlerbehebung bei hohem Speicherverbrauch von NodeJs, tatsächlicher Kampfrekord
  • JavaScript-Garbage-Collection-Mechanismus und Speicherverwaltung
  • Analyse häufiger JS-Speicherlecks und Lösungen
  • Detaillierte Erläuterung des Beispiels eines Javascript-Speichermodells
  • Analyse mehrerer Beispiele für durch JS verursachte Speicherlecks
  • Detaillierte Erläuterung des JavaScript-Stapelspeichers und des Heapspeichers
  • So gehen Sie mit JavaScript-Speicherlecks um
  • Detaillierte Erklärung des JS-Speicherplatzes

<<:  Ein genauerer Blick auf SQL-Injection

>>:  So konfigurieren Sie ein SSL-Zertifikat in Nginx, um den HTTPS-Dienst zu implementieren

Artikel empfehlen

So generieren Sie eine Vue-Benutzeroberfläche per Drag & Drop

Inhaltsverzeichnis Vorwort 1. Technisches Prinzip...

Detaillierte Erklärung der Beziehung zwischen Vue und VueComponent

Der folgende Fall überprüft die Wissenspunkte der...

Die Rolle von nextTick in Vue und mehrere einfache Anwendungsszenarien

Zweck Verstehen Sie die Rolle von nextTick und me...

Implementierung eines Web-Rechners auf Basis von JavaScript

In diesem Artikel wird der spezifische JavaScript...

Detaillierte Erklärung der Verwendung von DECIMAL im MySQL-Datentyp

Detaillierte Erklärung der Verwendung von DECIMAL...

So deinstallieren Sie MySQL sauber (getestet und effektiv)

Wie deinstalliere ich Mysql vollständig? Befolgen...

Grafisches Tutorial zur Installation und Konfiguration von MySQL 5.7

In diesem Tutorial erfahren Sie alles über die In...

Gründe und Lösungen für die Auswahl des falschen Index durch MySQL

In MySQL können Sie mehrere Indizes für eine Tabe...

Code zur Änderung des CSS-Bildlaufleistenstils

Code zur Änderung des CSS-Bildlaufleistenstils .s...

Vue implementiert eine kleine Countdown-Funktion

In vielen Projekten muss eine Countdown-Funktion ...

MySQL query_cache_type-Parameter und Verwendungsdetails

Der Zweck der Einrichtung eines MySQL-Abfragecach...

Vue-cli erstellt ein Projekt und analysiert die Projektstruktur

Inhaltsverzeichnis 1. Geben Sie ein Verzeichnis e...

Vue implementiert Beispielcode zur Formulardatenvalidierung

Fügen Sie dem el-form-Formular Regeln hinzu: Defi...