2016-12-23 5 views
13

Я выполняю нагрузочное тестирование на платформе Akka-http (версия: 10.0), я использую инструмент wrk. Команда WRK:Akka Http Performance tuning

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 documentation я добавил отдельный пул диспетчерских потоков для маршрута, который создает максимум, 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 

В мой блокирующие-диспетчерских конфигурациях, если я увеличить размер пула выше 200 производительность же.

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

ответ

17

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

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

Также я запустил сервер на той же машине, что и wrk, и моя машина кажется слабее вашей (у меня только двухъядерный процессор), поэтому я сократил количество потоков wrk до 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, которая работала довольно хорошо в течение многих лет, пока асинхронные серверы ввода-вывода значительно увеличились.

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

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-сервера, вам необходимо не блокировать весь путь вниз, а не просто асинхронно. Это может быть не всегда возможно, но это то, к чему нужно стремиться.

+0

Очень хороший анализ.

+2

Итак, проблема заключается в том, что 'Thread.sleep' ест ваши потоки. В целях тестирования вы также можете попробовать 'akka.pattern.after' создать будущее, которое будет завершено позже, без блокировки потоков. – jrudolph

+0

@Haspemulator Я новичок в мире акков и должен был по-настоящему почесать голову о том, что попало в файл conf, чтобы получить упомянутую производительность для «Для сравнения, вот полные неблокирующие результаты теста на моей машине с пулом потоков fork-join по умолчанию: «Не могли бы вы поделиться тем же. Ваш ответ очень информативен. Cheers – Akash