CLR 徹底解剖

CLR 4 における運用向け診断機能の強化

Jon Langdon

共通言語ランタイム (CLR) チームには、他の開発者がマネージ コード向けの診断ツールを作成できる API やサービスを提供することを目的としているグループがあります。このグループが (専門のエンジニアリング リソースに関して) 提供している 2 つの主要コンポーネントが、マネージ デバッグ API (ICorDebug*) とマネージ プロファイル API (ICorProfiler*) です。

CLR やフレームワークに関する他のチームと同様、このグループの価値を決めるのは、メンバーの努力の結果を土台に構築されるアプリケーションだけです。たとえば、Visual Studio に関するチームは、マネージ デバッガーやパフォーマンス プロファイル ツールにこれらのデバッグ API やプロファイル API を利用しています。また、多くのサードパーティの開発者が、プロファイル API を使用してツールを構築しています。

ここ 10 年、CLR でも Visual Studio でも、この分野の関心事項の大半は、開発者のデスクトップ シナリオを実現することに向けられてきました。たとえば、デバッガーでソース ステッピングを行ってコードのエラーを検出する、パフォーマンス プロファイラーの管理下でアプリケーションを起動して遅いコード パスを特定する、エディット コンティニュを使用して編集、ビルド、デバッグのサイクルにかかる時間を短縮するといったシナリオです。これらのツールをユーザーのコンピューターにインストールしたり、サーバーに配置したりすると (以下、この両方をまとめて「運用」と呼びます)、アプリケーションのバグの検出に非常に役立ちます。また、これらのツールを土台にして世界規模の運用向け診断ツールを構築しているサードパーティのベンダーも多数あります。

ただし、ユーザーやベンダーからは、アプリケーションのライフサイクル全体でのバグ検出をさらに容易にすることがいかに重要であるかを強調するご意見が絶えず寄せられています。そもそも、一般にソフトウェアのバグは、検出時点がアプリケーションのライフサイクルの後半になるほど修正コストが高くなると考えられています。

CLR 4 (Microsoft .NET Framework 4 に基づくランタイム) は、マイクロソフトがこうしたご意見に耳を傾け、診断 API がサポートするシナリオを運用向けに大きく広げることに力を注いだ最初のリリースです。

今回のコラムでは、現時点では非常に厄介であるとマイクロソフトが認識しているいくつかのシナリオ、こうしたシナリオを解決するためにマイクロソフトが採用している手法、およびその結果として実現されるツールを紹介します。具体的には、アプリケーションがクラッシュしたシナリオやハングしたシナリオに対してダンプのデバッグをサポートするようにデバッグ API を進化させた方法や、マルチスレッドの問題が原因でハングが発生した状況をデバッグ API で検出しやすくした方法について説明します。

また、既に実行中のアプリケーションにプロファイル ツールをアタッチする機能を追加すると、上記と同じシナリオのトラブルシューティングがさらに簡単になり、メモリ使用量が多すぎるために発生した問題の診断にかかる時間を大幅に短縮できることについて説明します。

最後に、レジストリへの依存関係を解消したことで、プロファイル ツールをどれほど配置しやすくなったかについて概説します。このコラム全体では、主に CLR 4 の機能によって実現する新しいツールの種類に注目しますが、必要に応じて、Visual Studio でこの機能を活用する方法を理解するのに役立つその他のリソースも紹介します。

ダンプのデバッグ

Visual Studio 2010 に付属する人気機能の 1 つが、マネージ ダンプのデバッグです。プロセス ダンプ (通常は単に "ダンプ") は、ネイティブ コードとマネージ コードの両方を対象に、運用デバッグのシナリオで使用するのが一般的です。ダンプとは、基本的に特定の時点におけるプロセスの状態を表すスナップショットです。具体的に言えば、これはプロセスの仮想メモリ (またはそのサブセット) の内容がファイルにダンプされたものです。

Visual Studio 2010 よりも前のバージョンでは、ダンプ ファイルのマネージ コードをデバッグするには、(おそらく製品開発時にコードを作成してデバッグするのに使用した) Visual Studio などの使い慣れた ツールではなく、特殊な Windows デバッガーの拡張 (sos.dll) を使用して分析する必要がありました。Visual Studio でダンプを使用して問題を診断する際に使用できる高度な機能に関して目指したのは、停止状態でのライブ デバッグ機能です。つまり、コードのデバッグ中、ブレーク ポイントで停止したときに使用できる機能です。

