Настройка производительности Http Akka
Я выполняю нагрузочное тестирование на Akka-http framework (версия: 10.0), я использую 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, я добавил отдельный пул потоков диспетчера для маршрута, который создает max, of 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, производительность будет такой же.
теперь, какие другие параметры или конфигурацию я должен использовать для увеличьте производительность при использовании future call.Так что приложение дает максимальную пропускную способность.
1 ответов
сначала некоторые оговорки: я не работал с wrk
инструмент раньше, поэтому я могу получить что-то не так. Вот предположения, которые я сделал для этого ответа:
- количество соединений не зависит от количества потоков, т. е. если я укажу
-t4 -c10000
оно держит 10000 соединений, не 4 * 10000. - для каждого соединения поведение выглядит следующим образом: он отправляет запрос, получает ответ полностью и немедленно отправляет следующий и т. д. пока время бежит из.
также я запустил сервер на той же машине, что и wrk, и моя машина кажется слабее вашей (У меня есть только двухъядерный процессор), поэтому я уменьшил количество потоков wrk до 2, а количество соединений до 1000, чтобы получить достойные результаты.
версия Http Akka, которую я использовал, - это 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
, который работал довольно хорошо в течение многих лет, пока асинхронный IO не позволил серверам масштабироваться намного выше.
для сравнения, вот полностью неблокирующие результаты теста на моей машине с пулом потоков 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, AWS Java SDKs и др.). Чтобы в полной мере воспользоваться преимуществами неблокирующего HTTP-сервера, необходимо не-преграждать полностью вниз, не как раз асинхронный. Это может быть не всегда возможно, но это то, к чему нужно стремиться.