Node.js でメモリ効率の高いアプリケーションを作成する方法

Node.js でメモリ効率の高いアプリケーションを作成する方法

序文

ソフトウェア アプリケーションは、ランダム アクセス メモリ (RAM) と呼ばれるコンピューターのメイン メモリ内で実行されます。 JavaScript、特に Nodejs (サーバーサイド js) を使用すると、エンド ユーザー向けの小規模から大規模のソフトウェア プロジェクトを作成できます。プログラム メモリの処理は常に難しい問題です。実装が適切でないと、特定のサーバーまたはシステムで実行されている他のすべてのアプリケーションがブロックされる可能性があるためです。 C および C++ プログラマーはメモリ管理を気にします。コードのあらゆる場所にひどいメモリ リークが潜んでいるからです。しかし、JS 開発者にとって、この問題は本当に気にかけているのでしょうか?

JS 開発者は通常、専用の高容量サーバー上で Web サーバー プログラミングを行うため、マルチタスクの遅延に気付かない場合があります。例えば、Webサーバーを開発する場合、データベースサーバー(MySQL)、キャッシュサーバー(Redis)などの複数のアプリケーションも必要に応じて実行します。これらも利用可能なメインメモリを消費することに注意する必要があります。アプリケーションを不注意に作成すると、他のプロセスのパフォーマンスが低下したり、メモリ割り当てがまったく拒否されたりする可能性があります。この記事では、ストリーム、バッファ、パイプなどの NodeJS 構造を理解するための問題を解決し、それぞれがメモリ効率の高いアプリケーションの作成をどのようにサポートするかを確認します。

問題: 大きなファイルのコピー

NodeJS を使用してファイルコピー プログラムを作成するように依頼された場合、すぐに次のコードが作成されます。

定数 fs = require('fs');

ファイル名をprocess.argv[2]とします。
destPathをprocess.argv[3]とします。

fs.readFile(ファイル名, (err, データ) => {
    (err) の場合、err をスローします。

    fs.writeFile(destPath || '出力', データ, (err) => {
        (err) の場合、err をスローします。
    });
    
    console.log('新しいファイルが作成されました!');
});

このコードは、入力ファイル名とパスを取得し、ファイルの読み取りを試みた後にそれを宛先パスに書き込むだけなので、小さなファイルの場合は問題になりません。

ここで、このプログラムを使用してバックアップする必要がある大きなファイル (4 GB 以上) があるとします。 7.4G 超高精細 4K ムービーを例に挙げます。上記のプログラム コードを使用して、現在のディレクトリから別のディレクトリにコピーします。

$ ノード basic_copy.js cartoonMovie.mkv ~/Documents/bigMovie.mkv

その後、Ubuntu (Linux) で次のエラー メッセージが表示されました。

/home/shobarani/ワークスペース/basic_copy.js:7

(err) の場合、err をスローします。

^

RangeError: ファイル サイズが可能なバッファ サイズを超えています: 0x7fffffff バイト

FSReqWrap.readFileAfterStat [oncomplete として] (fs.js:453:11)

ご覧のとおり、NodeJS ではバッファに書き込めるデータの量は最大 2GB までなので、ファイルの読み取り中にエラーが発生します。この問題を解決するには、I/O を集中的に使用する操作 (コピー、処理、圧縮など) を実行するときに、メモリの状況を考慮するのが最適です。

NodeJS のストリームとバッファ

上記の問題を解決するには、大きなファイルを多数のファイル ブロックに分割する方法と、これらのファイル ブロックを格納するデータ構造が必要です。バッファはバイナリデータを格納するために使用される構造です。次に、ファイル ブロックを読み書きする方法が必要ですが、Streams はこの機能を提供します。

バッファ

Buffer オブジェクトを使用して簡単にバッファを作成できます。

let buffer = new Buffer(10); # 10 はバッファの容量です console.log(buffer); # <Buffer 00 00 00 00 00 00 00 00 00> と出力されます

NodeJS の新しいバージョン (>8) では、次のように記述することもできます。

バッファを新しいバッファ.alloc(10)とします。
console.log(buffer); # <Buffer 00 00 00 00 00 00 00 00 00 00> と出力されます

配列やその他のデータ セットなどのデータがすでにある場合は、そのためのバッファーを作成できます。

名前を 'Node JS DEV' にします。
buffer = Buffer.from(name); とします。
console.log(buffer) # 出力 <Buffer 4e 6f 64 65 20 4a 53 20 44 45 5>

