SSR環境におけるメモリリークデバッグガイド(1)
この記事は、FEConf 2023で発表された講演
A Guide to Debugging Memory Leaks in SSR Environments (Node.js)
の内容をまとめたものです。
この記事は全2回に分けてお届けします。
Part 1では、メモリリークとは何か、そしてモニタリングツールを使ってその兆候を検知する方法を紹介します。
Part 2では、実際のメモリリークをどのようにデバッグして解消するのか、そのプロセスを解説します。
この記事に掲載されている画像は、すべて同名の講演スライドから引用されたものであり、別途の出典表記は行っておりません。
発表資料はFEConf 2023の公式サイトからダウンロードできます。
「SSR環境(Node.js)におけるメモリリークのデバッグガイド」 – Toss Place フロントエンドエンジニア パク・ジヘ氏による講演(FEConf 2023)
こんにちは、Toss Placeのフロントエンドエンジニア、パク・ジヘです。
この記事では、Node.jsで構築されたサーバーサイドレンダリング(SSR)環境において発生する可能性のあるメモリリークのデバッグ方法について紹介します。
「SSR環境(Node.js)におけるメモリリークのデバッグガイド」というタイトルを見て、どのキーワードが重要だと思いますか?
私自身は、**「Node.js」と「メモリリーク」**が最も重要なキーワードだと考えています。
この中でも、今回は「メモリリーク」という概念に焦点を当てて、実際の経験をもとに話を進めていきます。
ある日、チームのDevOpsエンジニアから「特定のサービスでOOM(Out Of Memory:メモリ不足)エラーが発生している。確認してもらえますか?」と相談を受けました。
最初は、コードを少し見直せばすぐに直ると思っていたのですが、コードを開いても明らかな問題が見つからず、リークは止まりませんでした。
そこで、私は改めて腰を据えて学習し、地道にデバッグして修正に取り組むことにしました。
この記事を通じて、次の2つのメッセージを伝えたいと思います:
- 「私にもできる」と思える自信
- ブラウザのMemoryタブを活用し、複雑な環境でも原因を突き止めるためのノウハウ
もし同じような問題に直面している方がいれば、かつての私のように、この記事がヒントや支えになれば嬉しいです。
メモリリークとは?なぜ問題なのか?
メモリリーク
メモリリークとは、本来解放されるべきメモリが解放されず、使い続けられてしまう状態のことです。
より直感的に理解するために、「エレベーター」を例にしてみましょう。
📝 スライド要約:「降りるべき人が降りずに居座ってしまうエレベーター=解放されないメモリ」
エレベーターの定員が10人で、4人がずっと乗ったままだとしましょう。
他の人が乗り降りしても、常に4人分のスペースが占拠されたままなので、実質的に使えるスペースは6人分しかありません。
結果として効率が悪くなり、すぐに満員になります。
これと同じで、不要になったメモリが解放されず残っている状態をメモリリークと呼びます。
なぜメモリリークは危険なのか?
エレベーターが非効率だと困るのと同じように、アプリケーションでもメモリが十分にないと性能が著しく低下します。
JavaScriptは**ガーベジコレクタ(GC)**によって不要なメモリを自動的に解放しますが、メモリリークがあるとGCの稼働頻度が上がり、CPU使用率が増加します。
その結果、イベントループがブロックされる可能性が出てきます。
イベントループはJavaScriptの中核を担っているため、ここが滞るとアプリケーション全体が遅延します。
最悪の場合、サーバーがクラッシュすることもあります。
自動再起動の仕組みがあったとしても、その一瞬はユーザーにサービスを提供できない=サービス停止に繋がります。
つまり、メモリリークはパフォーマンス低下、安定性の欠如、サーバーダウンなど多くの問題を引き起こします。
📝 スライド要約:「メモリリークはメモリ不足、GCの負荷増大、イベントループの遅延、サーバークラッシュを引き起こす」
メモリリークの検出方法
では、Node.js環境でどうやってメモリリークを検知すればよいのでしょうか?
もっとも一般的な兆候は、Node.jsを実行しているターミナルにheap out of memory
というエラーが表示されることです。
📝 スライド要約:「'heap out of memory'エラー=典型的なメモリリークの兆候」
とはいえ、常にターミナルを監視しているわけにもいきません。
現実的には、モニタリングツールを用いてサーバーの状態をグラフなどで可視化しながら観察します。
サーバー環境であれば、こうしたモニタリングは比較的簡単に設定できます。
一方、クライアント(ブラウザ)環境では、ユーザーごとにブラウザやハードウェアが異なるため、観測が難しくなります。
とはいえ、デバッグの基本的な方法はサーバーでもクライアントでも共通ですので、今回は共通アプローチで説明します。
モニタリングツールでメモリリークを検知する
このセクションでは、実際のソースコードを使って、モニタリングツールを通じてメモリリークを観察する方法を紹介します。
意図的にメモリリークを仕込んだコードを用意し、これをデバッグしていきます。
以下の例では、リークがないバージョンとあるバージョンの2つを比較します。
📝 スライド要約:「Node.jsの基本的なHTTPサーバーコード」
上記は、リクエストに対してHTMLを返す単純なNode.jsサーバーです。
📝 スライド要約:「2つのコードは1行だけ異なる。その違いがメモリリークを生む」
条件文を使って、メモリリークあり/なし
を切り替えられるようにしています。
📝 スライド要約:「トラフィックを模倣するため、一定間隔でリクエストを送るシェルスクリプト」
まずはメモリリークのないバージョンを見てみましょう。
このバージョンでは、listItems
配列を関数スコープ内に定義しています。
ループで100万件のデータを生成し、使用中のヒープメモリを表示します。
📝 スライド要約:「関数内で定義されたリストは、使用後にGCで回収される」
このコードを実行しても、メモリ使用量は25MB前後で安定しており、異常な増加は見られません。
📝 スライド要約:「メモリ使用量が安定している=メモリリークなし」
モニタリングツール上でもフラットなグラフが確認できるはずです。
このように、明確な増加傾向がない場合、メモリリークの可能性は低いと判断できます。
📝 スライド要約:「フラットなグラフ=安定したメモリ使用」
では、今度はメモリリークありのバージョンを見てみましょう。
このバージョンでは、listItems
が**関数外(グローバルスコープ)**に定義されています。
📝 スライド要約:「グローバルに定義されたリストは解放されず、メモリリークを起こす」
実行すると、メモリ使用量が33MBから193MBまで増加し、最終的にはheap out of memory
でプロセスがクラッシュします。
📝 スライド要約:「時間とともにメモリが増加し、最終的にクラッシュ」
このときのモニタリンググラフはどうなるでしょうか?
右肩上がりのグラフになり、サーバーがクラッシュして再起動されるたびに急降下→再上昇という形になります。
これが**「山型」パターン**で、メモリリークの典型的な兆候です。
📝 スライド要約:「山型のグラフ=再起動と再発を繰り返すメモリリーク」
次回は、このメモリリークをどのようにデバッグし、解決するかについて詳しく解説していきます。
Discussion