ダンプを収集する最も一般的な状況は、アプリケーションでハンドルされない例外、つまりクラッシュが発生した場合です。通常は、ダンプを使用してエラーが発生しているスレッドのコール スタックを最初に調べることで、クラッシュの発生原因を特定します。ダンプを使用するその他のシナリオには、アプリケーションのハングやメモリ使用量の問題があります。

たとえば、Web サイトで要求の処理が停止した場合、デバッガーをアタッチし、ダンプを収集して、アプリケーションを再起動します。ダンプをオフラインで分析することで、たとえば、要求を処理するすべてのスレッドでデータベースへの接続を待機していることが判明したり、コードにデッドロックが見つかったりします。メモリ使用量の問題は、エンド ユーザーにはさまざまな現象として現れます。たとえば、ガベージ コレクションがあまりにも頻繁に行われるためアプリケーションの処理速度が低下したり、アプリケーションの仮想メモリが不足したためにサービスが中断され、再起動が必要になったりします。

CLR 2 の時点では、実行中のプロセスのデバッグだけがデバッグ API のサポート対象だったので、ここまで紹介してきたシナリオをツールの処理対象にするのは困難でした。基本的に、デバッグ API は、ダンプをデバッグするシナリオを前提として設計されていませんでした。この点をよく表しているのが、ターゲット プロセスで実行中のヘルパー スレッドを使用して、API でデバッガーの要求を処理していることです。

たとえば、CLR 2 では、マネージ デバッガーでスレッドのスタックを走査する必要がある場合、マネージ デバッガーからデバッグ対象のプロセスに含まれるヘルパー スレッドに要求が送信されます。そのプロセスの CLR によって要求が処理され、結果がデバッガーに返されます。ダンプはファイルにすぎないため、要求を処理するヘルパー スレッドはありません。

ダンプ ファイルのマネージ コードをデバッグするソリューションを提供するには、マネージ コードの状態を調べる際にターゲット プロセスでコードを実行する必要がない API を構築する必要がありました。しかし、デバッガー作成ツール (主に Visual Studio) ではライブ デバッグを提供するために既に CLR のデバッグ API にかなりの投資を行っていたため、2 つの異なる API を強制的に使用させるつもりはありませんでした。

CLR 4 でマイクロソフトがたどり着いた結論は、多数のデバッガー API (主にコードやデータの調査に必要な API) を再実装して、ヘルパー スレッドが使用されないようにすることでした。この結果、既存の API で、ダンプ ファイルと実行中のプロセスのいずれがデバッグ対象なのかを気にする必要がなくなりました。また、デバッガー作成ツールでは、同じ API でライブ デバッグのシナリオもダンプのデバッグのシナリオも処理対象にできます。実行制御を目的としてライブ デバッグを行う (ブレークポイントを設定してコードをステップ スルーする) 場合、これまでと同様にデバッグ API ではヘルパー スレッドが使用されます。長期的には、マイクロソフトではこれらのシナリオ間の依存関係を解消することも目指しています。Rick Byers (デバッグ サービス API の元開発者) は、この活動を詳しく記したブログ記事 (blogs.msdn.com/rmbyers/archive/2008/10/27/icordebug-re-architecture-in-clr-4-0.aspx、英語) を執筆しています。

CLR 4 では、ICorDebug を使用してダンプ ファイルのマネージ コードやデータを調査できるようになりました。たとえば、スタック ウォーク、ローカル変数の列挙、例外の種類の取得などが可能です。クラッシュやハングであれば、多くの場合、問題の原因を特定するのに十分なコンテキストをスレッド スタックや補助データから取得できます。

メモリ診断などのシナリオも重要なことは承知していますが、CLR 4 のスケジュールには、このシナリオに必要な方法でマネージ ヒープをデバッガーで調査できるようにする、新しい API を構築できるほどの時間がありませんでした。サポート対象の運用向け診断シナリオを引き続き拡大しているので、将来はこのようなシナリオがサポート対象に追加されるでしょう。今回のコラムの後半では、このシナリオに対処するうえで役に立つその他の機能についても紹介します。