バッファには、buffer.toString() や buffer.toJSON() などの重要なメソッドがあり、これらを使用してバッファに格納されているデータをドリルダウンすることができます。

コードを最適化するために、生のバッファを直接作成することはありません。 NodeJS と V8 エンジンは、ストリームとネットワーク ソケットを処理するときに内部バッファー (キュー) を作成することで、これをすでに実装しています。

ストリーム

簡単に言えば、ストリームは NodeJS オブジェクト上の任意のドアのようなものです。コンピュータ ネットワークでは、イングレスは入力アクションであり、エグレスとは出力アクションです。以下では引き続きこれらの用語を使用します。

ストリームには 4 つの種類があります。

  • 読み取り可能なストリーム(データの読み取り用)
  • 書き込み可能なストリーム(データの書き込み用)
  • デュプレックス ストリーム (読み取りと書き込みの両方に使用可能)
  • 変換ストリーム (データの圧縮、検査など、データを処理するために使用されるカスタム デュプレックス ストリーム)

次の文は、ストリームを使用する必要がある理由を明確に説明しています。

Stream API (特に stream.pipe() メソッド) の重要な目標は、データのバッファリングを許容レベルに制限し、異なる速度のソースと宛先によって使用可能なメモリが詰まらないようにすることです。

システムに負担をかけずに仕事を完了する方法が必要です。これは記事の冒頭で述べたことです。

上の図には、読み取り可能なストリームと書き込み可能なストリームの 2 種類のストリームがあります。 .pipe() メソッドは、読み取り可能なストリームを書き込み可能なストリームに接続するために使用される非常に基本的なメソッドです。上の図が理解できなくても心配しないでください。例を見た後、図に戻ってみるとすべてが理解できるようになります。パイプは魅力的なメカニズムです。ここでは 2 つの例を使ってその仕組みを説明します。

解決策 1 (ストリームを使用してファイルをコピーするだけ)

上記の大きなファイルのコピー問題に対する解決策を設計しましょう。まず 2 つのフローを作成し、次のいくつかの手順に従います。

1. 読み取り可能なストリームからデータチャンクをリッスンする

2. データブロックを書き込み可能なストリームに書き込む

3. ファイルコピーの進行状況を追跡する

このコードの名前はstreams_copy_basic.jsです

/*
    ストリームとイベントを含むファイルコピー - 著者: Naren Arya
*/

定数ストリーム = require('stream');
定数 fs = require('fs');

ファイル名をprocess.argv[2]とします。
destPathをprocess.argv[3]とします。

const readaball = fs.createReadStream(fileName);
const writeable = fs.createWriteStream(destPath || "出力");

fs.stat(ファイル名, (err, 統計) => {
    this.fileSize = stats.size;
    this.counter = 1;
    this.fileArray = ファイル名.split('.');
    
    試す {
        this.duplicate = destPath + "/" + this.fileArray[0] + '_Copy.' + this.fileArray[1];
    } キャッチ(e) {
        console.exception('ファイル名が無効です! 正しいファイル名を渡してください');
    }
    
    process.stdout.write(`ファイル: ${this.duplicate} を作成しています:`);
    
    readabale.on('data', (チャンク)=> {
        コピーされたパーセンテージを ((chunk.length * this.counter) / this.fileSize) * 100 とします。
        process.stdout.clearLine(); // 現在のテキストをクリアする
        プロセス.stdout.cursorTo(0);
        process.stdout.write(`${Math.round(percentageCopied)}%`);
        書き込み可能。書き込み(チャンク);
        this.counter += 1;
    });
    
    readabale.on('end', (e) => {
        process.stdout.clearLine(); // 現在のテキストをクリアする
        プロセス.stdout.cursorTo(0);
        process.stdout.write("操作は正常に終了しました");
        戻る;
    });
    
    readabale.on('エラー', (e) => {
        console.log("エラーが発生しました: ", e);
    });
    
    書き込み可能.on('終了', () => {
        console.log("ファイルのコピーが正常に作成されました!");
    });
    
});

このプログラムでは、ユーザーから渡された 2 つのファイル パス (ソース ファイルとターゲット ファイル) を受け取り、読み取り可能なストリームから書き込み可能なストリームにデータ ブロックを転送するための 2 つのストリームを作成します。次に、ファイルのコピーの進行状況を追跡するための変数をいくつか定義し、それをコンソール (この場合はコンソール) に出力します。同時に、いくつかのイベントにも参加します:

