web-dev-qa-db-ja.com

AkkaHttpパフォーマンスチューニング

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を超えて増やすと、パフォーマンスは同じになります。

さて、将来の呼び出しを使用しているときにパフォーマンスを向上させるために、他にどのようなパラメーターまたは構成を使用する必要がありますか?そのアプリが最大のスループットを提供します。

20
Vikas Naidu

最初にいくつかの免責事項:私は以前にwrkツールを使用したことがないので、何か問題が発生する可能性があります。これが私がこの答えのために作った仮定です:

  1. 接続数はスレッド数とは無関係です。つまり、-t4 -c10000を指定すると、4 * 10000ではなく10000接続が維持されます。
  2. すべての接続の動作は次のとおりです。要求を送信し、応答を完全に受信し、時間がなくなるまですぐに次の応答を送信するなどです。

また、サーバーを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を調整する必要がある場合があります。最高のスループットを得るには、操作のブロックを避けてください。

また、非同期操作と非ブロッキング操作を混同しないでください。 FutureThread.sleepを含むコードは、非同期ですがブロック操作の完璧な例です。多くの人気のあるソフトウェアがこのモードで動作します(一部のレガシーHTTPクライアント、Cassandraドライバー、AWS Java SDKなど))。 -HTTPサーバーをブロックする場合は、非同期だけでなく、完全に非ブロックにする必要があります。常に可能であるとは限りませんが、努力する必要があります。

28
Haspemulator