Подтвердить что ты не робот

Настройка производительности Akka Http

Я выполняю нагрузочное тестирование на платформе 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

Как вы можете видеть, с будущим вызовом << → << →

После документации Akka я добавил отдельный пул потоков диспетчера для маршрута, который создает max, 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, если я увеличиваю размер пула выше 200, производительность будет такой же.

Теперь, какие другие параметры или конфиг следует использовать для повышения производительности при использовании будущего вызова. Поэтому это приложение дает максимальную пропускную способность.

4b9b3361

Ответ 1

Некоторые отказы от ответственности сначала: я раньше не работал с инструментом wrk, поэтому я мог бы получить что-то неправильно. Вот предположения, которые я сделал для этого ответа:

  • Количество подключений не зависит от количества потоков, т.е. если я укажу -t4 -c10000, он поддерживает 10000 соединений, а не 4 * 10000.
  • Для каждого подключения поведение выглядит следующим образом: он отправляет запрос, полностью получает ответ и сразу же отправляет следующий и т.д. до истечения времени.

Также я запустил сервер на том же компьютере, что и wrk, и моя машина, по-видимому, слабее вашей (у меня только двухъядерный процессор), поэтому я уменьшил количество слов wrk thread до 2 и количество подключений до 1000, чтобы получить достойные результаты.

Версия Akka Http, которую я использовал, - это 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 - прошедшее время в миллисекундах.

Это дает нам 2000 сообщений, обработанных в секунду, или ~ 60000 сообщений за 30 секунд, что в основном согласуется с показателями:

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

Также очевидно, что это число (2000 сообщений в секунду) строго связано с количеством потоков. Например. если бы у нас было 300 потоков, мы обрабатывали 300 сообщений каждые 100 мс, поэтому у нас было бы 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

Как вы можете видеть, теперь один запрос занимает в среднем почти ровно 100 мс, т.е. ту же сумму, которую мы помещаем в Thread.sleep. Кажется, мы не можем добиться намного быстрее этого! Теперь мы в значительной степени находимся в стандартной ситуации one thread per request, которая работала довольно хорошо в течение многих лет, пока асинхронные серверы ввода-вывода не увеличили уровень масштабирования.

Для сравнения, здесь полностью неблокирующие результаты теста на моей машине с пулом потоков fork-join по умолчанию:

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

Подводя итог, если вы используете блокирующие операции, вам нужен один поток для каждого запроса для достижения наилучшей пропускной способности, поэтому соответствующим образом настройте пул потоков. Существуют естественные ограничения на количество потоков, которые может обрабатывать ваша система, и вам может потребоваться настроить вашу ОС для максимального количества потоков. Для обеспечения максимальной пропускной способности избегайте операций блокировки.

Также не путайте асинхронные операции с неблокирующими. Ваш код с Future и Thread.sleep является прекрасным примером асинхронной, но блокирующей операции. В этом режиме работает много популярного программного обеспечения (некоторые устаревшие HTTP-клиенты, драйверы Cassandra, SDK JavaSVS и т.д.). Чтобы полностью воспользоваться преимуществами неблокирующего HTTP-сервера, вам необходимо не блокировать весь путь вниз, а не просто асинхронно. Возможно, это не всегда возможно, но к чему-то нужно стремиться.