8

着信要求ごとに個別のスレッドを使用する REST サービス用の古い Java コードがあります。つまり、メイン ループは socket.accept() でループし、ソケットを Runnable に渡します。Runnable は独自のバックグラウンド スレッドを起動し、それ自体で run を呼び出します。これはしばらくの間見事にうまく機能していましたが、最近、要求を処理するための受け入れの遅れが高負荷下で受け入れられなくなることに気付きました。見事にうまく言えば、CPU をあまり使用せずに 1 秒あたり 100 ~ 200 のリクエストを処理していたことを意味します。パフォーマンスが低下したのは、他のデーモンも負荷を追加していたときだけで、負荷が 5 を超えたのは 1 回だけでした。マシンが他のプロセスの組み合わせから高い負荷 (5-8) にさらされていた場合、受け入れから処理までの時間が途方もなく長くなりました ( 500 ミリ秒から 3000 ミリ秒)、実際の処理は 10 ミリ秒未満のままでした。

.NET のスレッドプールに慣れていたので、スレッドの作成が原因であると想定し、Java で同じパターンを適用すると考えました。今私の Runnable は ThreadPool.Executor で実行されます (そしてプールは ArrayBlockingQueue を使用します)。繰り返しになりますが、マシンの負荷が高くならない限り、ほとんどのシナリオでうまく機能します。その後、ランナブルの作成から run() が呼び出されるまでの時間は、ほぼ同じばかげたタイミングを示します。さらに悪いことに、スレッドプール ロジックを導入すると、システム負荷がほぼ 2 倍 (10-16) になりました。そのため、負荷が 2 倍になると同じ遅延の問題が発生します。

私の疑いでは、キューのロック競合は、ロックのない以前の新しいスレッドの起動コストよりも悪いということです。新しいスレッドとスレッドプールの経験を共有できる人はいますか? そして、私の疑いが正しければ、ロック競合なしでスレッドプールを処理するための代替アプローチを誰かが持っていますか?

スレッド化がどれだけ役立つかわからず、IO が問題にならないように見えるので、システム全体をシングルスレッド化したくなるかもしれませんが、長寿命のリクエストがいくつかあります。すべてをブロックします。

ありがとう、アルネ

更新: 切り替えてExecutors.newFixedThreadPool(100);、同じ処理能力を維持しながら、負荷はすぐにほぼ 2 倍になり、12 時間実行すると、負荷が常に 2 倍にとどまることがわかりました。私の場合、リクエストごとの新しいスレッドの方が安いと思います。

4

4 に答える 4

12

次の構成で:

new ThreadPoolExecutor(10, 100, 30, TimeUnit.SECONDS, 
        new ArrayBlockingQueue<Runnable>(100))

次に、10 個のスレッドが同時に要求を処理すると、キュー内の要求が 100 個に達しない限り、さらに要求がキューに追加されます。その時点で、コマンドの処理が開始されるときに、既に 100 個のスレッドがある場合を除き、新しいスレッドの作成が開始されます。拒否されます。

(以下にコピーされている) のjavadocThreadPoolExecutorのセクションは、もう一度読む価値があるかもしれません。

それらに基づいて、100 個のスレッドを実行したいという明確な意思と、すべてのリクエストを受け入れて最終的に処理したいという願望..次のようなバリエーションを試すことをお勧めします。

new ThreadPoolExecutor(100, 100, 0, TimeUnit.SECONDS, 
        new LinkedBlockingQueue<Runnable>())

ちなみに、これはあなたが得るものですExecutors.newFixedThreadPool(100);


待ち行列

送信されたタスクを転送および保持するために、任意の BlockingQueue を使用できます。このキューの使用は、プールのサイズ設定と相互作用します。

  • 実行中のスレッドが corePoolSize よりも少ない場合、Executor は常に、キューに入れるよりも新しいスレッドを追加することを優先します。
  • corePoolSize 以上のスレッドが実行されている場合、Executor は常に、新しいスレッドを追加するよりも要求をキューに入れることを優先します。
  • 要求をキューに入れることができない場合、これが maximumPoolSize を超えない限り、新しいスレッドが作成されます。この場合、タスクは拒否されます。

