適用対象: .NET Core 2.1、.NET Core 3.1、.NET 5
この記事では、パフォーマンスの問題を分析するのに役立ちます。また、createdump と ProcDump を使用して Linux で .NET Core メモリ ダンプ ファイルを手動でキャプチャする方法について説明します。
前提条件
これらのトラブルシューティング ラボに従う最小要件は次のとおりです。
- 低 CPU および高 CPU パフォーマンスの問題とクラッシュの問題を示す ASP.NET Core アプリケーション。
- コア ダンプが開かれたときに SOS 拡張機能を読み込むようインストールおよび構成された lldb デバッガー。
このシリーズの前の部分に従っている場合は、次のセットアップを準備しておく必要があります。
- Nginx は、次の 2 つの Web サイトをホストするように構成されています。
- 1 つ目は、 myfirstwebsite ホスト ヘッダー (
http://myfirstwebsite) を使用して要求をリッスンし、ポート 5000 でリッスンするデモ ASP.NET Core アプリケーションに要求をルーティングします。 - 2 つ目は、 buggyamb ホスト ヘッダー (
http://buggyamb) を使用して要求をリッスンし、ポート 5001 でリッスンする 2 番目の ASP.NET Core サンプル バグのあるアプリケーションに要求をルーティングします。
- 1 つ目は、 myfirstwebsite ホスト ヘッダー (
- ASP.NET コア アプリケーションは、サーバーが再起動されたとき、またはアプリケーションが応答を停止したときに自動的に再起動するサービスとして実行されている必要があります。
- Linux ローカル ファイアウォールが有効になっており、SSH トラフィックと HTTP トラフィックを許可するように構成されています。
Note
セットアップの準備ができていない場合は、「Part 2 Core アプリ ASP.NET 作成して実行する」に進みます。
このラボの目標
ここまで、このトラブルシューティング シリーズでは、クラッシュの問題を分析しました。 このラボでは、パフォーマンスの問題を分析する機会が与えられ、createdump と ProcDump を使用してメモリ ダンプ ファイルを手動でキャプチャする方法について説明します。
問題を再現します。
前のパートでは、 Slow リンクを選択して、最初の "低速" シナリオをテストしました。 これを行うと、ページは正しく読み込まれますが、予想よりもはるかに遅くなります。 このパートでは、 Load Generator 機能を使用して、このパフォーマンスの問題をトラブルシューティングします。 これは、問題のあるリソースに最大 6 つの同時要求を送信する "試験的" な機能です。 jQuery と Ajax 呼び出しを使用して要求を発行するため、6 個に制限されています。 Web ブラウザーでは、ほとんどの Ajax 要求に対する制限が、特定の URL に対する 6 つの同時要求に設定されます。 Load Generator を使用してさまざまなシナリオを再現する方法については、「Experimental "Load Generator"」を参照してください。
問題を再現するには、 Problem Pagesを開き、 Load Generator を選択し、 Slow シナリオで 6 つの要求を送信します。
次の一覧は、最終的にブラウザーで表示する必要がある内容を示しています。 表示される応答時間が長くなっています。 予想される応答時間が 1 秒未満です。 これは、アプリケーションのランディング ページから Expected Results リンクを選択したときに表示されます。
これは、トラブルシューティングを行う問題です。
症状を監視する
適切なトラブルシューティング セッションは、まず問題を定義し、症状を理解することから始めます。 htopを使用して、負荷を生成して問題を再現しようとしたときに、ASP.NET Core アプリケーションをホストするプロセスのプロセス メモリと CPU 使用率を監視します。 htopを覚えていない場合は、前のシリーズ パーツを確認してください。
問題を再現する前に、まず、アプリケーションの実行方法のベースラインを設定します。 Load Generator 機能を使用して、Expected Results を選択するか、複数の要求を Expected Results シナリオに送信します。 次に、問題が発生していない場合の CPU とメモリの使用状況を確認します。 htopを使用して、CPU とメモリの使用量を確認します。
htop実行し、フィルター処理して、バグのあるアプリケーションが実行されているユーザーに属するプロセスのみを表示します。 この場合、ターゲット ASP.NET Core アプリケーションのユーザーは www-data です。 U キーを押して、その www-data ユーザーを一覧から選択します。 また、 Shift + H を押してスレッドを非表示にします。 ご覧のように、www-data のコンテキストでは 4 つのプロセスが実行されており、そのうちの 2 つのプロセスが Nginx プロセスです。 その他のアプリケーションは、環境を設定するときに作成したバグのあるアプリケーションとデモ アプリケーション用です。
パフォーマンスの問題をまだ再現していないため、現在、すべての CPU とメモリ使用量の統計情報が低くなっていることに注意してください。
次に、クライアント ブラウザーに戻り、Load Generator を使用して、Slow シナリオに 6 つの要求を送信します。 その後、Linux デバイスにすばやく戻り、 htopでプロセス リソースの消費量を確認します。 バグのあるアプリケーションの CPU 使用率が大幅に増加し、メモリ使用量が上下に変動することがわかります。
Note
この出力は 2 つの論理 CPU を搭載した仮想マシンから取得されるため、 htop は 100% を超える CPU 使用率を示します。
すべての要求が最終的に処理されると、CPU とメモリの使用量が減少します。 CPU 使用率とメモリ使用量の傾向の両方で、要求の処理中にアプリケーションで GC (ガベージ コレクター) の使用率が高い可能性があると思われるはずです。
コア ダンプ ファイルを収集する
パフォーマンスの問題をトラブルシューティングすると、連続するメモリ ダンプ ファイルがキャプチャされ、分析されます。 複数のダンプ ファイルのキャプチャの背後にある考え方は簡単です。プロセス ダンプはプロセス メモリのスナップショットです。 過去の情報は含んでいません。 パフォーマンスの問題をトラブルシューティングするには、スレッドとヒープなどを比較できるように、複数の手動メモリ ダンプ ファイルまたはコア ダンプ ファイルをキャプチャする必要があります。
必要に応じて手動メモリ ダンプ ファイルをキャプチャするには、次の推奨オプションを使用します。
- Createdump
- Procdump
- Dotnet-dump
Createdump
Createdump は、.NET Core ランタイムと共に含まれます。 これはランタイム ディレクトリにあります。 ランタイム ディレクトリ パスは、 dotnet --list-runtimes コマンドを使用して見つけることができます。
バグのあるアプリケーションは .NET Core 3.1 アプリケーションであるため、createdump の完全なパスは /usr/share/dotnet/shared/Microsoft.NETCore.App/3.1.10/createdump。
このコマンドの最も簡単な形式は createdump <PID>です。 これにより、ターゲット プロセスのコア ダンプが 1 つ書き込まれます。 -f スイッチ (createdump <PID> -f <filepath>) を追加して、ダンプ ファイルを作成する場所をツールに指定できます。 この演習では、 ~/dumps/ ディレクトリにダンプ ファイルを作成します。
10 秒間隔で、2 つの連続するメモリ ダンプ ファイルを取り込みます。 "応答が遅い要求" の問題を再現する間、ダンプ ファイルをキャプチャする必要があります。 開始するには、まずプロセスの PID を見つける必要があります。 htop または systemctl status buggyamb.service コマンドを使用します。 次の一覧では、プロセス PID は 11724 です。
ダンプ ファイルを作成するには、次の手順に従います。
- 最初のファイル (
sudo /usr/share/dotnet/shared/Microsoft.NETCore.App/3.1.10/createdump 11724 -f ~/dumps/coredump.manual.1.%d) を作成します。 - 最初のダンプ ファイルが書き込まれた後、10 秒待ちます。
- 2 番目のファイルを作成します。
sudo /usr/share/dotnet/shared/Microsoft.NETCore.App/3.1.10/createdump 11724 -f ~/dumps/coredump.manual.2.%d
最後に、2 つのメモリ ダンプ ファイルが必要です。 各ダンプ ファイルのサイズに注意してください。
lldb でダンプ ファイルを分析する
lldb でダンプ ファイルを開く方法は既にわかっているはずです。 2 つの異なる SSH セッションで lldb で両方のファイルを開きます。
あなたの目的は、パフォーマンスの問題を引き起こしている可能性のあるものについての理論を開発することです。 問題が発生したときに CPU とメモリの使用率が高いことが既にわかっています。 マネージド メモリを確認するには、 dumpheap -stat コマンドを使用します。 開始する前に、最初のダンプ ファイルを簡単に確認してください。
clrthreads コマンドを実行して、マネージド スレッドの一覧を取得します。
Note
1 つのスレッドで GC モードが Cooperative に設定され、他のスレッドは Preemptive に設定されます。
スレッドの GC モードが Preemptive に設定されている場合、これは GC がいつでもこのスレッドを中断できることを意味します。 これに対し、協調モードとは、スレッドがプリエンプティブ モードに切り替わるまで GC が待機してから中断する必要があることを意味します。 スレッドがマネージド コードを実行している場合は、協調モードになります。
まず、協調モードでスレッドを調べることから始めます。 協調スレッドのデバッガーのスレッド ID は、例の一覧で19 です。 演習を繰り返すと、ID が異なります。 thread select 19を実行してスレッドに切り替え、clrstackを実行してマネージド呼び出し履歴を一覧表示します。 バグのあるアプリケーションの "低速" ページが文字列 concat 操作を実行しています。
これは、文字列 concat 操作がコストがかかることを知っている必要があるため、疑わしいものにする必要があります。 これは、.NET の文字列オブジェクトが不変であるためです。つまり、割り当てられた後に値を変更することはできません。 次の擬似コード スニペットを考えてみましょう。
string myText = "Debugging";
myText = myText + " .NET Core";
myText = myText + " is awesome";
このコードは、メモリ内に複数の文字列 ( Debugging、 Debugging .NET Core、 Debugging .NET Core is awesome) を作成します。 1 つの最後の文字列を生成 (連結) するには、3 つの異なる文字列オブジェクトを作成する必要があります。 これが十分に頻繁に発生する場合は、GC をトリガーできるようにメモリ不足が発生する可能性があります。
この理論は有望に聞こえます。 ただし、正しいことを確認する必要があります。 マネージド ヒープを確認する前に、既にスレッド コンテキスト上に配置されている間に、このスレッドから参照されているオブジェクトを調べて、文字列と string[] オブジェクトの値を確認します。 dsoを実行し、文字列配列と文字列配列に焦点を当てます。
文字列配列を調べてみてください。 オブジェクトのアドレスを使用して dumpobj を実行します。 ただし、これは問題のオブジェクトが配列であることを示しているだけであることに注意してください。 SOS には、配列を調査するための dumparray コマンドが用意されています。 dumparray 00007faf309528c8を実行して、配列内の項目の一覧を取得します。 (配列オブジェクトのアドレスは、調べているダンプ ファイルでは異なることがあります。
配列内に含まれる結果の文字列アドレスを使用して、 dumpobj コマンドをもう一度実行します。 アドレスの一部を選択し、調査します。
これらの文字列は、ページに表示される製品テーブル内の文字列に似ています。
文字列が大きい場合、lldb (または SOS) では文字列値が表示されない可能性があることに注意してください。 このような場合、オプションの 1 つは、lldb のネイティブ コマンドを使用してネイティブ メモリ アドレスを調べることです。 これは、WinDbg で d* コマンド ( dc など) を使用する場合と似ています。
次のコマンドは、指定されたメモリ位置にあるネイティブ メモリを読み取り、最初の 384 バイトを示しています。 この一覧では、文字列アドレスの 1 つを使用して示します。 実行中のコマンドは memory read -c 384 00007fb14d5da040。
スレッドのスタックによって参照される文字列の数は、文字列連結の問題がパフォーマンスの問題の原因となっているという理論を確認しているようです。
ただし、調査はまだ完了していません。 2 つのメモリ ダンプ ファイルがあります。 そのため、マネージド メモリ ヒープを比較し、ヒープが時間でどのように変化したかを確認します。
各ダンプ ファイルで dumpheap -stat コマンドを実行します。 最初のファイルを次に示します。 次の一覧には、105,401 個の文字列オブジェクトがあり、文字列オブジェクトの合計サイズは約 480 MB です。 また、メモリが断片化している可能性があり、断片化の理由は文字列配列オブジェクトと System.Data.DataRow オブジェクトに関連しているようです。
2 番目のダンプ ファイルで同じ dumpheap -stat コマンドを実行して続行します。 断片化の統計に変更が表示されるはずですが、この調査のコンテキスト内では重要ではありません。 重要な部分は、文字列オブジェクトの数と、これらのオブジェクトのサイズの大幅な増加です。
同時に、 System.Data.DataRow オブジェクトの数も増加します。
ラージ オブジェクト ヒープ (LOH) に関連する問題があると思われる場合があります。 したがって、LOH オブジェクトを調べることが必要になる場合があります。 この場合は、 dumpheap -stat -min 85000 コマンドを実行する必要があります。 次の一覧には、最初のメモリ ダンプの LOH の統計情報が含まれています。
2 番目のメモリ ダンプの LOH の統計情報を次に示します。
これは、ヒープの増加も明確に示しています。 これはすべて、 string オブジェクトに関連しているようです。
最後に、LOH から 1 つの "live" オブジェクトを選択してそのルートを見つけた場合はどうでしょうか。 この場合、"Live" は、オブジェクトがどこかにルート化されているため、GC プロセスによって削除されないようにアプリケーションによってアクティブに使用されていることを意味します。
この状況の処理は簡単です。 dumpheap -stat -min 85000 -live を実行します。 このコマンドは、どこかにルート化されたオブジェクトのみを表示します。 この例では、LOH に存在する string オブジェクトの適切なインスタンスのみが存在します。
string オブジェクトの MT アドレスを使用して、それらのライブ オブジェクトのアドレスの一覧を取得します。 dumpheap -mt 00007fb1602c0f90 -min 85000 -live を実行します。
次に、結果の一覧から 1 つのアドレスをランダムに選択します。 次のスクリーンショットでは、一覧の 3 番目のアドレスが表示されます。 選択したアドレスを調べるには、 dumpobjを実行します。 ただし、これは大きなオブジェクトであるため、デバッガーには値が表示されません。 そのため、代わりに、ネイティブ メモリ アドレスをもう一度調べると、ページ上の products テーブルの一覧で見つかるような、応答が遅い string オブジェクトであることがわかります。
一覧表示したオブジェクトのルートを調べます。 これを行うには、SOS gcroot コマンドを使用します。 このコマンドは、オブジェクトのアドレスをパラメーターとして最も単純な形式で受け取ります。 ご覧のように、この string は、"低速" ページが実行されているスレッドに根ざしています。 ソース ファイル名と行番号の情報も表示されます。
Note
ソース ファイル名と行番号の情報は、トラブルシューティングを行う場所とシンボルが正しく設定されているかどうかによって異なります。 最悪のシナリオでは、少なくともスレッド ID を回復できます。 次の一覧では、 b6c はマネージド スレッド ID です。 clrthreadsを実行すると、対応するスレッド ID が見つかります。
上のスクリーンショットに示すように、マネージド スレッド ID b6c のデバッガー スレッド ID は 23 です。 スレッド 23 に切り替え、マネージド呼び出し履歴を確認します。 前に説明したように、このスレッドは文字列 concat 操作も行う必要があります。
また、 bt コマンドを使用してネイティブ呼び出し履歴で調べると、GC がこのスレッドにメモリを割り当てられていることがわかります。
この証拠は、問題が、"低速" ページの処理中にトリガーされる、これまで以上に大きな文字列を作成する多数の文字列連結操作に関連していることを確認します。
このような問題の解決策は、このシリーズの範囲内ではありません。 ただし、このソリューションは、文字列 concat 操作ではなく、 StringBuilder クラス インスタンスを使用して簡単に実装できます。