Node.jsでのメモリ誤用問題の解決

最近、 Reside Real Estateという会社が問題に直面しました。最も重要な瞬間に、Node.jsサーバーが落ち始めました。 疑いは記憶に落ちました。 会社の従業員は、ユーザーの不便を解消する一時的な措置に頼り、問題の原因を探し始めました。 その結果、彼らは問題を見つけて修正することができました。









この記事では、メモリ使用量エラーを探して修正する方法について説明します。 つまり、メモリリーク、およびプログラムが実際に必要とするよりもはるかに多くのメモリを使用する状況について説明します。 このストーリーは、サーバーの奇妙な動作の理由をすぐに理解し、すぐにサービスに戻すために、似たようなものに出くわした人々を助けるでしょう。



メモリの問題の種類



▍メモリリーク



コンピュータサイエンスでは、メモリリークとは、プログラムがメモリ割り当てを誤って管理し、その結果、不要になったメモリが解放されない場合に発生する、制御不能なリソースの使用の一種です。



Cなどの低レベル言語では、次のようにメモリが割り当てられている状況でメモリリークが発生することがよくありますbuffer = malloc(num_items*sizeof(double));



、ただし、メモリが不要になった後は解放しないでください: free(buffer);







自動メモリ割り当て解除制御を備えた言語では、実行可能プログラムまたは何らかのルートオブジェクトから不要になったエンティティにアクセスできる場合にリークが発生します。 JavaScriptの場合、プログラムからアクセスできるオブジェクトはそれぞれガベージコレクターによって破壊されることはなく、ヒープで占有されているスペースは解放されません。 ヒープサイズが大きくなりすぎると、メモリが不足します。



memoryメモリの過剰使用



メモリが過剰に使用される状況では、プログラムは、割り当てられたタスクを解決するために必要なメモリよりもはるかに多くのメモリを消費します。 たとえば、これは、プログラムが正常に動作するために必要以上に長いオブジェクトへのリンクが保存されている場合に発生し、ガベージコレクターがこれらのオブジェクトを破壊するのを防ぎます。 これは、プログラムが単に必要としないメモリにラージオブジェクトが格納されている場合に発生します(これにより、以下で説明する2つの主な問題のいずれかが発生します)。



メモリの問題を特定する



私たちの記憶の問題は、主にこの厳しいジャーナルメッセージの形で、非常に明白な形で現れました。



 FATAL ERROR: CALL_AND_RETRY_LAST Allocation failed - JavaScript heap out of memory
      
      





メモリリークの兆候には、プログラムのパフォーマンスの経時的な低下も含まれます。 サーバーが同じプロセスを定期的に実行している場合、これは最初は高速ですが、障害が徐々に遅くなる前に、メモリリークを示している可能性があります。



通常、過剰なメモリ使用の兆候は、プログラムのパフォーマンスの低下につながります。 ただし、時間の経過に伴う漏れのないメモリの過剰使用は、パフォーマンスの低下につながりません。



回避策



多くの場合、何かが起こると、問題の本質を理解して修正する時間がありません。 絶対にありませんでした。 幸いなことに、Nodeプロセスに割り当てられるメモリの量を増やす方法があります。 V8エンジンには、64ビットコンピューターで約1.5 GBの標準メモリ制限があります。 はるかに多くのRAMを搭載したコンピューターでNodeプロセスを実行しても、この制限を大きくしない限り、これは問題になりません。 制限を増やすために、 max_old_space_size



キーをNodeプロセスに渡すことができます。 次のようになります。



 node --max_old_space_size=$SIZE server.js
      
      





$SIZE



パラメーターはメガバイト単位で指定され、理論的には特定のコンピューターで意味のある任意の数値にすることができます。 私たちの場合、パラメーター8000が使用されました。これは、サーバーの機能を考慮に入れて、調査に十分な時間を獲得できるようにしました。 さらに、動的メモリが増加しました。 Herokuを使用しますが、そこでは単純に行われます。



また、Twilioサービスを使用し、特に大量のメモリを必要とするサーバーにリクエストが到着するたびに通知されるように設定しました。 これにより、リクエストを監視し、完了後にサーバーを再起動することができました。 このような解決策は不完全ですが、ユーザーに障害が発生するのを防ぐために、休みのない24時間体制であっても何にでも対応できました。



