私はかつてV8ツインターボエンジンを搭載したアウディを運転しましたが、その性能は素晴らしかったです。道路に誰もいない午前3時にシカゴ近くのIL-80高速道路を約140MPHで運転していました。それ以来、「V8」という言葉は私にとって高性能に関連するようになりました。
冷戦中の技術Node.jsは、ChromeのV8 JavaScriptエンジン上に構築されたプラットフォームであり、高速でスケーラブルなネットワークアプリケーションを簡単に構築できます。
アウディのV8は非常に強力ですが、それでもガソリンタンクの容量には制限があります。同じことがGoogleのV8(Node.jsの背後にあるJavaScriptエンジン)にも当てはまります。そのパフォーマンスは信じられないほどであり、Node.jsには多くの理由があります 多くのユースケースでうまく機能します 、ただし、ヒープサイズによって常に制限されます。 Node.jsアプリケーションでさらにリクエストを処理する必要がある場合は、垂直方向にスケーリングするか、水平方向にスケーリングするかの2つの選択肢があります。水平スケーリングは、より多くの同時アプリケーションインスタンスを実行する必要があることを意味します。正しく実行すると、より多くのリクエストを処理できるようになります。垂直スケーリングとは、アプリケーションのメモリ使用量とパフォーマンスを改善するか、アプリケーションインスタンスで利用可能なリソースを増やす必要があることを意味します。
最近、メモリリークの問題を修正するために、ApeeScapeクライアントの1つでNode.jsアプリケーションを使用するように依頼されました。 APIサーバーであるこのアプリケーションは、毎分数十万のリクエストを処理できるようにすることを目的としていました。元のアプリケーションは約600MBのRAMを占有していたため、ホットAPIエンドポイントを取得して再実装することにしました。多くの要求に対応する必要がある場合、オーバーヘッドは非常に高価になります。
新しいAPIの場合、バックグラウンドジョブ用にネイティブMongoDBドライバーとKueを使用したrestifyを選択しました。非常に軽量なスタックのようですね。完全ではありません。ピーク負荷時には、新しいアプリケーションインスタンスが最大270MBのRAMを消費する可能性があります。したがって、1X HerokuDynoごとに2つのアプリケーションインスタンスを持つという私の夢は消えました。
「ノードのリークを見つける方法」を検索すると、おそらく最初に見つかるツールは memwatch 。元のパッケージはかなり前に放棄され、現在は維持されていません。ただし、GitHubで新しいバージョンを簡単に見つけることができます リポジトリのフォークリスト 。このモジュールは、ヒープが5つの連続したガベージコレクションを超えて増大するのを確認すると、リークイベントを発行する可能性があるため便利です。
を可能にする素晴らしいツール Node.js開発者 ヒープスナップショットを取得し、後でChromeデベロッパーツールで検査します。
実行中のアプリケーションに接続し、ヒープダンプを取得し、その場でデバッグして再コンパイルすることもできるため、ヒープダンプのさらに便利な代替手段です。
残念ながら、Herokuで実行されている本番アプリケーションに接続することはできません。これは、実行中のプロセスにシグナルを送信できないためです。ただし、Herokuだけがホスティングプラットフォームではありません。
node-inspectorの動作を体験するために、restifyを使用して単純なNode.jsアプリケーションを作成し、その中にメモリリークの小さなソースを配置します。ここでのすべての実験は、V8v3.28.71.19に対してコンパイルされたNode.jsv0.12.7を使用して行われます。
var restify = require('restify'); var server = restify.createServer(); var tasks = []; server.pre(function(req, res, next) { tasks.push(function() { return req.headers; }); // Synchronously get user from session, maybe jwt token req.user = { id: 1, username: 'Leaky Master', }; return next(); }); server.get('/', function(req, res, next) { res.send('Hi ' + req.user.username); return next(); }); server.listen(3000, function() { console.log('%s listening at %s', server.name, server.url); });
ここでのアプリケーションは非常に単純で、非常に明白なリークがあります。配列 タスク アプリケーションの存続期間中に成長し、速度が低下し、最終的にはクラッシュします。問題は、クロージャーだけでなく、リクエストオブジェクト全体もリークしていることです。
V8のGCは、ストップザワールド戦略を採用しているため、メモリ内にあるオブジェクトの数が多いほど、ガベージの収集にかかる時間が長くなります。以下のログを見ると、アプリケーションの寿命の初めにゴミを収集するのに平均20ミリ秒かかることがはっきりとわかりますが、数十万のリクエストの後は約230ミリ秒かかります。私たちのアプリケーションにアクセスしようとしている人は待たなければならないでしょう 230ms GCのおかげで今は長くなっています。また、GCが数秒ごとに呼び出されることがわかります。これは、数秒ごとにユーザーがアプリケーションへのアクセスで問題が発生することを意味します。また、アプリケーションがクラッシュするまで遅延が大きくなります。
[28093] 7644 ms: Mark-sweep 10.9 (48.5) -> 10.9 (48.5) MB, 25.0 ms [HeapObjectsMap::UpdateHeapObjectsMap] [GC in old space requested]. [28093] 7717 ms: Mark-sweep 10.9 (48.5) -> 10.9 (48.5) MB, 18.0 ms [HeapObjectsMap::UpdateHeapObjectsMap] [GC in old space requested]. [28093] 7866 ms: Mark-sweep 11.0 (48.5) -> 10.9 (48.5) MB, 23.2 ms [HeapObjectsMap::UpdateHeapObjectsMap] [GC in old space requested]. [28093] 8001 ms: Mark-sweep 11.0 (48.5) -> 10.9 (48.5) MB, 18.4 ms [HeapObjectsMap::UpdateHeapObjectsMap] [GC in old space requested]. ... [28093] 633891 ms: Mark-sweep 235.7 (290.5) -> 235.7 (290.5) MB, 357.3 ms [HeapObjectsMap::UpdateHeapObjectsMap] [GC in old space requested]. [28093] 635672 ms: Mark-sweep 235.7 (290.5) -> 235.7 (290.5) MB, 331.5 ms [HeapObjectsMap::UpdateHeapObjectsMap] [GC in old space requested]. [28093] 637508 ms: Mark-sweep 235.7 (290.5) -> 235.7 (290.5) MB, 357.2 ms [HeapObjectsMap::UpdateHeapObjectsMap] [GC in old space requested].
これらのログ行は、Node.jsアプリケーションが –trace_gc 国旗:
node --trace_gc app.js
このフラグを使用してNode.jsアプリケーションを既に開始していると仮定します。アプリケーションをnode-inspectorに接続する前に、実行中のプロセスにSIGUSR1シグナルを送信する必要があります。クラスターでNode.jsを実行する場合は、必ずスレーブプロセスの1つに接続してください。
kill -SIGUSR1 $pid # Replace $pid with the actual process ID
これにより、Node.jsアプリケーション(正確にはV8)をデバッグモードにします。このモードでは、アプリケーションは自動的にポート5858を開きます。 V8デバッグプロトコル 。
次のステップは、実行中のアプリケーションのデバッグインターフェイスに接続し、ポート8080で別のWebインターフェイスを開くnode-inspectorを実行することです。
$ node-inspector Node Inspector v0.12.2 Visit http://127.0.0.1:8080/?ws=127.0.0.1:8080&port=5858 to start debugging.
アプリケーションが本番環境で実行されていて、ファイアウォールが設定されている場合は、リモートポート8080をローカルホストにトンネリングできます。
ssh -L 8080:localhost:8080 [email protected]
これで、Chrome Webブラウザーを開いて、リモートの本番アプリケーションに接続されているChrome開発ツールにフルアクセスできます。残念ながら、Chromeデベロッパーツールは他のブラウザでは機能しません。
V8でのメモリリークは、C / C ++アプリケーションからわかっているため、実際のメモリリークではありません。 JavaScriptでは、変数は空白に消えることはなく、単に「忘れられる」だけです。私たちの目標は、これらの忘れられた変数を見つけて、ドビーが無料であることを彼らに思い出させることです。
Chromeデベロッパーツール内では、複数のプロファイラーにアクセスできます。特に興味があります ヒープ割り当てを記録する これは実行され、時間の経過とともに複数のヒープスナップショットを取得します。これにより、どのオブジェクトがリークしているかを明確に確認できます。
ヒープ割り当ての記録を開始し、ApacheBenchmarkを使用してホームページで50人の同時ユーザーをシミュレートしましょう。
ab -c 50 -n 1000000 -k http://example.com/
新しいスナップショットを作成する前に、V8はマークスイープガベージコレクションを実行するため、スナップショットに古いガベージがないことは間違いありません。
の期間にわたってヒープ割り当てスナップショットを収集した後 3分 最終的には次のようになります。
ブートストラップの操作方法
いくつかの巨大な配列、多くのIncomingMessage、ReadableState、ServerResponse、およびDomainオブジェクトもヒープ内にあることがはっきりとわかります。リークの原因を分析してみましょう。
チャートで20秒から40秒までのヒープ差分を選択すると、プロファイラーを開始してから20秒後に追加されたオブジェクトのみが表示されます。このようにして、すべての正規データを除外できます。
システム内にある各タイプのオブジェクトの数に注意して、フィルターを20秒から1分に拡張します。すでにかなり巨大なアレイが成長し続けていることがわかります。 「(配列)」の下に、等距離のオブジェクト「(オブジェクトプロパティ)」がたくさんあることがわかります。これらのオブジェクトは、メモリリークの原因です。
また、「(closure)」オブジェクトも急速に成長していることがわかります。
文字列も見ると便利かもしれません。文字列リストの下には、「HiLeakyMaster」というフレーズがたくさんあります。それらは私たちにもいくつかの手がかりを与えるかもしれません。
私たちの場合、文字列「HiLeakyMaster」は「GET /」ルートでしか組み立てられないことがわかっています。
リテーナパスを開くと、この文字列が何らかの形で参照されていることがわかります。 必須 、次にコンテキストが作成され、これらすべてがクロージャの巨大な配列に追加されます。
したがって、この時点で、ある種の巨大なクロージャの配列があることがわかります。実際に行って、[ソース]タブですべてのクロージャにリアルタイムで名前を付けましょう。
コードの編集が完了したら、CTRL + Sを押して、コードをその場で保存および再コンパイルできます。
では、別の録音をしましょう ヒープ割り当てスナップショット どのクロージャがメモリを占有しているかを確認します。
それは明らかです SomeKindOfClojure() 私たちの悪役です。今、私たちはそれを見ることができます SomeKindOfClojure() クロージャは、という名前の配列に追加されています タスク グローバル空間で。
この配列が役に立たないことは簡単にわかります。コメントアウトできます。しかし、どのようにしてすでに占有されているメモリを解放するのでしょうか?非常に簡単です。空の配列をに割り当てるだけです。 タスク 次のリクエストでオーバーライドされ、次のGCイベント後にメモリが解放されます。
ドビーは無料です!
V8ヒープは、いくつかの異なるスペースに分割されています。
mmap
‘ed領域がありますCell
s、PropertyCell
s、およびMap
sが含まれます。これは、ガベージコレクションを簡素化するために使用されます。各スペースはページで構成されています。ページは、mmapを使用してオペレーティングシステムから割り当てられたメモリの領域です。大きなオブジェクトスペースのページを除いて、各ページのサイズは常に1MBです。
モノのインターネットのセキュリティ問題
V8には、Scavenge、Mark-Sweep、Mark-Compactの2つのガベージコレクションメカニズムが組み込まれています。
スカベンジは非常に高速なガベージコレクション手法であり、 新しいスペース 。スカベンジはの実装です チェイニーのアルゴリズム 。アイデアはとてもシンプルです、 新しいスペース To-SpaceとFrom-Spaceの2つの等しい半空間に分割されます。スカベンジGCは、To-Spaceがいっぱいになると発生します。 ToスペースとFromスペースを交換し、すべてのライブオブジェクトをTo-Spaceにコピーするか、2回の清掃を生き延びた場合は古いスペースのいずれかにプロモートし、スペースから完全に消去します。スカベンジは非常に高速ですが、2倍のサイズのヒープを保持し、メモリ内のオブジェクトを常にコピーするというオーバーヘッドがあります。スカベンジを使用する理由は、ほとんどのオブジェクトが若くして死ぬためです。
Mark-Sweep&Mark-Compactは、V8で使用される別のタイプのガベージコレクターです。もう1つの名前はフルガベージコレクターです。すべてのライブノードにマークを付けてから、すべてのデッドノードをスイープし、メモリをデフラグします。
Webアプリケーションの場合、高性能はそれほど大きな問題ではないかもしれませんが、それでもリークを絶対に避けたいと思うでしょう。フルGCのマークフェーズ中、ガベージコレクションが完了するまでアプリケーションは実際に一時停止されます。つまり、ヒープ内のオブジェクトが多いほど、GCの実行にかかる時間が長くなり、ユーザーが待機する時間が長くなります。
すべてのクロージャと関数に名前が付いていると、スタックトレースとヒープを調べるのがはるかに簡単になります。
db.query('GIVE THEM ALL', function GiveThemAllAName(error, data) { ... })
理想的には、すべてのデータが収まるように、ホット関数内の大きなオブジェクトを避けたいと考えています 新しいスペース 。 CPUとメモリにバインドされたすべての操作は、バックグラウンドで実行する必要があります。また、ホット関数の最適化解除トリガーを回避します。最適化されたホット関数は、最適化されていない関数よりも少ないメモリを使用します。
実行速度は速いがメモリ消費量が少ないホット関数は、GCの実行頻度を減らします。 V8は、最適化されていない関数または最適化されていない関数を見つけるための便利なデバッグツールをいくつか提供します。
インラインキャッシュ(IC)は、オブジェクトプロパティアクセスをキャッシュすることにより、コードの一部のチャンクの実行を高速化するために使用されますobj.key
またはいくつかの単純な関数。
function x(a, b) { return a + b; } x(1, 2); // monomorphic x(1, “string”); // polymorphic, level 2 x(3.14, 1); // polymorphic, level 3
いつ x(a、b) 初めて実行されると、V8は単形ICを作成します。電話をかけるときx
2回目は、V8が古いICを消去し、整数と文字列の両方のタイプのオペランドをサポートする新しいポリモーフィックICを作成します。 3回目にICを呼び出すと、V8は同じ手順を繰り返し、レベル3の別の多形ICを作成します。
ただし、制限があります。 ICレベルが5に達した後(で変更可能 –max_inlining_levels フラグ)関数はメガモーフィックになり、最適化可能とは見なされなくなります。
単相関数が最も高速に実行され、メモリフットプリントも小さいことは直感的に理解できます。
これは明白でよく知られています。大きなCSVファイルなど、処理する大きなファイルがある場合は、ファイル全体をメモリにロードするのではなく、1行ずつ読み取り、小さなチャンクで処理します。 csvの1行が1MBを超えるという非常にまれなケースがあります。そのため、これを収めることができます。 新しいスペース 。
画像のサイズを変更するAPIなど、処理に時間がかかるホットAPIがある場合は、別のスレッドに移動するか、バックグラウンドジョブに変換します。 CPUを集中的に使用する操作では、メインスレッドがブロックされ、他のすべての顧客が待機して要求を送信し続ける必要があります。未処理のリクエストデータはメモリにスタックされるため、完全なGCが完了するまでに長い時間がかかります。
私はかつてrestifyで奇妙な経験をしました。無効なURLに数十万のリクエストを送信すると、アプリケーションのメモリは最大100メガバイトで急速に増加し、数秒後に完全なGCが開始されます。これにより、すべてが正常に戻ります。無効なURLごとに、restifyは長いスタックトレースを含む新しいエラーオブジェクトを生成することがわかりました。これにより、新しく作成されたオブジェクトがに割り当てられました 大きなオブジェクトスペース ではなく 新しいスペース 。
このようなデータにアクセスできることは、開発中に非常に役立つ可能性がありますが、本番環境では明らかに必要ありません。したがって、ルールは単純です。確かに必要でない限り、データを生成しないでください。
最後に、もちろん重要なことですが、ツールを知ることです。さまざまなデバッガー、リークキャザー、および使用状況グラフジェネレーターがあります。これらのツールはすべて、ソフトウェアをより高速かつ効率的にするのに役立ちます。
V8のガベージコレクションとコードオプティマイザーがどのように機能するかを理解することは、アプリケーションのパフォーマンスの鍵です。 V8はJavaScriptをネイティブアセンブリにコンパイルし、場合によっては、適切に記述されたコードがGCCコンパイル済みアプリケーションと同等のパフォーマンスを達成する可能性があります。
ご参考までに、私のApeeScapeクライアント用の新しいAPIアプリケーションは、改善の余地はありますが、非常にうまく機能しています。
Joyentは最近、V8の最新バージョンの1つを使用するNode.jsの新しいバージョンをリリースしました。 Node.js v0.12.x用に作成された一部のアプリケーションは、新しいv4.xリリースと互換性がない場合があります。ただし、アプリケーションでは、新しいバージョンのNode.js内でパフォーマンスとメモリ使用量が大幅に向上します。