.NETアプリケーションでの高いCPU使用率のハンティングと分析

公開: 2022-03-11

ソフトウェア開発は非常に複雑なプロセスになる可能性があります。 私たち開発者は、さまざまな変数を考慮する必要があります。 いくつかは私たちの管理下になく、いくつかは実際のコード実行の瞬間に私たちに知られておらず、いくつかは私たちによって直接制御されています。 そして、.NET開発者も例外ではありません。

この現実を考えると、制御された環境で作業する場合、通常、物事は計画どおりに進みます。 例としては、開発マシン、または完全にアクセスできる統合環境があります。 このような状況では、コードとソフトウェアに影響を与えているさまざまな変数を分析するためのツールを自由に使用できます。 このような場合、サーバーの高負荷や、同じことを同時に実行しようとする同時ユーザーに対処する必要もありません。

説明された安全な状況では、コードは正常に機能しますが、高負荷またはその他の外部要因の下での本番環境では、予期しない問題が発生する可能性があります。 本番環境でのソフトウェアパフォーマンスを分析するのは困難です。 ほとんどの場合、理論的なシナリオで潜在的な問題に対処する必要があります。問題が発生する可能性があることはわかっていますが、テストすることはできません。 そのため、使用している言語のベストプラクティスとドキュメントに基づいて開発を行い、よくある間違いを回避する必要があります。

前述のように、ソフトウェアが稼働すると、問題が発生したり、コードが計画外の方法で実行され始めたりする可能性があります。 デバッグしたり、何が起こっているのかを確実に知ることができずに問題に対処しなければならない状況に陥る可能性があります。 この場合、私たちは何ができますか?

CPU使用率が高いとは、プロセスがCPUの90%以上を長期間使用している場合であり、問​​題が発生しています。

プロセスがCPUの90%以上を長期間使用している場合、問題が発生します
つぶやき

この記事では、Windowsベースのサーバーでの.NET WebアプリケーションのCPU使用率が高い実際のシナリオ、問題の特定に関連するプロセス、さらに重要なことに、この問題が最初に発生した理由とその方法を分析します。それを解決します。

CPU使用率とメモリ消費量は広く議論されているトピックです。 通常、特定のプロセスが使用する必要のあるリソース(CPU、RAM、I / O)の適切な量と、その期間を確実に知ることは非常に困難です。 1つ確かなことは、プロセスがCPUの90%以上を長期間使用している場合、サーバーがこの状況で他の要求を処理できないという事実だけで問題が発生することです。

これは、プロセス自体に問題があることを意味しますか? 必ずしも。 プロセスがより多くの処理能力を必要とするか、大量のデータを処理している可能性があります。 まず、私たちにできることは、なぜこれが起こっているのかを特定することだけです。

すべてのオペレーティングシステムには、サーバーで何が起こっているかを監視するためのいくつかの異なるツールがあります。 Windowsサーバーには、特にタスクマネージャー、パフォーマンスモニターがあります。この場合、サーバーを監視するための優れたツールであるNewRelicServerを使用しました。

最初の症状と問題分析

アプリケーションをデプロイした後、最初の2週間の経過中に、サーバーにCPU使用率のピークがあり、サーバーが応答しなくなっていることがわかりました。 再び利用できるようにするために再起動する必要があり、このイベントはその時間枠の間に3回発生しました。 前述したように、サーバーモニターとしてNew Relic Serversを使用しましたが、サーバーがクラッシュしたときにw3wp.exeプロセスがCPUの94%を使用していたことがわかりました。

インターネットインフォメーションサービス(IIS)ワーカープロセスは、Webアプリケーションを実行するWindowsプロセス( w3wp.exe )であり、特定のアプリケーションプールのWebサーバーに送信される要求を処理します。 IISサーバーには、問題を引き起こす可能性のある複数のアプリケーションプール(およびいくつかの異なるw3wp.exeプロセス)が存在する可能性があります。 プロセスが持っていたユーザーに基づいて(これはNew Relicレポートに示されていました)、問題は.NET C#Webフォームのレガシーアプリケーションにあることがわかりました。

.NET FrameworkはWindowsデバッグツールと緊密に統合されているため、最初に試みたのは、イベントビューアとアプリケーションログファイルを調べて、何が起こっているかについての有用な情報を見つけることでした。 イベントビューアにログインしている例外があるかどうかにかかわらず、それらは分析するのに十分なデータを提供しませんでした。 そのため、さらに一歩進んでより多くのデータを収集することにしました。そのため、イベントが再び発生したときに準備が整いました。

