Programme verwenden im Laufe der Arbeit den Direktzugriffsspeicher von Computern. In JavaScript können Sie in der Umgebung von Node.js Serverprojekte in verschiedenen Maßstäben schreiben. Die Organisation der Arbeit mit dem Gedächtnis ist immer eine schwierige und verantwortungsvolle Aufgabe. Wenn Programmierer in Sprachen wie C und C ++ ziemlich eng in die Speicherverwaltung involviert sind, verfügt JS über automatische Mechanismen, die anscheinend die Verantwortung des Programmierers für eine effiziente Arbeit mit dem Speicher vollständig aufheben. Dies ist jedoch tatsächlich nicht der Fall. Schlecht geschriebener Code für Node.js kann den normalen Betrieb des gesamten Servers beeinträchtigen, auf dem er ausgeführt wird.

Das Material, dessen Übersetzung wir heute veröffentlichen, konzentriert sich auf die effektive Arbeit mit dem Gedächtnis in der Umgebung von Node.js. Insbesondere werden hier Konzepte wie Streams, Puffer und die
pipe()
Stream-Methode diskutiert. In den Experimenten wird Node.js v8.12.0 verwendet. Ein Repository mit Beispielcode finden Sie
hier .
Aufgabe: Kopieren einer riesigen Datei
Wenn jemand gebeten wird, ein Programm zum Kopieren von Dateien in Node.js zu erstellen, wird er höchstwahrscheinlich sofort über das schreiben, was unten gezeigt wird. Wir benennen die Datei, die diesen Code enthält
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!'); });
Dieses Programm erstellt Handler zum Lesen und Schreiben einer Datei mit einem bestimmten Namen und versucht, nach dem Lesen Dateidaten zu schreiben. Bei kleinen Dateien funktioniert dieser Ansatz.
Angenommen, unsere Anwendung muss während des Datensicherungsprozesses eine große Datei kopieren (wir betrachten "große" Dateien mit mehr als 4 GB). Ich habe beispielsweise eine Videodatei mit einer Größe von 7,4 GB, die ich mit dem oben beschriebenen Programm versuchen werde, aus meinem aktuellen Verzeichnis in das Verzeichnis "
Documents
zu kopieren. Hier ist der Befehl zum Starten des Kopierens:
$ node basic_copy.js cartoonMovie.mkv ~/Documents/bigMovie.mkv
In Ubuntu wurde nach Ausführung dieses Befehls eine Fehlermeldung im Zusammenhang mit einem Pufferüberlauf angezeigt:
/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)
Wie Sie sehen können, ist der Dateilesevorgang fehlgeschlagen, da mit Node.js nur 2 GB Daten in den Puffer eingelesen werden können. Wie kann diese Einschränkung überwunden werden? Bei Operationen, bei denen das E / A-Subsystem intensiv genutzt wird (Kopieren, Verarbeiten, Komprimieren von Dateien), müssen die Funktionen der Systeme und die mit dem Speicher verbundenen Einschränkungen berücksichtigt werden.
Streams und Puffer in Node.js
Um das oben beschriebene Problem zu umgehen, benötigen wir einen Mechanismus, mit dem wir große Datenmengen in kleine Fragmente aufteilen können. Wir werden auch Datenstrukturen benötigen, um diese Fragmente zu speichern und mit ihnen zu arbeiten. Ein Puffer ist eine Datenstruktur, mit der Sie Binärdaten speichern können. Als nächstes müssen wir in der Lage sein, Daten von der Festplatte zu lesen und auf die Festplatte zu schreiben. Diese Gelegenheit kann uns Flüsse geben. Lassen Sie uns über Puffer und Threads sprechen.
▍Puffer
Ein Puffer kann durch Initialisieren des
Buffer
werden.
let buffer = new Buffer(10);
In Versionen von Node.js, die neuer als die 8. sind, ist es am besten, die folgende Konstruktion zu verwenden, um Puffer zu erstellen:
let buffer = new Buffer.alloc(10); console.log(buffer);
Wenn wir bereits einige Daten haben, wie z. B. ein Array oder ähnliches, kann basierend auf diesen Daten ein Puffer erstellt werden.
let name = 'Node JS DEV'; let buffer = Buffer.from(name); console.log(buffer)
Puffer verfügen über Methoden, mit denen Sie sie „untersuchen“ und herausfinden können, welche Daten vorhanden sind. Dies sind die Methoden
toString()
und
toJSON()
.
Bei der Optimierung des Codes werden wir selbst keine Puffer erstellen. Node.js erstellt diese Datenstrukturen automatisch, wenn Sie mit Streams oder Netzwerk-Sockets arbeiten.
▍ Streams
Streams können, wenn wir uns der Sprache der Science-Fiction zuwenden, mit Portalen zu anderen Welten verglichen werden. Es gibt vier Arten von Streams:
- Ein Stream zum Lesen (Daten können daraus gelesen werden).
- Stream zur Aufzeichnung (Daten können an ihn gesendet werden).
- Duplex-Stream (er ist offen zum Lesen von Daten und zum Senden von Daten an ihn).
- Transformierender Stream (ein spezieller Duplex-Stream, mit dem Sie Daten verarbeiten, z. B. komprimieren oder auf ihre Richtigkeit überprüfen können).
Wir benötigen Streams, da das Hauptziel der Stream-API in Node.js und insbesondere der
stream.pipe()
-Methode darin besteht, die
stream.pipe()
auf akzeptable Werte zu beschränken. Dies geschieht, damit die Arbeit mit Quellen und Empfängern von Daten, die sich in unterschiedlichen Verarbeitungsgeschwindigkeiten unterscheiden, den verfügbaren Speicher nicht überläuft.
Mit anderen Worten, um das Problem des Kopierens einer großen Datei zu lösen, benötigen wir einen Mechanismus, der es uns ermöglicht, das System nicht zu überlasten.
Streams und Puffer (basierend auf der Node.js-Dokumentation)Das vorherige Diagramm zeigt zwei Arten von Streams - lesbare Streams und beschreibbare Streams. Die
pipe()
-Methode ist ein sehr einfacher Mechanismus, mit dem Sie Threads zum Lesen an Threads zum Schreiben anhängen können. Wenn Ihnen das obige Schema nicht besonders klar ist, ist das in Ordnung. Nachdem Sie die folgenden Beispiele analysiert haben, können Sie problemlos damit umgehen. Insbesondere werden wir nun Beispiele für die Datenverarbeitung mit der
pipe()
-Methode betrachten.
Lösung 1. Kopieren von Dateien mithilfe von Streams
Betrachten Sie die Lösung für das Problem des Kopierens einer großen Datei, über das wir oben gesprochen haben. Diese Lösung kann auf zwei Threads basieren und sieht folgendermaßen aus:
- Wir erwarten, dass die nächsten Daten zum Lesen im Stream erscheinen.
- Wir schreiben die empfangenen Daten zur Aufzeichnung in den Stream.
- Wir überwachen den Fortschritt des Kopiervorgangs.
Wir werden das Programm aufrufen, das diese Idee
streams_copy_basic.js
.
streams_copy_basic.js
. Hier ist ihr Code:
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();
Wir erwarten, dass der Benutzer dieses Programm ausführt, um ihm zwei Dateinamen zu geben. Die erste ist die Quelldatei, die zweite ist der Name der zukünftigen Kopie. Wir erstellen zwei Streams - einen Stream zum Lesen und einen Stream zum Schreiben, um Daten vom ersten zum zweiten zu übertragen. Es gibt auch einige Hilfsmechanismen. Sie dienen zur Überwachung des Kopiervorgangs und zur Ausgabe der entsprechenden Informationen an die Konsole.
Wir verwenden hier den Ereignismechanismus, insbesondere sprechen wir über das Abonnieren der folgenden Ereignisse:
data
- Wird beim Lesen eines data
aufgerufen.end
- wird aufgerufen, wenn Daten aus dem Lesestream gelesen werden.error
- wird aufgerufen, wenn beim Lesen von Daten ein Fehler auftritt.
Mit diesem Programm wird eine 7,4-GB-Datei ohne Fehlermeldungen kopiert.
$ time node streams_copy_basic.js cartoonMovie.mkv ~/Documents/4kdemo.mkv
Es gibt jedoch ein Problem. Sie kann anhand von Daten zur Verwendung von Systemressourcen durch verschiedene Prozesse identifiziert werden.
Daten zur SystemressourcennutzungBeachten Sie, dass der
node
nach dem Kopieren von 88% der Datei 4,6 GB Speicher belegt. Dies ist eine Menge, eine solche Handhabung des Speichers kann die Arbeit anderer Programme beeinträchtigen.
▍ Gründe für übermäßigen Speicherverbrauch
Beachten Sie die Geschwindigkeit beim Lesen von Daten von der Festplatte und beim Schreiben von Daten auf die Festplatte aus der vorherigen Abbildung (Spalten zum
Disk Read
und
Disk Write
). Hier sehen Sie nämlich die folgenden Indikatoren:
Disk Read: 53.4 MiB/s Disk Write: 14.8 MiB/s
Ein solcher Unterschied in den Lesegeschwindigkeiten aus dem Datensatz bedeutet, dass die Datenquelle sie viel schneller erzeugt, als der Empfänger sie empfangen und verarbeiten kann. Der Computer muss die gelesenen Datenfragmente im Speicher speichern, bis sie auf die Festplatte geschrieben werden. Infolgedessen sehen wir solche Indikatoren für die Speichernutzung.
Auf meinem Computer lief dieses Programm 3 Minuten 16 Sekunden lang. Hier finden Sie Informationen zum Fortschritt der Implementierung:
17.16s user 25.06s system 21% cpu 3:16.61 total
Lösung 2. Kopieren von Dateien mithilfe von Streams und automatische Optimierung der Lese- und Schreibgeschwindigkeit von Daten
Um das oben genannte Problem zu lösen, können wir das Programm so ändern, dass beim Kopieren von Dateien die Lese- und Schreibgeschwindigkeiten automatisch konfiguriert werden. Dieser Mechanismus wird als Gegendruck bezeichnet. Um es zu benutzen, müssen wir nichts Besonderes tun. Mit der Methode
pipe()
reicht es aus, den
pipe()
mit dem Schreibstrom zu verbinden, und Node.js passt die Datenübertragungsgeschwindigkeiten automatisch an.
Rufen Sie dieses Programm
streams_copy_efficient.js
. Hier ist ihr Code:
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();
Der Hauptunterschied zwischen diesem und dem vorherigen Programm besteht darin, dass der Code zum Kopieren von Datenfragmenten durch die folgende Zeile ersetzt wird:
readable.pipe(writeable);
Das Herzstück von allem, was hier passiert, ist die
pipe()
-Methode. Es steuert die Lese- und Schreibgeschwindigkeit, was dazu führt, dass der Speicher nicht mehr überlastet ist.
Führen Sie das Programm aus.
$ time node streams_copy_efficient.js cartoonMovie.mkv ~/Documents/4kdemo.mkv
Wir kopieren dieselbe riesige Datei. Schauen wir uns nun an, wie die Arbeit mit dem Speicher und mit der Festplatte aussieht.
Mit Pipe () werden Lese- und Schreibgeschwindigkeiten automatisch konfiguriertJetzt sehen wir, dass der Knotenprozess nur 61,9 MB Speicher belegt. Wenn Sie sich die Daten zur Festplattennutzung ansehen, sehen Sie Folgendes:
Disk Read: 35.5 MiB/s Disk Write: 35.5 MiB/s
Dank des Gegendruckmechanismus sind Lese- und Schreibgeschwindigkeit jetzt immer gleich. Außerdem läuft das neue Programm 13 Sekunden schneller als das alte.
12.13s user 28.50s system 22% cpu 3:03.35 total
Mithilfe der
pipe()
-Methode konnten wir die Programmausführungszeit und den Speicherverbrauch um 98,68% reduzieren.
In diesem Fall entspricht 61,9 MB der Puffer, der vom Datenlesestream erstellt wird. Wir können diese Größe gut selbst festlegen, indem wir die
read()
-Methode des Streams verwenden, um Daten zu lesen:
const readable = fs.createReadStream(fileName); readable.read(no_of_bytes_size);
Hier haben wir die Datei in das lokale Dateisystem kopiert. Mit demselben Ansatz können jedoch viele andere Dateneingabe- / Ausgabeaufgaben optimiert werden. Dies funktioniert beispielsweise mit Datenströmen, deren Quelle Kafka ist, und deren Empfänger die Datenbank ist. Nach dem gleichen Schema ist es möglich, das Lesen von Daten von einer Festplatte zu organisieren, sie zu komprimieren, wie sie sagen, "on the fly", und sie bereits in komprimierter Form auf die Festplatte zurückzuschreiben. Tatsächlich gibt es viele andere Anwendungen für die hier beschriebene Technologie.
Zusammenfassung
Eines der Ziele dieses Artikels war es zu demonstrieren, wie einfach es ist, schlechte Programme auf Node.js zu schreiben, obwohl diese Plattform großartige APIs für den Entwickler bietet. Mit etwas Aufmerksamkeit für diese API können Sie die Qualität von serverseitigen Softwareprojekten verbessern.
Liebe Leser! Wie arbeiten Sie mit Puffern und Threads in Node.js?