ローグウェーブソフトウェアのブログ

開発をシンプルに 安全で高品質のコードを 素早くお客様のもとへ

分散環境におけるメモリエラー解析 - ホワイトペーパー紹介

こんにちは、ローグウェーブセールスエンジニアの柄澤です。暑くじめじめした季節になってきましたが、開発者の皆様にとってアプリケーションのメモリ関係のエラーもこの上なく鬱陶しい存在として日々悩まされているのではと思います。

この記事は、ローグウェーブの英語ホワイトペーパー Memory error analysis in distributed applications を日本語訳したものです。このホワイトペーパーでは、並列実行環境における様々なメモリ関係の問題の紹介と分類、そしてソリューションとして、ローグウェーブの並列デバッガTotalViewに搭載されたメモリデバッグ機能 MemoryScapeがどのように問題に対処するのか、ご紹介します。

分散環境におけるメモリエラー解析

序文

並列アプリケーションはあらゆる場所で使用されています。動画をストリーミング配信したり、常にユーザーのリクエストに応じた様々なコンテンツを表示するWebアプリケーションを提供している企業にとってそれは特に顕著です。そうした業界で使われているアプリケーションは、多くのユーザーからリクエストを受け取り、それぞれがプロセスやスレッドによって処理されます。スレッドやプロセスの数は、任意の時点におけるシステム上のユーザー数に基づいています。アプリケーションによっては何度もスレッドは作成され破棄されますが、時にはメモリが割り当てられたままになることもあります。また、その反対にアプリケーションによっては同じプロセスやスレッドをずっと使い続けることがありますが、長時間実行されるプロセスは、データフィードのサイズに比例した大きなメモリリークを引き起こす可能性があります。メモリのバグ、つまりヒープメモリの管理の失敗により、システムが急速に遅くなったりクラッシュすることにつながります。

メモリバグは一般に追跡が困難です。スタックの上書き、ヒープの解放忘れ、間違ったタイミングでの解放など、さまざまな要因によって生じるからです。現象が顕在化するのに時間がかかるため、原因となったコードの特定の行までトレースするのは簡単でありません。全体として、メモリのバグは、アプリケーションのユーザビリティに大きな損害を与える可能性があります。定期的にサーバーの電源を切って再起動しなければならないようでは、これらのサーバーを信頼することはできません。幸いなことに、解決策があります。このホワイトペーパーでは、根本原因とこれらのバグの解決方法について説明します。

はじめに

メモリバグは、どんなプログラムでも発生する可能性があり、さまざまな要因によって引き起こされます。例えば、エラー状態をチェックしない、標準的でない挙動に頼っている、メモリを解放しない、ダングリングポインタへの参照、配列境界違反、メモリ破損など。これらは、プログラムをクラッシュさせたり、間違ったランダムな結果を生成したり、長期間コードベースに潜んでおり、最も起こってほしくない最悪の瞬間に顕在化することになります。

メモリのリークは、たとえデスクトップ上で実行されているシングルアプリケーションであっても追跡が難しく、まして分散型の並列システムで遭遇すると非常に厄介です。開発者は、大きなデータサイズを処理する必要性に応じて並列プログラムを作成するので、プログラムは必然的に膨大な量のデータをロードし、多くのメモリを使用することになります。

メモリエラーの分類

開発者はヒープメモリの扱いに注意を払わなければなりません。プログラムはコンパイル時や実行時にヒープメモリを暗黙的でなく明示的に管理するためです。プログラムがヒープメモリの扱いを失敗する例のいくつかをご紹介しましょう。ここではC言語malloc() APIの観点から説明しますが、C++のnewやFortran90のallocate文を使用しても、同様のエラーを発生させることができます。

Mallocエラー

プログラムが不正な値をHeap Manager APIの操作の1つに渡すと、Mallocエラーが発生します。これは、例えばポインタの値を別のポインタにコピーした後に、両方のポインタがfree()に渡されたときなどに発生します。

ダングリングポインタ

以前に解放されたメモリを参照しているポインタをdangling(ぶら下がり)ポインタと呼びます。ダングリングポインタによるメモリアクセスは、未定義の動作を引き起こします。ダングリングポインタのバグのあるプログラムは、一見明白なエラーがなく、きちんと機能しているように見えてしまいます。

メモリ境界違反

malloc()によって返された個々のメモリ割り当ては、定義されたサイズを持つ個別のメモリブロックを表します。メモリブロックの最下位アドレスの直前だったり最上位アドレスの直後にあるメモリへアクセスすると、未定義の動作を引き起こします。

Read-Before-Writeエラー

初期化の前にメモリを読み込むことはよくあるエラーです。 多くのコンパイラは、初期化前に行われるローカル変数の読み取りを識別してくれます。しかしポインタがメモリを初期化する前にメモリの読み取りを検出することははるかに困難です。 動的メモリへのアクセスは常にポインタを経由するため、この問題の影響を受けます。