デバッグ



そのため、ノードの設定とサーバー監視の組織のおかげで、問題の根本原因に到達するために費やすことができる時間を獲得しました。 一見、「サーバーメモリの問題」はひどいものであるように思えるかもしれません。この「問題」を取り除くには素晴らしいツールとスキルが必要になります。 しかし、実際には、すべてがそれほど怖いわけではありません。 アプリケーションを研究するための非常にアクセスしやすいツールがあり、ヒントを見つけることができる多くの資料があります。 Nodeサーバーのメモリの調査には、Chrome 開発者のツールを使用します



▍スナップショットヒープ



メモリリークは、ヒープサイズの増加につながる問題です。 その結果、サーバーの通常の操作を続行するには、ヒープが大きすぎます。 したがって、調査の最初に、一定の間隔でヒープのスナップショット(スナップショット)をいくつか取得し、Chrome開発者ツールを使用してこれらのスナップショットの調査に没頭し、ヒープが非常に大きい理由と成長する理由を理解する必要があります。 しばらくしてからいくつかのスナップショットを作成する必要があることに注意してください。その結果、あるスナップショットから別のスナップショットに切り替わるオブジェクトを調べることができます。 これらのオブジェクトは、おそらくメモリリークの原因です。 ヒープスナップショットを作成する方法は多数あります。



he heapdumpを使用してヒープスナップショットを作成する



heapdumpを使用して、ヒープスナップショットを作成しました。 このnpmパッケージは非常に有用であることが判明しました。 コードにインポートして、スナップショットを作成する必要があるプログラムの場所でアクセスできます。 たとえば、サーバーがメモリを集中的に使用しているプロセスを引き起こすリクエストを受信するたびにスナップショットを作成しました。 すぐに現在の時刻を含むファイル名を作成しました。 したがって、サーバーに新しいリクエストを送信することで、問題を再現できます。 コードでは次のようになります。



 import heapdump from 'heapdump'; export const handleUserRequest = (req) => { heapdump.writeSnapshot(   `1.User_Request_Received-${Date.now()}.heapsnapshot`,   (err, filename) => {     console.log('dump written to', filename); }); return startMemoryIntensiveProcess(req); };
      
      





Chrome Chromeリモートデバッガーを使用してヒープスナップショットを作成する



Node 6.3で作業している場合。 またはそれ以降のバージョンでは、Chromeリモートデバッガーを使用してヒープスナップショットを作成できます。 これを行うには、まずこの種類のコマンドでノードを起動しnode --inspect server.j



node --inspect server.j



s。 次にchrome://inspect



ます。 Nodeプロセスをリモートでデバッグできるようになりました。 時間を節約するために、 この Chrome プラグインをインストールできます。 このプラグインは、-- --inspect



フラグを指定してNodeを起動すると、デバッガータブを自動的に開きます。 その後、必要に応じてスナップショットを作成します。









Chromeリモートデバッグツールとヒープスナップショット



スナップショットの読み込みとメモリの問題の種類の特定



次のステップは、Chrome開発者ツールの[メモリ]タブでスナップショットをダウンロードすることです。 リモートChromeデバッガーを使用してヒープスナップショットを作成した場合、それらは既に読み込まれています。 heapdumpを使用した場合は、自分でダウンロードする必要があります。 必ず正しい順序で、つまり作成された順にダウンロードしてください。



作業のこの段階で注意を払う必要がある最も重要なことは、リークまたはメモリの過剰使用を理解することです。 メモリリークがある場合は、問題の原因を探してヒープの調査を開始するのに十分なデータを既に受信している可能性があります。 ただし、過剰なメモリ使用量がある場合は、意味のあるデータを取得するために他の分析方法を試す必要があります。



以下に示すように、最初のメモリの問題は、Chrome開発者ツールの[メモリ]タブにあります。 山が常に成長しているのは簡単にわかります。 これは、メモリリークを示しています。









時間とともにヒープが増加する-明らかなメモリリーク



リークを修正してから数か月後に生じた2番目のメモリの問題は、結果として、同じテストで次の図に示すようになりました。









ヒープは時間とともに成長しません-メモリリークではありません