また、はっきりお伝えしておきますが、この機能では、デバッグ対象として 32 ビットと 64 ビットの両方をサポートし、マネージのみのデバッグと混合モード (ネイティブとマネージ) のデバッグの両方をサポートしています。Visual Studio 2010 には、マネージ コードが含まれたダンプ向けの混合モードが用意されています。

モニター ロックの調査

マルチスレッド プログラミングは難しくなることがあります。明示的にマルチスレッド コードを記述しているのか、マルチスレッド コードが自動的に作成されるフレームワークやライブラリを利用しているのかによって、非同期の並列コードで発生する問題を診断するのが非常に難しくなることがあります。1 つのスレッドで実行される作業の論理単位が 1 つだけなら、原因を把握するのは実に簡単で、たいていはスレッドのコール スタックを確認するだけで十分です。しかし、この作業が複数のスレッドに分割されていると、処理の流れの追跡はきわめて難しくなります。作業が完了しないのはなぜでしょう。何かが作業の一部をブロックしているのでしょうか。

マルチコアの普及が進んでいることから、開発者は、パフォーマンス向上の手段として、チップの速度向上を利用するだけでなく、並列プログラミングにますます関心を払うようになっています。マイクロソフトの開発者も例外ではありません。ここ数年、マイクロソフトは、開発者が並列プログラミングで容易に成果をあげられるようにすることに多大な労力を注いできました。診断の観点から、マイクロソフトではいくつかの単純ながらも役に立つ API を追加しました。これらの API を使用して、開発者がマルチスレッド コードの複雑さに適切に対処するのに役立つツールを実現できます。

CLR のデバッガー API には、モニター ロック向けの調査 API を追加しました。簡単に言えば、モニター機能は、複数スレッド間の共有リソース (.NET コードの任意のオブジェクト) へのアクセスを同期する手段をプログラムに提供します。このため、一方のスレッドでリソースがロックされている間は、もう一方のスレッドはそのリソースを待機します。ロックを保持しているスレッドがリソースを解放すると、待機中の最初のスレッドでリソースを取得できるようになります。

