この記事では、Node.js での高いメモリ使用量を追跡して修正する私のアプローチを共有します。
最近、「ライブラリ x のメモリ リーク問題を修正する」というタイトルのチケットを受け取りました。説明には、高メモリ使用量に悩まされ、最終的には OOM (メモリ不足) エラーでクラッシュする十数のサービスを示す Datadog ダッシュボードが含まれており、それらはすべて共通の x ライブラリを持っていました。
私がコードベースを知ったのはつい最近 (2 週間以内) で、それがこのタスクをやりがいのあるものにし、共有する価値のあるものでもありました。
私は 2 つの情報をもとに作業を開始しました:
以下はチケットにリンクされたダッシュボードです:
サービスは Kubernetes 上で実行されており、サービスがメモリ制限に達し、クラッシュ (メモリを再利用) して再び開始するまで、時間の経過とともにメモリが蓄積されていることは明らかでした。
このセクションでは、私が目の前のタスクにどのように取り組み、メモリ使用率が高い原因を特定し、後でそれを修正したかを共有します。
私はコードベースにあまり慣れていなかったので、まずコード、問題のライブラリが何をするのか、どのように使用されるのかを理解したいと思いました。このプロセスにより問題を特定しやすくなるだろうと期待していました。残念ながら、適切なドキュメントはありませんでしたが、コードを読み、サービスがライブラリをどのように利用しているかを検索することで、その要点を理解することができました。これは、Redis ストリームをラップし、イベントの生成と消費に便利なインターフェイスを公開するライブラリでした。コードを読むのに 1 日半かかりましたが、コードの構造と複雑さ (多くのクラス継承と rxjs が不慣れでした) のため、すべての詳細とデータの流れを把握することはできませんでした。
そこで、読むのを一時停止し、実際のコードを観察しながら問題を特定し、テレメトリ データを収集することにしました。
さらなる調査に役立つ利用可能なプロファイリング データ (連続プロファイリングなど) がなかったため、問題をローカルで再現し、メモリ プロファイルのキャプチャを試みることにしました。
Node.js でメモリ プロファイルをキャプチャする方法をいくつか見つけました。
どこを見ればよいのか手がかりがなかったので、ライブラリの中で最も「データ集約型」と思われる部分、つまり Redis ストリームのプロデューサーとコンシューマーを実行することにしました。 Redis ストリームからデータを生成および消費する 2 つの単純なサービスを構築し、メモリ プロファイルをキャプチャして結果を経時的に比較する作業を進めました。残念ながら、サービスに負荷をかけてプロファイルを比較してから数時間後、2 つのサービスのいずれでもメモリ消費量の違いを見つけることができず、すべてが正常に見えました。このライブラリは、Redis ストリームと対話するためのさまざまなインターフェイスと方法を多数公開していました。特に実際のサービスに関するドメイン固有の知識が限られていると、問題を再現するのは予想よりも複雑になることが明らかになりました。
そこで問題は、メモリ リークを捕捉する適切な瞬間と条件をどのように見つけられるかということでした。
前述したように、メモリ プロファイルをキャプチャする最も簡単で便利な方法は、影響を受ける実際のサービスに対して継続的にプロファイリングを行うことですが、私にはこのオプションはありませんでした。私は、追加の労力を必要とせずに必要なデータをキャプチャできるよう、少なくともステージング サービスを活用する方法 (同じように大量のメモリ消費に直面していました) を調査し始めました。
私は、Chrome DevTools を実行中のポッドの 1 つに接続し、経時的にヒープ スナップショットをキャプチャする方法を探し始めました。メモリ リークがステージングで発生していることはわかっていたので、そのデータをキャプチャできれば、少なくともいくつかのホットスポットを特定できるだろうと期待していました。驚いたことに、まさにそれを行う方法があります。
これを行うためのプロセス
kubectl exec -it <nodejs-pod-name> -- kill -SIGUSR1 <node-process-id>
Node.js シグナルの詳細については、シグナル イベントを参照してください
成功した場合は、サービスからのログが表示されるはずです。
Debugger listening on ws://127.0.0.1:9229/.... For help, see: https://nodejs.org/en/docs/inspector
kubectl port-forward <nodejs-pod-name> 9229
そうでない場合は、ターゲット検出設定が適切に設定されていることを確認してください
これで、時間をかけてスナップショットのキャプチャを開始し (期間はメモリ リークが発生するまでに必要な時間によって異なります)、比較できるようになります。 Chrome DevTools は、これを行うための非常に便利な方法を提供します。
メモリ スナップショットと Chrome 開発ツールの詳細については、ヒープ スナップショットの記録
をご覧ください。スナップショットを作成すると、メインスレッドの他のすべての作業が停止します。ヒープの内容によっては、1 分以上かかる場合もあります。スナップショットはメモリに組み込まれるため、ヒープ サイズが 2 倍になり、メモリ全体がいっぱいになってアプリがクラッシュする可能性があります。
運用環境でヒープ スナップショットを取得する場合は、アプリケーションの可用性に影響を与えることなく、取得元のプロセスがクラッシュしても問題がないことを確認してください。
Node.js ドキュメントより
私の場合に戻り、比較するために 2 つのスナップショットを選択し、デルタで並べ替えると、以下に示すような結果が得られました。
最大のプラスの差分が文字列コンストラクターで発生していることがわかります。これは、サービスが 2 つのスナップショットの間に大量の文字列を作成したが、それらはまだ使用されていることを意味します。ここで問題となるのは、それらがどこで作成され、誰が参照しているのかということでした。キャプチャされたスナップショットにリテイナーと呼ばれるこの情報が含まれていて良かったです。
スナップショットと縮小することのない文字列リストを調べていると、ID に似ている文字列のパターンに気づきました。それらをクリックすると、それらを参照しているチェーン オブジェクト (別名リテイナー) が表示されます。ライブラリコードから認識できるクラス名からsentEventsという配列でした。さあ、犯人が決まりました。この時点では公開されていないと私が推測していた ID のリストは増え続けるばかりです。時間をかけて大量のスナップショットを撮影しましたが、大きなプラスのデルタを持つホットスポットとして繰り返し出現したのは、この 1 つの場所でした。
この情報により、コード全体を理解しようとするのではなく、配列の目的、いつデータが設定され、いつクリアされるかに焦点を当てる必要がありました。コードが項目を配列にプッシュしていた場所が 1 か所あり、コードが項目をポップアウトしていた場所が 1 か所あり、修正の範囲が狭まっています。
配列が空になるべきときに空になっていなかったと考えても問題ありません。コードの詳細は省略しますが、基本的に何が起こっているかは次のとおりです:
これがどこへ向かうかわかりますか? ?サービスがイベントを生成するためだけにライブラリを使用していた場合でも、sentEvents にはすべてのイベントが設定されますが、それをクリアするためのコード パス (コンシューマ) がありませんでした。
プロデューサー、コンシューマー モードでのみイベントを追跡するようにコードにパッチを適用し、ステージングにデプロイしました。ステージング負荷があったとしても、このパッチが高いメモリ使用量の削減に役立ち、いかなるリグレッションも引き起こさないことは明らかでした。
パッチが運用環境にデプロイされると、メモリ使用量が大幅に削減され、サービスの信頼性が向上しました (OOM がなくなりました)。
嬉しい副作用として、同じトラフィックを処理するのに必要なポッドの数が 50% 削減されました。
これは、Node.js でのメモリ問題の追跡と、利用可能なツールにさらに慣れることに関して、私にとって素晴らしい学習機会でした。
各ツールの詳細については別の投稿に値するため、ここでは触れない方がよいと考えましたが、このトピックについて詳しく知りたい人や、同様の問題に直面している人にとって、これが良い出発点となることを願っています。
以上がNode.js での高いメモリ使用量を追跡するの詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。