キューイングには 3 つの一般的な戦略があります。

  1. 直接引き継ぎ。ワーク キューの適切なデフォルトの選択肢は、タスクをスレッドに保持せずにスレッドに渡す SynchronousQueue です。ここで、タスクをすぐに実行できるスレッドがない場合、タスクをキューに入れる試みは失敗するため、新しいスレッドが構築されます。このポリシーは、内部依存関係を持つ可能性のある一連の要求を処理する際のロックアップを回避します。直接ハンドオフでは、通常、新しく送信されたタスクの拒否を回避するために、無制限の maximumPoolSizes が必要です。これは、コマンドが処理できるよりも平均して速く到着し続ける場合に、無限のスレッド成長の可能性を認めています。
  2. 無制限のキュー。無制限のキュー (事前定義された容量のない LinkedBlockingQueue など) を使用すると、すべての corePoolSize スレッドがビジー状態のときに、新しいタスクがキューで待機します。したがって、corePoolSize を超えるスレッドが作成されることはありません。(したがって、 maximumPoolSize の値は何の効果もありません。) これは、各タスクが他のタスクから完全に独立している場合に適切な場合があるため、タスクが他のタスクの実行に影響を与えることはできません。たとえば、Web ページ サーバーで。このスタイルのキューイングは、リクエストの一時的なバーストをスムーズにするのに役立ちますが、コマンドが処理できるよりも平均して速く到着し続けると、無制限のワーク キューの成長の可能性が認められます。
  3. 制限されたキュー。バインドされたキュー (たとえば、ArrayBlockingQueue) は、有限の maximumPoolSizes で使用するとリソースの枯渇を防ぐのに役立ちますが、調整と制御がより困難になる可能性があります。キュー サイズと最大プール サイズは互いにトレードオフされる可能性があります。大きなキューと小さなプールを使用すると、CPU 使用率、OS リソース、およびコンテキスト切り替えのオーバーヘッドが最小限に抑えられますが、スループットが人為的に低くなる可能性があります。タスクが頻繁にブロックされる場合 (たとえば、I/O バウンドの場合)、システムは、他の方法で許可されているよりも多くのスレッドの時間をスケジュールできる場合があります。一般に、小さなキューを使用するには、より大きなプール サイズが必要になるため、CPU のビジー状態が維持されますが、許容できないスケジューリング オーバーヘッドが発生する可能性があり、スループットも低下します。
于 2009-03-04T07:34:20.537 に答える
4

計測、計測、計測!どこで時間を過ごしていますか?Runnable を作成するときに何が必要ですか? Runnable には、インスタンス化をブロックまたは遅延させる可能性のあるものはありますか? その遅延の間に何が起こっていますか?

私は実際には、一般的に物事を熟考することを強く信じていますが、この種のケースでは、このような予期しない動作が発生するため、いくつかの測定が必要です.

ランタイム環境、JVM のバージョン、およびアーキテクチャは何ですか?

于 2009-03-01T23:05:50.617 に答える
1

Sun の の実装はThread、以前よりもはるかに高速ですが、ロックがあります。IIRC は、ArrayBlockingQueueビジー状態のときは実際にはまったくロックすべきではありません。したがって、プロファイラーの時間です (または数ctrl-\秒または数jstack秒)。

システム負荷は、キューに入れられているスレッドの数を示しているだけです。必ずしも非常に役立つとは限りません。

于 2009-03-02T12:56:31.933 に答える
0

私は自分のコードのいくつかでこれをやっただけです。Netbeans プロファイラーを使用して、使用していたスレッド プールの実装を変更しました。Visual VMでも同じことができるはずですが、まだ試していません。

于 2009-03-01T23:43:13.897 に答える