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