背景#
0 から 1 までのビジネス並列フレームワークシリーズの設計:
前の 2 つの記事では、フレームワークの設計の背景と抽象的な設計の詳細について説明しました。今日は、並列フレームワークの中でも最も重要な並列スレッドプールのコア設計について説明し、スレッドプールの分割設計に遭遇した問題と最終的にどのように実装したかについて説明します。
依存関係のあるタスクをグループに分割して、グループごとに実行すると、必要な結果をすべて取得できますが、スレッドプールをどのように分割し、設定するかという問題があります。
次に、実際のビジネスの複雑さを簡略化して設計し、問題を具体化して紹介します。
方案:共有スレッドプール#
方案#
最初に、割り当てられたタスクを共有スレッドプールで共有し、タスクがスレッドプールと競合するようにしました。以下の図のように:
しかし、すぐにわかったのは、単一のスレッドプールが要求数が増えると、あるタスクのインターフェースが遅くなると、インターフェース全体の成功率が急速に低下し、使用不能の状態になるということです。
なぜこのような状況が発生するのでしょうか?
効果#
- T1 の時刻、第 1 波のトラフィックが入り、その後、TaskA または TaskB が最初に実行されます。
- TaskA のリクエストが急速に増加し、インターフェースがますます遅くなります。
- T2 の時刻、まだ 2 つの TaskA が完了していない状態で、第 2 波のトラフィックが入ります:
- 第 1 波のトラフィックは TaskC と TaskD を実行し始めます。
- 第 2 波のトラフィックが入り、TaskA と TaskB もスレッドを取得します。
- T3 の時刻、この時点で 4 つの TaskA がまだ完了しておらず、最初の 2 つの TaskA はタイムアウトの状態に直面しています:
- 第 1 波のトラフィックで実行された TaskA はタイムアウト中断の状態に直面しています。
- 第 2 波のトラフィックで実行された TaskA も実行中です。
- 第 3 波のトラフィックが入り、状況は複雑になり、新しいトラフィックには TaskA と TaskB の実行が含まれます。
- この時点で、最初の波のトラフィックの最初の 2 つのレベルが完了し、TaskE の実行が開始されます。
- この時点で、2 番目の波のトラフィックの前のレベルが完了し、TaskC と TaskD の実行が開始されます。
- その後、TaskA が常に遅くなる状況が続きます......
- Tn の時刻、この時点でスレッドプールのほとんどが前の n 波の TaskA に占有され、多くの中断タイムアウトが発生し、他の Task はスレッドを競争して実行できません。
このように、インターフェースの可用性は完全に TaskA の可用性に依存することになりますが、もう 1 つ致命的な問題があります。他の Task は実行できず、または依存関係の問題により、前のレベルのほとんどがリクエストパラメータとして使用されることができず、正常にリクエストすることができません。したがって、インターフェースがデータを返しても、データは完全ではありません。
このようなアプローチでは、共有スレッドプールに大量のスレッドがタイムアウト待ちの状況が発生し、望ましくありません。
方案:階層化スレッドプール#
方案#
共有スレッドプールの状況は明らかに問題があります。その上で、階層化並列を試み、異なる並列プールに分割し、各層で共有スレッドプールを使用することにしました。以下の図のように:
階層化共有スレッドプールを導入した後、パフォーマンステストの結果、目標の効果はわずかに向上し、期待された目標には遠く及ばないことがわかりました。なぜこの問題が発生するのでしょうか?
効果#
引き続き、TaskA が並行リクエストの増加とともに大量のタイムアウトを引き起こす例として説明します。
- T1 の時刻、第 1 波のトラフィックが入り、すべてのスレッドプールのスレッドが埋まり、コアスケジューリングが開始されます。
- T2 の時刻、第 2 波のリクエストが入り、第 1 波のリクエストの 2 つの TaskA がまだ完了していない状態で、他のスレッドプールのスレッドが次々と第 2 波のリクエストを受け入れ、スケジューリングを待ちます。
- T3 の時刻、第 3 波のリクエストが入り、状況は比較的複雑になります:
- 第 1 波のトラフィックの 2 つの TaskA は既にタイムアウトして中断され、TaskC のスレッドプールの 2 つの TaskC スレッドはタスクの実行結果を待っている間に失敗し、タスクを終了します。
- 第 2 波のトラフィックの 2 つの TaskA はまだ完了しておらず、タイムアウトの状態にあります。
- 他のスレッドプールの実行は正常に行われます。
- しばらくして...
- Tn の時刻:
- TaskA は使用不能の状態になりました。
- TaskC に依存するタスクも徐々に使用不能の状態になります。
- 他のスレッドの実行は正常です。
このように、数十から数百のインターフェースを呼び出すシナリオでは、1 つのインターフェースまたは依存関係のあるインターフェースの可用性の低下が、インターフェース全体の可用性に影響を与えることはありません。また、単一のスレッドプールを監視し、アラートを追加するだけで、上流インターフェースの失敗を動的に検知し、対応するシステムメンテナンスチームに通知することができます。これにより、メンテナンスコストが大幅に削減されます。
このバージョンは、オンラインのプロダクション環境で最初のバージョンとしてデプロイされ、単一の 8C 8G(k8s)の設定でフレームワークを実行すると、QPS が 1.4w に達し、インターフェースの可用性が 99.96%に達しました(結果は参考までに、会社のクラスター展開戦略、マシンのパフォーマンスなどによって変動する可能性があります)。
ただし、このアプローチには明らかな問題がまだ存在しており、各タスクのインターフェースの応答が一貫していないため、いくつかは 50ms 以内、いくつかは 100ms 以内、いくつかは 500ms 以内といった具体的な応答時間があります。同じ数のスレッドプールを割り当てることは合理的ではないため、CPU スケジューリングが公平でなくなる可能性があります。では、どのようにスケジューリングをより公平にするのでしょうか?
最適化#
この問題に対処するために、スレッドプールのサイズをウェイトに基づいて作成しました。応答が遅いが一定時間待って返すことができるインターフェースには、より多くのスレッドプールサイズを割り当て、応答が非常に速いインターフェースには相対的にスレッドプールサイズを減らすという設計を行いました。この設計により、インターフェースの可用性を確保しつつ、インターフェースの返されるフィールドの完全性を考慮することができます。
最後に#
この記事では、フレームワークの設計において、分散並列実行をどのように実現するかについて説明しました。最終的には、独立したタスクスレッドプールのアプローチを採用し、タスクごとにスレッドプールを独立して作成し、CPU スレッドスケジューリングによってスレッドができるだけ公平に実行されるようにしました。これにより、インターフェースの並行要件と高可用性のシナリオを確保することができました。
興味がある場合は、公式アカウントをフォローするか、このサイトを購読することをお勧めします。お互いに交流し、一緒により強くなりましょう~