Akka-httpフレームワーク(バージョン:10.0)で負荷テストを実行しています。 wrk ツールを使用しています。 wrkコマンド:
wrk -t6 -c10000 -d 60s --timeout 10s --latency http://localhost:8080/hello
ブロッキング呼び出しなしで最初に実行し、
object WebServer {
implicit val system = ActorSystem("my-system")
implicit val materializer = ActorMaterializer()
implicit val executionContext = system.dispatcher
def main(args: Array[String]) {
val bindingFuture = Http().bindAndHandle(router.route, "localhost", 8080)
println(
s"Server online at http://localhost:8080/\nPress RETURN to stop...")
StdIn.readLine() // let it run until user presses return
bindingFuture
.flatMap(_.unbind()) // trigger unbinding from the port
.onComplete(_ => system.terminate()) // and shutdown when done
}
}
object router {
implicit val executionContext = WebServer.executionContext
val route =
path("hello") {
get {
complete {
"Ok"
}
}
}
}
wrkの出力:
Running 1m test @ http://localhost:8080/hello
6 threads and 10000 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 4.22ms 16.41ms 2.08s 98.30%
Req/Sec 9.86k 6.31k 25.79k 62.56%
Latency Distribution
50% 3.14ms
75% 3.50ms
90% 4.19ms
99% 31.08ms
3477084 requests in 1.00m, 477.50MB read
Socket errors: connect 9751, read 344, write 0, timeout 0
Requests/sec: 57860.04
Transfer/sec: 7.95MB
ルートに将来の呼び出しを追加して、テストを再実行するとします。
val route =
path("hello") {
get {
complete {
Future { // Blocking code
Thread.sleep(100)
"OK"
}
}
}
}
Wrkの出力:
Running 1m test @ http://localhost:8080/hello
6 threads and 10000 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 527.07ms 491.20ms 10.00s 88.19%
Req/Sec 49.75 39.55 257.00 69.77%
Latency Distribution
50% 379.28ms
75% 632.98ms
90% 1.08s
99% 2.07s
13744 requests in 1.00m, 1.89MB read
Socket errors: connect 9751, read 385, write 38, timeout 98
Requests/sec: 228.88
Transfer/sec: 32.19KB
将来の呼び出しでのみわかるように13744リクエストが処理されています。
Akkaのドキュメント をフォローした後、最大200スレッドを作成するルート用に別のディスパッチャースレッドプールを追加しました。
implicit val executionContext = WebServer.system.dispatchers.lookup("my-blocking-dispatcher")
// config of dispatcher
my-blocking-dispatcher {
type = Dispatcher
executor = "thread-pool-executor"
thread-pool-executor {
// or in Akka 2.4.2+
fixed-pool-size = 200
}
throughput = 1
}
上記の変更後、パフォーマンスは少し向上しました
Running 1m test @ http://localhost:8080/hello
6 threads and 10000 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 127.03ms 21.10ms 504.28ms 84.30%
Req/Sec 320.89 175.58 646.00 60.01%
Latency Distribution
50% 122.85ms
75% 135.16ms
90% 147.21ms
99% 190.03ms
114378 requests in 1.00m, 15.71MB read
Socket errors: connect 9751, read 284, write 0, timeout 0
Requests/sec: 1903.01
Transfer/sec: 267.61KB
my-blocking-dispatcher configで、プールサイズを200を超えて増やすと、パフォーマンスは同じになります。
さて、将来の呼び出しを使用しているときにパフォーマンスを向上させるために、他にどのようなパラメーターまたは構成を使用する必要がありますか?そのアプリが最大のスループットを提供します。
最初にいくつかの免責事項:私は以前にwrk
ツールを使用したことがないので、何か問題が発生する可能性があります。これが私がこの答えのために作った仮定です:
-t4 -c10000
を指定すると、4 * 10000ではなく10000接続が維持されます。また、サーバーをwrkと同じマシンで実行しましたが、私のマシンはあなたのマシンよりも弱いようです(デュアルコアCPUしかありません)。そのため、wrkのスレッド数を2に減らし、接続数を1000に減らしました。まともな結果を得るために。
私が使用したAkkaHttpバージョンは10.0.1
であり、wrkバージョンは4.0.2
です。
今答えに。あなたが持っているブロッキングコードを見てみましょう:
Future { // Blocking code
Thread.sleep(100)
"OK"
}
つまり、すべてのリクエストには少なくとも100ミリ秒かかります。 200のスレッドと1000の接続がある場合、タイムラインは次のようになります。
Msg: 0 200 400 600 800 1000 1200 2000
|--------|--------|--------|--------|--------|--------|---..---|---...
Ms: 0 100 200 300 400 500 600 1000
Msg
は処理されたメッセージの量ですが、Ms
はミリ秒単位の経過時間です。
これにより、1秒あたり2000メッセージ、または30秒あたり最大60000メッセージが処理されます。これは、テストの数値とほぼ一致しています。
wrk -t2 -c1000 -d 30s --timeout 10s --latency http://localhost:8080/hello
Running 30s test @ http://localhost:8080/hello
2 threads and 1000 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 412.30ms 126.87ms 631.78ms 82.89%
Req/Sec 0.95k 204.41 1.40k 75.73%
Latency Distribution
50% 455.18ms
75% 512.93ms
90% 517.72ms
99% 528.19ms
here: --> 56104 requests in 30.09s <--, 7.70MB read
Socket errors: connect 0, read 1349, write 14, timeout 0
Requests/sec: 1864.76
Transfer/sec: 262.23KB
この数(1秒あたり2000メッセージ)がスレッド数によって厳密に制限されていることも明らかです。例えば。 300スレッドの場合、100ミリ秒ごとに300メッセージを処理するため、システムが非常に多くのスレッドを処理できる場合は、1秒あたり3000メッセージになります。接続ごとに1つのスレッド、つまりプールに1000のスレッドを提供した場合、どのように処理されるかを見てみましょう。
wrk -t2 -c1000 -d 30s --timeout 10s --latency http://localhost:8080/hello
Running 30s test @ http://localhost:8080/hello
2 threads and 1000 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 107.08ms 16.86ms 582.44ms 97.24%
Req/Sec 3.80k 1.22k 5.05k 79.28%
Latency Distribution
50% 104.77ms
75% 106.74ms
90% 110.01ms
99% 155.24ms
223751 requests in 30.08s, 30.73MB read
Socket errors: connect 0, read 1149, write 1, timeout 0
Requests/sec: 7439.64
Transfer/sec: 1.02MB
ご覧のとおり、1つのリクエストには平均でほぼ正確に100ミリ秒かかります。つまり、Thread.sleep
に入力したのと同じ量です。これ以上速くなることはできないようです!これで、one thread per request
の標準的な状況になりました。これは、非同期IOサーバーのスケールアップがはるかに大きくなるまで、長年にわたってかなりうまく機能していました。
比較のために、デフォルトのフォークジョインスレッドプールを使用した私のマシンでの完全にノンブロッキングのテスト結果を次に示します。
complete {
Future {
"OK"
}
}
====>
wrk -t2 -c1000 -d 30s --timeout 10s --latency http://localhost:8080/hello
Running 30s test @ http://localhost:8080/hello
2 threads and 1000 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 15.50ms 14.35ms 468.11ms 93.43%
Req/Sec 22.00k 5.99k 34.67k 72.95%
Latency Distribution
50% 13.16ms
75% 18.77ms
90% 25.72ms
99% 66.65ms
1289402 requests in 30.02s, 177.07MB read
Socket errors: connect 0, read 1103, write 42, timeout 0
Requests/sec: 42946.15
Transfer/sec: 5.90MB
要約すると、ブロッキング操作を使用する場合、最高のスループットを達成するには、要求ごとに1つのスレッドが必要なので、それに応じてスレッドプールを構成します。システムが処理できるスレッドの数には自然な制限があり、最大スレッド数になるようにOSを調整する必要がある場合があります。最高のスループットを得るには、操作のブロックを避けてください。
また、非同期操作と非ブロッキング操作を混同しないでください。 Future
とThread.sleep
を含むコードは、非同期ですがブロック操作の完璧な例です。多くの人気のあるソフトウェアがこのモードで動作します(一部のレガシーHTTPクライアント、Cassandraドライバー、AWS Java SDKなど))。 -HTTPサーバーをブロックする場合は、非同期だけでなく、完全に非ブロックにする必要があります。常に可能であるとは限りませんが、努力する必要があります。