データ: データブロックが読み取られたときにトリガーされます

end: 読み取り可能なストリームによってデータブロックが読み取られたときにトリガーされます

エラー: データブロックの読み取り中にエラーが発生したときにトリガーされます

このプログラムを実行すると、大きなファイル (ここでは 7.4 G) をコピーするタスクを正常に完了できます。

$ 時間ノード streams_copy_basic.js cartoonMovie.mkv ~/Documents/4kdemo.mkv

しかし、タスク マネージャーを通じてプログラムの動作中のメモリ状態を観察すると、まだ問題があります。

4.6GB?プログラムの実行中に消費されるメモリはここでは意味がなく、他のアプリケーションをブロックする可能性が非常に高くなります。

どうしたの?

上の図の読み取り速度と書き込み速度をよく見ると、いくつかの手がかりが見つかります。

ディスク読み取り: 53.4 MiB/秒

ディスク書き込み: 14.8 MiB/秒

これは、生産者がより速いペースで生産しており、消費者がそれに追いつけないことを意味します。読み取られたデータ ブロックを節約するために、コンピューターは余分なデータをマシンの RAM に保存します。 RAM が急増するのはそのためです。

上記のコードは私のマシンでは 3 分 16 秒で実行されます...

17.16秒 ユーザー 25.06秒 システム 21% CPU 3:16.61 合計

ソリューション 2 (ストリームと自動バック プレッシャーに基づくファイルのコピー)

上記の問題を克服するには、プログラムを変更して、ディスクの読み取り速度と書き込み速度を自動的に調整することができます。このメカニズムがバックプレッシャーです。あまり多くの作業は必要ありません。読み取り可能なストリームを書き込み可能なストリームにインポートするだけで、NodeJS がバックプレッシャーを処理します。

このプログラムをstreams_copy_efficient.jsと名付けましょう。

/*
    ストリームとパイプを使用したファイルコピー - 著者: Naren Arya
*/

定数ストリーム = require('stream');
定数 fs = require('fs');

ファイル名をprocess.argv[2]とします。
destPathをprocess.argv[3]とします。

const readaball = fs.createReadStream(fileName);
const writeable = fs.createWriteStream(destPath || "出力");

fs.stat(ファイル名, (err, 統計) => {
    this.fileSize = stats.size;
    this.counter = 1;
    this.fileArray = ファイル名.split('.');
    
    試す {
        this.duplicate = destPath + "/" + this.fileArray[0] + '_Copy.' + this.fileArray[1];
    } キャッチ(e) {
        console.exception('ファイル名が無効です! 正しいファイル名を渡してください');
    }
    
    process.stdout.write(`ファイル: ${this.duplicate} を作成しています:`);
    
    readabale.on('data', (チャンク) => {
        コピーされたパーセンテージを ((chunk.length * this.counter) / this.fileSize) * 100 とします。
        process.stdout.clearLine(); // 現在のテキストをクリアする
        プロセス.stdout.cursorTo(0);
        process.stdout.write(`${Math.round(percentageCopied)}%`);
        this.counter += 1;
    });
    
    readabale.pipe(writeable); // オートパイロット ON!
    
    // コピー中に中断があった場合
    writeable.on('unpipe', (e) => {
        process.stdout.write("コピーに失敗しました!");
    });
    
});

この例では、以前のデータ ブロック書き込み操作を 1 行のコードに置き換えました。

readabale.pipe(writeable); // オートパイロット ON!

ここのパイプですべての魔法が起きます。メインメモリ (RAM) が詰まらないように、ディスクの読み取りと書き込みの速度を制御します。

実行してください。

$ 時間ノード streams_copy_efficient.js cartoonMovie.mkv ~/Documents/4kdemo.mkv

同じ大きなファイル (7.4 GB) をコピーして、メモリの使用率を見てみましょう。

ショック!現在、Node プログラムは 61.9 MiB のメモリのみを占有します。読み取り速度と書き込み速度を観察すると、次のようになります。

ディスク読み取り: 35.5 MiB/秒

ディスク書き込み: 35.5 MiB/秒

バックプレッシャーにより、読み取り速度と書き込み速度は常に一定に保たれます。さらに驚くべきことは、この最適化されたプログラム コードは以前のものよりも 13 秒高速であることです。

12.13秒 ユーザー 28.50秒 システム 22% CPU 合計 3:03.35