ヒープサイズは時間とともに変化しません。 問題は、メモリを過度に使用すると、そのサイズが常にいくつかの予想されるインジケータを超えるわけではなく、特定の操作を実行するときだけであるということです。 同時に、スナップショットは、メモリを過剰に使用する状況に結び付けられない瞬間に作成されます。 スナップショットの作成時に、誤って記述されたリソース集中型機能が実行されなかった場合、ヒープには、この機能で使用されるメモリに関する貴重な情報が含まれません。



このような問題を特定するには、問題の原因を特定するのに役立つ2つの方法(関数と変数)をお勧めします。 これは、メモリ割り当てプロファイルと、深刻な負荷がかかっているサーバーでのスナップショットの作成の記録です。



Nodeバージョン6.3以降を使用している場合、前述の--inspect



キーを使用してNodeを実行することにより、リモートChromeデバッガーでメモリ割り当てプロファイルを作成できます。 これにより、個々の関数が時間とともにメモリを使用する方法に関する情報が提供されます。









メモリ割り当てプロファイルの書き込み



別のオプションは、多くの同時リクエストをサーバーに送信し、これらのリクエストの処理中に多くのスナップショットを作成することです(結果として、サーバーが非同期に動作すると想定されているため、一部のスナップショットは他のものよりもはるかに大きくなる場合があり、これは問題を示します) サーバーにリクエストを送り込み、スナップショットを作成しました。 それらのいくつかは非常に大きいことが判明しました。 これらのスナップショットを調べて、問題の原因を特定できます。



スナップショット分析



これで、メモリの問題の原因を見つけるのに非常に役立つデータが得られました。 特に、連続して作成されたスナップショットのサイズが大きくなる状況の分析を検討します。 これは、Chrome開発者ツールの[メモリ]タブに読み込まれるスナップショットの1つです。









メモリリークの調査-すべての機能がメールサービスを指している



Retained Sizeメトリックは、ルートオブジェクトから到達できない依存オブジェクトとともにオブジェクトが削除された後に解放されるメモリのサイズです。



分析を開始するには、リストを[保持サイズ]パラメーターで降順に並べ替えてから、大きなオブジェクトの調査に進みます。 この場合、関数名は問題の原因となったコードの一部を指していました。



メモリリークが発生していることが確実だったため、調査は間違ったスコープの変数を探すことから始めなければならないことがわかりました。 郵便サービスのindex.js



ファイルを開くと、すぐにファイルの上部にモジュールレベルの変数が見つかりました。



 const timers = {};
      
      





すべてを整理し、必要な変更を加え、プロジェクトをさらに数回テストし、最終的にメモリリークを修正しました。



2番目の問題はデバッグが困難でしたが、同じアプローチが機能しました。 以下は、Chrome開発者ツールとNodeキー--inspect



を使用して記録したメモリ割り当てプロファイルです。









過度のメモリ使用の原因を検索する



メモリリークを検索しながらデータを分析するときと同様に、関数やオブジェクトの多くの名前は、Node.js用に記述するコードよりも低いレベルにあるため、一見して認識できません。 同様の状況で、なじみのない名前に出会った場合、それを書き留めます。



メモリ割り当てプロファイルにより、関数の1つであるrecordFromSnapshot



。これは良い出発点になりました。 ヒープスナップショットの調査は、メモリリークの検索時に実行された調査と大差なく、非常に大きなtarget



明らかにしました。 これは、 recordFromSnapshot



関数内で宣言された変数でした。 この変数は、アプリケーションの古いバージョンから残っていたため、不要になりました。 それを取り除いて、メモリを過剰に使用することで状況を修正し、以前は40秒かかっていたプロセスを約10秒に加速しました。 このプロセスには追加のメモリは必要ありませんでした。



まとめ



上記の2つのメモリの問題により、私たちはプロジェクトの開発を遅らせ、それ以前に非常に速くなり、サーバーのパフォーマンスを分析しました。 これで、サーバーパフォーマンスの機能が以前よりもはるかに深く理解され、個々の機能の通常の実行に必要な時間と、それらが使用するメモリの量がわかりました。 プロジェクトをさらに拡大するために必要なリソースについて、よりよく理解しています。 そして、最も重要なことは、私たちはもはや記憶の問題を恐れず、将来それらが現れることを期待しないことです。



親愛なる読者! Node.jsでメモリの問題が発生しましたか? もしそうなら、どのようにそれらを解決したか教えてください。



All Articles