.NET Framework では、モニターは System.Threading.Monitor 名前空間を通じて直接公開されますが、通常は lock キーワード (C#) や SyncLock キーワード (Visual Basic) を通じて公開されます。また、同期メソッドの実装である Task Parallel Library (TPL: タスク並列ライブラリ) やその他の非同期プログラミング モデルでも、モニターが使用されます。新しいデバッガー API を使用すると、特定のスレッドをブロックしているオブジェクト (存在する場合) や、特定のオブジェクトのロックを保持しているスレッド (存在する場合) を適切に把握できます。これらの API を活用したデバッガーでは、開発者がデッドロックを特定したり、あるリソースに対して競合している複数のスレッド (ロック コンボイ) がアプリケーションのパフォーマンスに悪影響を及ぼす条件を理解したりできるようになります。

この機能によって実現されるツールの種類の例については、Visual Studio 2010 の並列デバッグ機能を参照してください。2009 年 9 月号の MSDN Magazine (msdn.microsoft.com/magazine/ee410778) で、Daniel Moth と Stephen Toub がこのようなツールに関するすばらしい概要を執筆しています。

ダンプのデバッグ機能に関する最も興味深い性質の 1 つは、デバッグ対象を抽象的に表すビューを構築すると、モニター ロックの調査機能などの新しい調査機能が追加されることです。これにより、ライブ デバッグのシナリオにもダンプのデバッグのシナリオにも付加価値がもたらされます。個人的に、この機能は開発者が最初にアプリケーションを開発する際に非常に役立つと考えていますが、モニター ロックの調査によって CLR 4 の運用向け診断機能が大幅に補強されるのは、ダンプのデバッグがサポートされるためです。

マイクロソフトのサポート エンジニアである Tess Ferrandez は、Channel 9 ビデオ (channel9.msdn.com/posts/Glucose/Hanselminutes-on-9-Debugging-Crash-Dumps-with-Tess-Ferrandez-and-VS2010/、英語) で、顧客のアプリケーションのトラブルシューティング時によく遭遇したロック コンボイのシナリオのシミュレーションを行っています。その後、Visual Studio 2010 を使用して問題を診断する方法についても説明しています。このビデオは、このような新機能によって実現できるシナリオの種類を示す、すばらしい例です。

ダンプ以外の機能

マイクロソフトでは、ここまで説明してきた機能で実現されるツールを使用すれば、開発者が運用環境の問題の解決にかかる時間を短縮できると確信していますが、運用環境の問題を診断する手段がダンプのデバッグだけとは考えていません (そのようなことを望んでもいません)。

メモリ使用量が多すぎる問題を診断する場合、通常、まず型ごとに分類されて数と合計サイズが表示されたオブジェクト インスタンスの一覧を調べてから、オブジェクト参照の依存関係を把握します。運用コンピューター、開発コンピューター、運用スタッフ、サポート エンジニア、および開発者の間でこのような情報が含まれたダンプ ファイルをやり取りすると、非効率的で時間がかかりがちです。また、アプリケーションのサイズが大きくなると (特に 64 ビット アプリケーションでは顕著です)、ダンプ ファイルのサイズも大きくなり、ファイルの移動と処理にかかる時間が長くなります。このような高度なシナリオを念頭に、マイクロソフトはこれから説明するプロファイル機能を開発しました。

プロファイル ツール

CLR のプロファイル API に基づいて構築されるツールには、さまざまな種類があります。プロファイル API に関するシナリオでは、一般に、パフォーマンス、メモリ、およびインストルメンテーションという 3 つの機能カテゴリが重視されます。

パフォーマンス プロファイラー (一部のバージョンの Visual Studio に付属しているプロファイラーなど) は、コードの中で処理に時間がかかっている箇所を通知することに重点を置いています。メモリ プロファイラーは、アプリケーションのメモリ使用量を詳細に示すことに重点を置いています。インストルメンテーション プロファイラーは、他の 2 つとまったく同じことを行います。

最後のインストルメンテーション プロファイラーについて少し説明しておきましょう。プロファイル API に備わっている機能の 1 つは、実行時に中間言語 (IL) をマネージ コードに挿入する機能です。これを、コード インストルメンテーションと呼びます。
ユーザーはこの機能を使用して、コード カバレッジ、フォールト挿入、.NET Framework ベースのアプリケーションにおけるエンタープライズ クラスの運用時の監視など、多彩なシナリオを実現するツールを構築します。

プロファイル API がデバッグ API に勝るメリットの 1 つは、プロファイル API が非常に軽量に設計されていることです。どちらの API もイベント駆動型の API ですが (たとえば、どちらの API にも、アセンブリの読み込み、スレッドの作成、例外のスローなどに関するイベントがあります)、プロファイル API を使用すると、注目する必要のあるイベントだけを登録できます。また、プロファイル DLL がターゲット プロセス内に読み込まれるため、実行時の状態にすばやくアクセスできます。

これに対し、デバッガー API では、すべてのイベントがアウトプロセス デバッガーに報告され (デバッガーがアタッチされている場合)、イベントが発生するたびにランタイムの処理が中断されます。このような点は、運用環境を対象とするオンライン診断ツールを構築するのにプロファイル API が適している理由のごく一部にすぎません。

プロファイラーのアタッチとデタッチ

複数のベンダーが、IL インストルメンテーションを利用して、運用アプリケーションを監視する常時稼働のツールを構築していますが、プロファイル API のパフォーマンスやメモリ監視機能を利用した事後対応型の診断をサポートするツールはそれほど多くありません。このようなツールが構築されてこなかった主な原因は、既に実行中のプロセスに CLR のプロファイル API ベースのツールをアタッチできないことでした。

CLR 4 よりも前のバージョンでは、プロファイラーが登録されているかどうかが起動時に CLR で確認されます。登録済みのプロファイラーが検出されると、そのプロファイラーが読み込まれ、必要に応じてコールバックが提供されます。DLL がアンロードされることはありません。この動作は、ツールのジョブによってアプリケーションの動作が完全に把握される場合はおおむね役に立ちますが、アプリケーションの起動時に存在が判明していなかった問題に対しては機能しません。

おそらく、この性質の最も厄介な例は、メモリ使用量を診断するシナリオでしょう。現在のこのシナリオでは、多くの場合の診断の手順として、まず複数のダンプを収集し、次に割り当てられた型どうしの違いを調べ、続いて使用量が増加するようす示すタイムラインを作成して、最後にアプリケーション コード内で疑わしい型が参照されている場所を特定します。不適切に実装されたキャッシュ方式が原因の場合もあれば、ある型のイベント ハンドラーに、その他の点ではスコープ外となる別の型への参照が保持されている場合もあります。ユーザーやサポート エンジニアは、この種の問題を診断するのに多くの時間を費やしています。

まず、既に簡単に述べたとおり、大規模なプロセスのダンプはそれだけでもサイズが大きいため、診断のためにダンプを専門家に渡すと解決まで長い時間がかかることがあります。さらに、専門家の関与が必要な問題もあります。これは主に、Windows デバッガーの拡張機能だけを介してデータが公開されるためです。ツールでデータを使用して、データに基づいて直感的に表示したり、分析に役立つ可能性がある他のツールとデータを統合したりすることができるパブリック API はありません。

このシナリオ (およびその他いくつかのシナリオ) が扱いやすくなるよう、マイクロソフトでは、実行中のプロセスにプロファイラーをアタッチして既存のプロファイル API のサブセットをプロファイラーから使用できる新しい API を追加しました。プロセスにアタッチすると使用可能になる API により、サンプリング (blogs.msdn.com/profiler/archive/2009/12/07/vs2010-attaching-the-profiler-to-a-managed-application.aspx の「VS 2010: Attaching the Profiler to a Managed Application」(VS2010: マネージ アプリケーションにプロファイラーをアタッチする、英語) 参照) やメモリ診断 (スタック ウォーク、シンボリック名への関数アドレスのマッピング、ほとんどのガベージ コレクター (GC) のコールバック、およびオブジェクト検査) を実行できます。

上記のシナリオでは、ツールからこの機能を利用することで、応答時間が長いアプリケーションやメモリ使用量が多すぎるアプリケーションにユーザーがツールをアタッチし、現在実行中の処理を把握して、マネージ ヒープでアクティブな型や、型がアクティブになっている原因を特定することができます。情報を収集したら、ツールをデタッチできます。ツールをデタッチすると、CLR によってプロファイラーの DLL がアンロードされます。他の作業は以前と同様ですが (コード内でこれらの型が参照または作成されている場所を特定します)、マイクロソフトでは、このような性質のツールを使用すると上記の問題の平均解決時間が大幅に短縮されると予想しています。CLR のプロファイル API の開発者である Dave Broman は、この機能やその他のプロファイル機能の詳細について、自身のブログ (blogs.msdn.com/davbr、英語) で説明しています。

ただし、今回のコラムでは、はっきり 2 つの制限事項をお伝えしておきましょう。1 つ目の制限事項は、アタッチ時のメモリ診断が、非同時実行 (ブロック) GC モードに限定されていることです。つまり、GC の実行中に GC によってすべてのマネージ コードの実行が中断されるモードに限定されています。同時実行 GC が既定のモードですが、ASP.NET ではサーバー モード (非同時実行モード) の GC が使用されます。この環境こそ、上記の問題のほとんどが発生する状況です。マイクロソフトでは、アタッチされるプロファイル診断のクライアント アプリケーションにおける実用性を評価しており、今後のリリースでこの機能を提供する予定です。CLR 4 では、より頻繁に発生する問題を優先しただけです。

2 つ目の制限事項は、アタッチ後にプロファイル API を使用して IL をインストルメント化できないことです。この機能は、実行時の条件に応じてインストルメンテーションを動的に変更できる必要がある、エンタープライズ監視ツールのベンダーにとって特に重要です。マイクロソフトでは、通常、この機能を JIT の再実行と呼んでいます。現在、メソッドの IL 本体を変更できるタイミングは、メソッドが最初に JIT コンパイルされるときだけです。マイクロソフトでは、JIT の再実行を提供することが意義深い重要な作業になると予想しており、今後のリリースで提供することを検討すると同時に、ユーザーにとっての価値と技術的な影響の両方を積極的に調査しています。

レジストリを操作しないでプロファイラーを有効にする

プロファイラーのアタッチと、アタッチ後に特定のプロファイル API を有効にする補助機能は、CLR 4 における CLR のプロファイル API で実現した最大の成果でした。うれしいことに、運用クラスのツールを構築しているユーザーに驚くほど効果的な、(エンジニアリングのコスト面で) 非常に小さな別の機能も生み出されました。

CLR 4 より前、ツールのプロファイラー DLL をマネージ アプリケーションに読み込むには、2 つの重要な構成情報を作成する必要がありました。1 つの構成情報は 2 つの環境変数で、CLR の起動時に、プロファイルを有効にすることと読み込むプロファイラーの実装 (プロファイラーの CLSID または ProgID) を CLR に指示するものです。プロファイラーの DLL がインプロセス COM サーバーとして実装されていた場合 (CLR はクライアント)、もう 1 つの構成情報は、レジストリに格納されている対応する COM 登録情報でした。基本的に、この情報は DLL が配置されているディスク上の場所を COM 経由でランタイムに通知します。

CLR 4 の計画段階でベンダーが運用クラスのツールを構築しやすくする方法を把握しようとしていたとき、CLR チームでは、マイクロソフトの一部のサポート エンジニアやツール ベンダーと興味深い会話を交わしました。そこで寄せられた意見は、運用環境でアプリケーション エラーが発生すると、多くの場合、ユーザーはアプリケーションへの影響よりも (アプリケーションでは既にエラーが発生しているので、ユーザーは診断情報を調べるだけで次の作業に移ります)、コンピューターの状態への影響を気にすることでした。多くのユーザーは労力を惜しまずコンピューターの状態を記録して監視しているため、コンピューターの構成を変更するとリスクが高まるおそれがあります。

したがって、ソリューションの CLR 部分に関しては、xcopy で配置可能な診断ツールで、状態が変更されるリスクを最小限に抑えると同時に、コンピューターでツールを起動するのにかかる時間を短縮できるようにすることを望んでいました。

CLR 4 では、レジストリに COM プロファイラーの DLL を登録する必要がなくなりました。アプリケーションをプロファイラーの管理下で起動する場合は CLR 4 でも環境変数が必要ですが、上述のアタッチのシナリオでは構成する必要がありません。ツールで新しいアタッチ API を呼び出して DLL のパスを渡すだけで、CLR にプロファイラーが読み込まれます。一部のシナリオでは依然として環境変数のソリューションが難しいとユーザーが感じていることから、今後はプロファイラーの構成をさらに簡略化する方法を調査します。

ここで紹介した 2 つのプロファイル機能を組み合わせると、オーバーヘッドの少ない一連のツールのクラスを有効にできます。予期しないエラーが発生した場合、ユーザーはすばやく運用コンピューターにこのクラスを配置して、パフォーマンスやメモリの診断情報を収集し、問題の原因特定に役立てることができます。その後、ユーザーはすぐにコンピューターを元の状態に戻すことができます。

まとめ

CLR の診断機能の開発は、楽しくも苦しい作業です。ある問題にユーザーが非常に手を焼いている例が多数あっても、開発を容易にしてユーザーを満足させるためにマイクロソフトで提供できる優れた機能は、必ず存在します。このようなユーザーが手を焼いている問題の種類に変化がない場合 (つまり、これらの問題が減っていない場合)、開発が成功したとは言えません。

私の考えでは、マイクロソフトが CLR 4 で構築した機能は、ユーザーにとって重要な問題に対処する価値をすぐに提供するだけでなく、今後も継続的に開発できる基盤も備えています。引き続き、ユーザーが新機能の構築に作業時間を集中し、既存機能のデバッグにかかる時間を削減できるよう、アプリケーションのライフサイクルの全段階で有意義な診断情報の公開をいっそう簡単にする API やサービスに力を注いでいく予定です。

次期リリースで検討中の診断機能に関するフィードバックを提供することに興味をお持ちの方は、アンケート (surveymonkey.com/s/developer-productivity-survey、英語) にご協力ください。CLR 4 の公開が間近に迫っている今、CLR チームではほとんどの時間を今後のリリースの計画に費やしており、作業の優先順位を決定するうえでアンケートのデータが役に立ちます。ほんの数分お時間がありましたら、ご意見をお待ちしております。

Jon Langdon は、CLR チームのプログラム マネージャーとして診断を担当しています。CLR チームに参加する前は、Microsoft Services のコンサルタントとして、顧客が大規模なエンタープライズ アプリケーションの問題を診断して解決するのを支援していました。

この記事のレビューに協力してくれた技術スタッフの Rick Byers に心より感謝いたします。