データ収集

ユーザーモードのプロセスダンプを収集する最も簡単な方法は、Debug DiagnosticToolsv2.0または単にDebugDiagを使用することです。 DebugDiagには、データを収集し(DebugDiag Collection)、データを分析する(DebugDiag Analysis)ための一連のツールがあります。

それでは、デバッグ診断ツールを使用してデータを収集するためのルールの定義を始めましょう。

  1. DebugDiagコレクションを開き、[ Performance ]を選択します。

    デバッグ診断ツール

  2. [ Performance Counters ]を選択し、[ Next ]をクリックします。
  3. [ Add Perf Triggersクリックします。
  4. Processではなく) Processorオブジェクトを展開し、 % Processor Timeを選択します。 Windows Server 2008 R2を使用していて、64を超えるプロセッサを使用している場合は、 ProcessorオブジェクトではなくProcessor Informationオブジェクトを選択してください。
  5. インスタンスのリストで、 _Totalを選択します。
  6. [ Add ]をクリックしてから、[ OK ]をクリックします。
  7. 新しく追加されたトリガーを選択し、[ Edit Thresholds ]をクリックします。

    パフォーマンスカウンター

  8. ドロップダウンで[ Above ]を選択します。
  9. しきい値を80に変更します。
  10. 秒数として20を入力します。 必要に応じてこの値を調整できますが、誤ったトリガーを防ぐために、秒数を指定しないように注意してください。

    パフォーマンスモニタートリガーのプロパティ

  11. [ OKをクリックします。
  12. [ Next ]をクリックします。
  13. [ Add Dump Targetクリックします。
  14. ドロップダウンから[ Web Application Pool ]を選択します。
  15. アプリプールのリストからアプリケーションプールを選択します。
  16. [ OKをクリックします。
  17. [ Next ]をクリックします。
  18. もう一度[ Next ]をクリックします。
  19. 必要に応じてルールの名前を入力し、ダンプが保存される場所をメモします。 必要に応じて、この場所を変更できます。
  20. [ Next ]をクリックします。
  21. [ Activate the Rule Now ]を選択し、[ Finish ]をクリックします。

説明されたルールは、サイズがかなり小さいミニダンプファイルのセットを作成します。 最終的なダンプはフルメモリのダンプになり、そのダンプははるかに大きくなります。 これで、高CPUイベントが再び発生するのを待つだけで済みます。

選択したフォルダにダンプファイルができたら、収集したデータを分析するためにDebugDiag分析ツールを使用します。

  1. PerformanceAnalyzerを選択します。

    DebugDiag分析ツール

  2. ダンプファイルを追加します。

    DebugDiag分析トールダンプファイル

  3. 分析を開始します。

DebugDiagは、ダンプを解析して分析を提供するのに数分(または数分)かかります。 分析が完了すると、次のようなスレッドに関する概要と多くの情報が記載されたWebページが表示されます。

分析概要

要約からわかるように、「ダンプファイル間の高いCPU使用率が1つ以上のスレッドで検出されました」という警告があります。 推奨事項をクリックすると、アプリケーションのどこに問題があるのか​​がわかり始めます。 レポートの例は次のようになります。

平均CPU別の上位10スレッド

レポートからわかるように、CPU使用率に関するパターンがあります。 CPU使用率が高いすべてのスレッドは、同じクラスに関連しています。 コードにジャンプする前に、最初のコードを見てみましょう。

.NETコールスタック

これは、問題のある最初のスレッドの詳細です。 私たちにとって興味深い部分は次のとおりです。

.NETコールスタックの詳細

ここでは、問題のある操作をトリガーしたコードGameHub.OnDisconnected()を呼び出していますが、その呼び出しの前に2つのDictionary呼び出しがあり、何が起こっているかを知ることができます。 .NETコードを見て、そのメソッドが何をしているのかを見てみましょう。

 public override Task OnDisconnected() { try { var userId = GetUserId(); string connId; if (onlineSessions.TryGetValue(userId, out connId)) onlineSessions.Remove(userId); } catch (Exception) { // ignored } return base.OnDisconnected(); }

ここには明らかに問題があります。 レポートのコールスタックによると、問題はディクショナリにあり、このコードではディクショナリにアクセスしています。具体的には、問題の原因となっている行は次のとおりです。

 if (onlineSessions.TryGetValue(userId, out connId))

これは辞書の宣言です:

 static Dictionary<int, string> onlineSessions = new Dictionary<int, string>();

この.NETコードの問題は何ですか?

オブジェクト指向プログラミングの経験がある人なら誰でも、静的変数がこのクラスのすべてのインスタンスによって共有されることを知っています。 .NETの世界で静的が何を意味するのかを詳しく見ていきましょう。

.NET C#仕様によると:

static修飾子を使用して、特定のオブジェクトではなく型自体に属する静的メンバーを宣言します。

これは、静的クラスとメンバーに関して.NET C#言語仕様で述べられていることです。

すべてのクラスタイプの場合と同様に、静的クラスのタイプ情報は、クラスを参照するプログラムがロードされるときに、.NET Framework共通言語ランタイム(CLR)によってロードされます。 プログラムは、クラスがいつロードされるかを正確に指定できません。 ただし、プログラムでクラスが初めて参照される前に、ロードされ、フィールドが初期化され、静的コンストラクターが呼び出されることが保証されています。 静的コンストラクターは1回だけ呼び出され、静的クラスは、プログラムが存在するアプリケーションドメインの存続​​期間中メモリに残ります。

非静的クラスには、静的メソッド、フィールド、プロパティ、またはイベントを含めることができます。 静的メンバーは、クラスのインスタンスが作成されていない場合でも、クラスで呼び出すことができます。 静的メンバーには、インスタンス名ではなく、常にクラス名でアクセスします。 クラスのインスタンスの数に関係なく、静的メンバーのコピーは1つだけ存在します。 静的メソッドとプロパティは、それらを含むタイプの非静的フィールドとイベントにアクセスできません。また、メソッドパラメータで明示的に渡されない限り、オブジェクトのインスタンス変数にアクセスすることはできません。

これは、静的メンバーがオブジェクトではなく、タイプ自体に属していることを意味します。 また、CLRによってアプリケーションドメインに読み込まれるため、静的メンバーは、特定のスレッドではなく、アプリケーションをホストしているプロセスに属します。

Web環境がマルチスレッド環境であるという事実を考えると、すべての要求はw3wp.exeプロセスによって生成される新しいスレッドであるためです。 静的メンバーがプロセスの一部であるとすると、複数の異なるスレッドが静的(複数のスレッドで共有される)変数のデータにアクセスしようとするシナリオがあり、最終的にマルチスレッドの問題が発生する可能性があります。

スレッドセーフの下の辞書のドキュメントには、次のように記載されています。

Dictionary<TKey, TValue>は、コレクションが変更されていない限り、複数のリーダーを同時にサポートできます。 それでも、コレクションを介して列挙することは、本質的にスレッドセーフな手順ではありません。 列挙型が書き込みアクセスと競合するまれなケースでは、列挙型全体でコレクションをロックする必要があります。 読み取りと書き込みのために複数のスレッドがコレクションにアクセスできるようにするには、独自の同期を実装する必要があります。

このステートメントは、この問題が発生する理由を説明しています。 ダンプ情報に基づくと、問題は辞書のFindEntryメソッドにありました。

.NETコールスタックの詳細

ディクショナリのFindEntry実装を見ると、値を見つけるためにメソッドが内部構造(バケット)を反復処理していることがわかります。

したがって、次の.NETコードはコレクションを列挙していますが、これはスレッドセーフな操作ではありません。

 public override Task OnDisconnected() { try { var userId = GetUserId(); string connId; if (onlineSessions.TryGetValue(userId, out connId)) onlineSessions.Remove(userId); } catch (Exception) { // ignored } return base.OnDisconnected(); }

結論

ダンプで見たように、共有リソース(静的ディクショナリ)を同時に反復および変更しようとするスレッドがいくつかあり、最終的に反復が無限ループに入り、スレッドがCPUの90%以上を消費する原因になりました。

この問題にはいくつかの解決策があります。 最初に実装したのは、パフォーマンスを低下させる代わりに、辞書へのアクセスをロックして同期することでした。 その時、サーバーは毎日クラッシュしていたので、できるだけ早くこれを修正する必要がありました。 これが最適な解決策ではなかったとしても、問題は解決しました。

この問題を解決するための次のステップは、コードを分析し、これに対する最適な解決策を見つけることです。 コードをリファクタリングすることはオプションです。新しいConcurrentDictionaryクラスは、全体的なパフォーマンスを向上させるバケットレベルでのみロックするため、この問題を解決できます。 ただし、これは大きなステップであり、さらなる分析が必要になります。