NodeJS ストリームとパイプのおかげで、メモリ負荷が 98.68% 削減され、実行時間も短縮されました。だからこそ、パイプラインは強力な存在なのです。

61.9 MiB は、読み取り可能なストリームによって作成されたバッファのサイズです。読み取り可能なストリームの read メソッドを使用して、バッファ チャンクにカスタム サイズを割り当てることもできます。

const readaball = fs.createReadStream(fileName);
読み取り可能。読み取り(バイトサイズなし);

この手法は、ローカル ファイルのコピーに加えて、多くの I/O 操作の問題を最適化するためにも使用できます。

  • Kafka からデータベースへのデータフローの処理
  • ファイルシステムからのデータストリームを処理し、オンザフライで圧縮してディスクに書き込みます。
  • もっと……

結論は

この記事を書いた主な動機は、NodeJS が優れた API を提供していても、誤ってパフォーマンスの悪いコードを書いてしまう可能性があることを示すことです。組み込まれているツールにもっと注意を払うことができれば、プログラムの実行方法をより最適化できるでしょう。

上記は、Node.js を使用してメモリ効率の高いアプリケーションを作成する方法の詳細な内容です。Node.js の詳細については、123WORDPRESS.COM の他の関連記事に注目してください。

以下もご興味があるかもしれません:
  • JavaScript のメモリ空間、割り当て、深いコピーと浅いコピーの詳細な説明
  • JavaScript のメモリリークを理解するための記事
  • NodeJs の高メモリ使用量のトラブルシューティング実戦記録
  • JavaScript のガベージコレクションメカニズムとメモリ管理
  • 一般的な JS メモリ リークとその解決策の分析
  • JavaScript メモリ モデルの例の詳細な説明
  • JS によるメモリリークの例をいくつか分析する
  • JavaScriptのスタックメモリとヒープメモリの詳しい説明
  • JavaScript のメモリリークに対処する方法
  • JSメモリ空間の詳細な説明

<<:  SQL インジェクションの詳細

>>:  nginx で SSL 証明書を設定して https サービスを実装する方法

推薦する

CentOS 7にMySQLをインストールする詳細な手順

CentOS7では、MySQLをインストールすると、MariaDBもデフォルトでインストールされます...

mysql bin-log ログファイルを sql ファイルに変換する方法

mysqlbinlogのバージョンを表示mysqlbinlog -V [--version] bin...

Linux でファイアウォールがオフになっているかどうかを確認する方法

1. サービス方法ファイアウォールのステータスを確認します。 [root@centos6 ~]# サ...

Tomcat のパフォーマンス最適化方法の簡単な概要

Tomcat自体の最適化Tomcat メモリ最適化起動時に大きなメモリ ブロックが必要であることを ...

CentOS7 インストール GUI インターフェースとリモート接続の実装

ブラウザ (Web ドライバー) ベースの Selenium テクノロジを使用してデータをクロールす...

MySQL 8.0 をインストールした後、初めてログインするときにパスワードを変更する問題を解決する

MySQL 8.0.16で初回ログイン時のパスワードを変更する方法を紹介します。 MySQLデータベ...

Dockerを使用してサーバー上で複数のPHPバージョンを実行する

PHP7 がリリースされてからかなり時間が経ちますが、パフォーマンスが大幅に向上したことはよく知られ...

同期スクロールを実現するための複数のテーブル要素のサンプルコード

Element UIは、複数のテーブルを同時に水平および垂直にスクロールすることを実装します。 コー...

Linux Centos でスクリプトを使用して Docker をインストールする方法

Dockerの主な機能は何ですか?現在、Docker には少なくとも次のアプリケーション シナリオが...

MySql ファジークエリ JSON キーワード取得ソリューションの例

目次序文オプション1:オプション2:オプション3:オプション4(最終的に採用されたオプション):要約...

ウェブページ経由で jar パッケージを Nexus にアップロードする方法

Maven を使用してプロジェクトを管理する場合、jar パッケージをプライベート ウェアハウスにア...

Vueでフォームデータを取得する方法

目次必要データを取得して送信するテンプレートフィルターフィルターの使用シナリオ要約する必要Vue を...

Jsモジュール化の動作原理とソリューションの詳細な説明

目次1. モジュラーコンセプト2. モジュール化3. モジュール化プロセス1. 通常の記述(グローバ...

Reactにおける不変値の説明

目次不変の値とは何ですか?不変の値を使用するのはなぜですか? Reactのパフォーマンス最適化は不変...