メモリリークの検出

リークが発生するのは、プログラムがメモリブロックの使用を終え、そのブロックへのすべての参照を破棄した後に解放がうまく行われなかったときに発生します。リークの影響は、アプリケーションの性質によって異なります。その影響は時には軽微ですが、リークの割合が十分高かったりプログラムの実行時間が十分に長い場合には、リークによってプログラムのパフォーマンスが大幅に悪化します。リークはしばしば十分理解されたはずのコードの中にも残っているため、とても厄介で腹立たしいものになります。複雑なアプリケーションで、メモリリークが発生しないように割り当てられたメモリが正確に一度だけ解放されるように動的メモリを管理するのは難しい作業です。

「メモリブロックの使用を中止する」を定義するのは難しいですが、高度なメモリデバッガでは、プログラムが特定のメモリロケーションへの参照を保持しているかどうかを調べることによって、リーク検出を実行します。参照できないならばそのメモリは本来解放されているべきです。

メモリリークを検出するには、オペレーティングシステムへのmalloc()呼び出しを監視する必要があります。 malloc()を追跡する一般的な方法は、アプリケーションのmalloc()を置き換えて計測コードを追加することです。このアプローチの主な欠点はアプリケーションを再コンパイルする必要があることで、これにより挙動が変わってしまったりコンパイルの問題を修正するために多くの時間を費やすことにつながります。もう1つの問題として、計測コードがアプリケーションの減速につながる可能性があることです。

Interposition (介入)malloc()を監視する別の方法です。 Interpositionライブラリは、実行時にユーザーのアプリケーションコードとmalloc()サブシステムの間に自身を挿入します。Interpositionライブラリは、各メモリ割り当て関数ごとに関数を定義します。これらの関数は、メモリブロックを割り当てたり解放したりするたびにプログラムから呼び出され、次にその呼び出しをそのままオペレーティングシステムAPI関数に渡します。このメソッドを使用すればmalloc()の呼び出しは通常通り動作しますし、アプリケーションを再コンパイルする必要もありません。

ヒープ境界違反の検出

メモリのブロックは、多くの場合、プログラムデータの他のブロックと連続しています。したがって、プログラムが配列の末尾を越えて書き込んだ場合、しばしば他の無関係な割り当ての内容が上書きされます。プログラムが再実行されると一般に割り当ての順序は変わるため、異なる方法でメモリの上書きが実行されます。これは、実行ごとに結果が変わってしまって非常にフラストレーションのたまる「レイシーな」バグにつながります。プログラムは時にはクラッシュし、時には悪いデータをもたらし、時には完全に無害になることもあります。

MemoryScape デバッガ

MemoryScapeはTotalViewのコンポーネントで、インタラクティブでダイナミックなメモリ解析およびデバッグ用のツールで、メモリに関するバグを解消するのに費やされる時間を削減します。プログラムの再コンパイルが不要で実行時のパフォーマンスへの影響が控えめな軽量なアーキテクチャを備えています。

MemoryScapeは、並列アプリケーションで使用するように設計されており、個々のプロセスに関する詳細情報と、すべてのプロセスにわたる高水準なメモリ使用統計を提供します。これには、並列ジョブのすべてのプロセスの起動やそれらプロセスへのアタッチ、1つのGUIから多数のプロセスをデバッグする機能、バッチ環境で使用するためのスクリプトベースのデバッグ機能が含まれます。

MemoryScapeのアーキテクチャ

MemoryScapeは、メモリの問題を検出するために介入方法を使用します。 そのライブラリは、ヒープ介入エージェント (HIA, Heap Interposition Agent)と呼ばれます (下の図参照。HIAはユーザーアプリケーションとシステムのメモリ管理レイヤーの間に挿入されます)。 Interposition方法を選択する主な理由は、軽量メモリデバッグを提供するためです。 デバッグ中のプログラムの実行時パフォーマンスは、HIAが存在しない場合と同様に機能します。 これは、より「重い」アプローチがプログラムのランタイムが開発者の忍耐を超えたり、データの使用に大きなラグが生じて挙動が変わってしまうような多くのアプリケーションにとって非常に重要です。

f:id:RWSJapan:20170619143655p:plain

MemoryScapeの並列アーキテクチャ

MemoryScapeの並列アーキテクチャはバックグラウンドで分散されており、ユーザーの並列プログラムとの実行時対話を管理します。 MemoryScapeは、ユーザーのコードが実行されているクラスタのノードで実行される軽量のデバッグエージェントプロセスを開始します。これらのプロセスは、個々のローカルのプロセスとデバッグ対象のプロセスにロードされているHIAモジュールとの間の低レベルな対話を管理します。これらのプロセスは、MemoryScapeフロントエンドと直接通信します。

