Linux上でGoogle CloudのKubernetes上でASP.NET Core APIを実行しています。
これは高負荷のAPIであり、すべてのリクエストで、CPUを集中的に使用する長時間(1〜5秒)のライブラリを実行しています。
デプロイ後しばらくの間、APIは適切に機能しますが、10〜20分後には応答しなくなり、ヘルスチェックエンドポイント(ハードコードされた200 OK
を返すだけ)も機能しなくなり、タイムアウトします。 (これにより、Kubernetesはポッドを強制終了します)。
時々、ログに悪名高いHeartbeat took longer than "00:00:01"
エラーメッセージが表示されることもあります。
これらの現象をグーグルすると、「スレッドの枯渇」が指摘され、開始されたスレッドプールスレッドが多すぎる、またはスレッドの待機がブロックされて何かを待機しているため、ASP.NET Coreを取得できるスレッドがプールに残っていないリクエスト(したがって、ヘルスチェックエンドポイントのタイムアウトも発生します)。
この問題をトラブルシューティングする最良の方法は何ですか?私はThreadPool.GetMaxThreads
とThreadPool.GetAvailableThreads
によって返される数値の監視を開始しましたが、それらは一定のままでした(完了ポートは常に最大と使用可能の両方で1000
であり、ワーカーは常に32767
です)。
監視すべき他のプロパティはありますか?
ASP.NET Core Webアプリでスレッドが不足していますか?利用可能なすべてのポッドリソースが飽和状態になり、Kubernetesがポッド自体を強制終了するため、ウェブアプリが停止する可能性があります。
OpenShift 環境内のLinux RedHatで実行されているASP.NET Core Web APIで非常によく似たシナリオを経験しました。これは、Kubernetesのようなポッドコンセプトもサポートしています。1回の呼び出しが完了するまでに約1秒必要でした。 、大きなワークロードでは、最初に遅くなり、その後応答がなくなり、OpenShiftがポッドを強制終了させたため、私のWebアプリが停止しました。
ASP.NET Core Webアプリでスレッドが不足していない可能性があります。特に、ThreadPoolで利用可能な大量のワーカースレッドを考慮すると、代わりに、CPUニーズと組み合わされたアクティブスレッドの数は、実行中のポッド内で使用可能な実際のミリコアと比較して多すぎる可能性があります。実際、アクティブスレッドは、作成後、使用可能なCPUに対して多すぎて、そのほとんどが使用できません。結局、スケジューラによってキューに入れられ、実行を待機することになりますが、実際に実行されるのは束だけです。次に、スケジューラはその仕事を行い、CPUを使用するスレッドを頻繁に切り替えることにより、CPUがスレッド間で公平に共有されるようにします。スレッドが重いCPUバインド操作を必要とする場合、時間の経過とともにリソースが飽和し、Webアプリが応答しなくなります。
緩和策として、ポッド、特にミリコアに容量を追加したり、Kubernetesが必要に応じてデプロイできるポッドの数を増やしたりできます。しかし、私の特定のシナリオでは、このアプローチはあまり役に立ちませんでした。代わりに、1つのリクエストの実行を1秒から300ミリ秒に短縮することでAPI自体を改善し、Webアプリケーション全体のパフォーマンスを大幅に向上させ、実際に問題を解決しました。
たとえば、ライブラリが複数のリクエストで同じ計算を実行する場合、特に操作が主にCPUである場合、メモリのわずかなコストで速度を向上させるために、データ構造にキャッシュを導入することを検討できます(これは私にとってはうまくいきました)。バインドされており、そのようなリクエストがウェブアプリにある場合。 APIのワークロードと応答で意味がある場合は、 ASP.NET Coreのキャッシュ応答 を有効にすることも検討してください。キャッシュを使用すると、Webアプリが同じタスクを2回実行しないようにして、CPUを解放し、スレッドをキューに入れるリスクを減らします。
各リクエストをより速く処理することで、Webアプリが使用可能なCPUをいっぱいにするリスクを軽減し、キューに入れられて実行を待機するスレッドが多すぎるリスクを軽減します。
一般的に言って、長時間実行する作業はWebアプリケーションにとっては致命的です。正常なWebアプリの1秒未満の応答時間が必要です。これは、実行する必要がある作業が同期的またはCPUバウンドである場合に特に当てはまります。 Asyncは少なくともプロセス中にスレッドを解放できますが、CPUにバインドされた作業では、スレッドは占有されます。
実行中の処理をすべて別のプロセスにオフロードし、進行状況を監視する必要があります。 APIの場合、ここでの典型的なアプローチは、別のプロセスで作業をスケジュールし、すぐに 202 Accepted を返すことです。クライアントが応答を監視し、進捗状況を監視/取得するために利用できる応答本文にエンドポイントがあります。最終的に完成した結果。 Webhookを実装することもできます。Webhookは、プロセスを継続的に確認する必要なく、プロセスが完了したという通知を受け取るようにクライアントが登録できます。
他の唯一の選択肢は、問題にさらに多くのリソースを投入することです。たとえば、ロードバランサーの背後に複数のインスタンスをステージングして、各インスタンス間でリクエストを分割し、それぞれの全体的な負荷を軽減できます。
また、コードに何らかの非効率性や問題があり、修正してプロセスにかかる時間やリソースの消費量を削減できる可能性もあります。簡単な例として、Task.Run
のようなものを使用している場合、notを実行することで、大量のスレッドを解放できる可能性があります。 Task.Run
は、Webアプリケーションのコンテキスト内ではほとんど使用しないでください。ただし、コードを投稿していないため、そこに正確なガイダンスを提供することは不可能です。