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 サービスを実装する方法

推薦する

Linux で FTP イメージ サーバーをインストールして展開する方法

Linux で FTP サーバーを設定するためのチュートリアルを参照してください https://w...

Vue要素はテーブルの追加、削除、データの変更を実装します

この記事では、テーブル内のデータを追加、削除、変更するためのvue要素の具体的なコードを参考までに共...

MySQLデータベースの一般的な最適化操作のまとめ(経験共有)

序文データ中心のアプリケーションの場合、データベースの品質はプログラムのパフォーマンスに直接影響する...

ミニプログラム録画機能の実装

序文ミニプログラムを開発する過程では、録音機能を実装し、録音を再生し、録音をサーバーにアップロードす...

Docker で Elasticsearch Kibana と ik Word Segender をデプロイする詳細な説明

esインストール docker pull elasticsearch:7.4.0 # -d : バッ...

Centos7のホスト名を変更する3つの方法

方法 1: hostnamectl の変更ステップ1 ホスト名を確認するホスト名ステップ2 ホスト名...

SQL 実装 LeetCode (185. 部門内で最も給与の高い上位 3 名)

[LeetCode] 185. 部門別給与上位3位従業員テーブルにはすべての従業員が保持されます。...

JavaScript オブジェクト指向クラス継承ケースの説明

1. オブジェクト指向のクラス継承これまでの章では、JavaScript のオブジェクト モデルがプ...

WeChatミニプログラム開発のためのコンポーネント設計仕様

WeChat ミニプログラム コンポーネント設計仕様コンポーネントベースの開発という考え方は、私の開...

Nodeイベントループの包括的な理解

目次ノードイベントループイベントループ図メインスレッドイベントループタイマーキューの仕組み投票キュー...

CSS スタイルの優先順位はどれくらい複雑ですか?

昨晩、面接の質問を見ていたら、CSS スタイルの優先順位について特に明確に説明していない人が何人かい...

Dockerイメージの作成、保存、読み込み方法

イメージを作成する方法は 3 つあります。既存のイメージに基づいてコンテナを作成する、ローカル テン...

MySQLのSQLモードの特徴のまとめ

序文SQL モードは、MySQL がサポートする SQL 構文と、実行されるデータ検証チェックに影響...

Vue ElementUI で Excel ファイルを手動でサーバーにアップロードする方法の詳細な説明

目次概要プロパティ設定処理ロジック概要具体的な需要シナリオは次のとおりです。 Excel ファイルを...

アイデアがWebプロジェクトを公開した後、Tomcatサーバーがプロジェクトとそのソリューションを見つけることができません

概要プロジェクトは正常に作成され、正常にデプロイされましたが、以下に示すように、Tomcat サーバ...