MemoryScapeを使用したメモリ統計の比較

多くのアプリケーションでは、どのくらいメモリを使用するのかあらかじめ想定されています。例えばすべてのノードが同じ量のメモリを似たようなパターンで確保されるように構成される、といったことです。このようにパターンが予想される場合や、ユーザーが一連のプロセスを調べてパターンを探したい場合、MemoryScapeは全てのプロセスやサブセットに対して、メモリ使用の統計をさまざまなグラフ形式 (線グラフ、棒グラフ、円グラフ) で提供するメモリ統計情報ウィンドウを提供します。

ユーザーは、統計情報を表示したいプロセスのセットを選択できます。ビューは、その時点でのプログラムの状態を表します。デバッガプロセスコントロールは、プログラムを新しい地点に到るまで実行し、ビューを更新します。いずれかのプロセスが他と異なるように見える場合、ユーザーはさらにヒープメモリーの詳細な状況を見ることができます。

MemoryScapeを使用したヒープ状態の表示

MemoryScapeは、ヒープの使用状況について広範な形式でのレポートを提供します。 プロセスがブレークポイントやメモリイベントなどで停止するたびに、ヒープのビューを取得できます。 これによりユーザーはプログラムのヒープメモリの構成を一目で見ることができます。 ビューはインタラクティブで、 ブロックを選択すると関連する割り当てが強調表示され、選択されたブロックと共に関連するブロックに関する詳細情報がユーザに提示されます。 ユーザーはサイズや割り当てられた共有オブジェクトなどのプロパティに基づいて、表示項目をフィルタリングすることができます。 f:id:RWSJapan:20170619143847p:plain

MemoryScapeを使用したリークの検出

MemoryScapeは、ヒープメモリのリークを検出してリークレポートを生成します。 結果のレポートには、有効な参照がないプログラム内のすべてのヒープ割り当てがリストされます。 プログラムがどこにも参照を格納していないというメモリのブロックは、すなわちリークです。 ユーザーは、ヒープのグラフィック表示でリークを観察できます。

MemoryScapeを使用したヒープ境界違反の検出

MemoryScapeは、ガードブロックをサポートしています。 ガードブロックとは確保されたヒープメモリの前後に割り当てられるメモリのブロックです。このメモリのビットはプログラムが要求した割り当ての一部ではないので、プログラムは本来その場所を読み書きしてはいけないはずです。 HIAは、ガードブロックを特定のメモリのパターンで初期化し、ガードブロックで変更をチェックします。 変更が行われたということは、プログラムが配列の境界を超えて書き込みを行ったことを意味します。

TotalViewの内部でMemoryScapeを使用する

メモリデバッグのデータファイルは、TotalViewデバッガのメモリモジュールによってロードできます。メモリのデバッグを活かし、すべての変数や状態データへのアクセスを利用して、さらに強力なデバッグ手法を適用することができます。

多くのユーザーがインタラクティブにMemoryScapeを使用しますが、進行中の開発では、新しいメモリバグが混入される可能性もあるでしょう。開発チームのみなさんには、進行中のテスト戦略にヒープメモリに対するテストを追加することを推奨します。 MemoryScapeには、テストフレームワークに組み込めるように設計されたコマンドラインバージョンが存在します。このようにMemoryScapeを使用する開発チームは、新しいメモリエラーが混入してもすぐにそれを検出して分析し、削除します。

結論

メモリバグはどんなプログラムでも発生する可能性があります。これらのタイプのバグは任意のタイミングで混入し、さまざまな要因によって引き起こされるため、開発者にとって大きなフラストレーションの原因となります。メモリバグは長い間コードベースに潜んでおり、さまざまな現れ方をします。

これがメモリのデバッグが困難な作業となる原因です。一般的に使用されている開発ツールとテクニックは、メモリの問題を解決するために特別に設計されたものではないため、メモリのバグを見つけて修正するプロセスをかえってさらに複雑にしてしまう可能性があります。

MemoryScapeは使いやすいメモリデバッグのためのツールで、開発者がメモリバグを発見して解決するのに役立ちます。メモリの使用統計を比較する機能、ヒープ状態を調べる機能、メモリリークを検出する機能など、メモリ問題に特化された機能は、並列アプリケーションや分散アプリケーションのデバッグに最適です。

編集後記

いかがだったでしょうか。ローグウェーブ日本語サイトのTotalViewに関する説明クイックガイドオンラインマニュアル(英語)やこのブログの過去記事をご覧ください。

blog.roguewave.jp

また、トライアルについてもお気軽にお問い合わせください。

roguewave.jp

ローグウェーブ セールスエンジニア 柄澤 (からさわ)