Репозиторий содержит код для сравнения производительности серверного рендеринга при наличии одной тяжелой синхронной задачи, или выполнения той же работы с помощью множества небольших синхронных задач.
В качестве эталона синхронной задачи используется метод ReactDOMServer.renderToString
.
Тяжелая задача - множество последовательных вызовов renderToString
, декомпозиция этой задачи на маленькие таски реализована с помощью вложенных вызовов setImmediate
.
Цель сравнения - проверка теории, что NodeJS
сервер лучше справляется с нагрузкой, если не выполняет тяжелых синхронных задач.
Производительность проверяется с помощью autocannon.
autocannon
позволяет увидеть RPS и время ответа от сервера на разных перцентилях.
Время выполнения одной тяжелой задачи === времени выполнения пачки мелких задач.
Проверка происходит и с небольшими нагрузками, и с огромным количеством запросов.
Первое исследование - исключительно сравнение одной тяжелой и множества декомпозированных задач.
Второе исследование - добавляется эмуляция запросов к API до вызова renderToString
.
Одна длинная таска (long task) или множество коротких задач (small tasks) - практически не влияет на RPS, и соответственно не значительно влияет на среднее (average) время ответа от сервера (latency). При этом, значительно отличаются latency на разных перцентилях.
Исследование без эмуляции запросов к API, исключительно синхронные задачи.
Long task сервер отдает быстро ответы на первые запросы, при этом все последующие буквально встают в очередь, и максимальное latency будет у клиента, который отправил последний запрос. Это интуитивно понятное поведение.
Команда для эмуляции большой нагрузки:
autocannon -c 50 -d 10 http://localhost:3000/ -l -t 1000 --amount 200
Running 200 requests test @ http://localhost:3000/
50 connections
┌─────────┬────────┬─────────┬─────────┬─────────┬────────────┬────────────┬─────────┐
│ Stat │ 2.5% │ 50% │ 97.5% │ 99% │ Avg │ Stdev │ Max │
├─────────┼────────┼─────────┼─────────┼─────────┼────────────┼────────────┼─────────┤
│ Latency │ 459 ms │ 4821 ms │ 4933 ms │ 4982 ms │ 4304.48 ms │ 1356.04 ms │ 9741 ms │
└─────────┴────────┴─────────┴─────────┴─────────┴────────────┴────────────┴─────────┘
┌───────────┬───────┬───────┬─────────┬─────────┬─────────┬────────┬───────┐
│ Stat │ 1% │ 2.5% │ 50% │ 97.5% │ Avg │ Stdev │ Min │
├───────────┼───────┼───────┼─────────┼─────────┼─────────┼────────┼───────┤
│ Req/Sec │ 8 │ 8 │ 10 │ 11 │ 10 │ 0.64 │ 8 │
├───────────┼───────┼───────┼─────────┼─────────┼─────────┼────────┼───────┤
│ Bytes/Sec │ 976 B │ 976 B │ 1.22 kB │ 1.34 kB │ 1.22 kB │ 77.2 B │ 976 B │
└───────────┴───────┴───────┴─────────┴─────────┴─────────┴────────┴───────┘
Req/Bytes counts sampled once per second.
┌────────────┬──────────────┐
│ Percentile │ Latency (ms) │
├────────────┼──────────────┤
│ 0.001 │ 147 │
├────────────┼──────────────┤
│ 0.01 │ 147 │
├────────────┼──────────────┤
│ 0.1 │ 147 │
├────────────┼──────────────┤
│ 1 │ 199 │
├────────────┼──────────────┤
│ 2.5 │ 459 │
├────────────┼──────────────┤
│ 10 │ 1946 │
├────────────┼──────────────┤
│ 25 │ 4789 │
├────────────┼──────────────┤
│ 50 │ 4821 │
├────────────┼──────────────┤
│ 75 │ 4889 │
├────────────┼──────────────┤
│ 90 │ 4901 │
├────────────┼──────────────┤
│ 97.5 │ 4933 │
├────────────┼──────────────┤
│ 99 │ 4982 │
├────────────┼──────────────┤
│ 99.9 │ 9741 │
├────────────┼──────────────┤
│ 99.99 │ 9741 │
├────────────┼──────────────┤
│ 99.999 │ 9741 │
└────────────┴──────────────┘
200 requests in 20.04s, 24.4 kB read
Small tasks сервер выполняет все полученные одновременно запросы параллельно. Это приводит к такому контринтуитивному поведению, что все пользователи получают ответы с большой задержкой, примерно равной average latency, даже если первые из этих пользователей сделали запрос на сервер в самом начале всплеска нагрузки.
Команда для эмуляции большой нагрузки:
autocannon -c 50 -d 10 http://localhost:4000/ -l -t 1000 --amount 200
Running 200 requests test @ http://localhost:4000/
50 connections
┌─────────┬─────────┬─────────┬─────────┬─────────┬────────────┬───────────┬─────────┐
│ Stat │ 2.5% │ 50% │ 97.5% │ 99% │ Avg │ Stdev │ Max │
├─────────┼─────────┼─────────┼─────────┼─────────┼────────────┼───────────┼─────────┤
│ Latency │ 4848 ms │ 4854 ms │ 5193 ms │ 5205 ms │ 4916.98 ms │ 114.35 ms │ 5213 ms │
└─────────┴─────────┴─────────┴─────────┴─────────┴────────────┴───────────┴─────────┘
┌───────────┬─────┬──────┬─────┬────────┬─────────┬─────────┬───────┐
│ Stat │ 1% │ 2.5% │ 50% │ 97.5% │ Avg │ Stdev │ Min │
├───────────┼─────┼──────┼─────┼────────┼─────────┼─────────┼───────┤
│ Req/Sec │ 0 │ 0 │ 0 │ 50 │ 10 │ 18.51 │ 2 │
├───────────┼─────┼──────┼─────┼────────┼─────────┼─────────┼───────┤
│ Bytes/Sec │ 0 B │ 0 B │ 0 B │ 6.1 kB │ 1.22 kB │ 2.26 kB │ 244 B │
└───────────┴─────┴──────┴─────┴────────┴─────────┴─────────┴───────┘
Req/Bytes counts sampled once per second.
┌────────────┬──────────────┐
│ Percentile │ Latency (ms) │
├────────────┼──────────────┤
│ 0.001 │ 4813 │
├────────────┼──────────────┤
│ 0.01 │ 4813 │
├────────────┼──────────────┤
│ 0.1 │ 4813 │
├────────────┼──────────────┤
│ 1 │ 4848 │
├────────────┼──────────────┤
│ 2.5 │ 4848 │
├────────────┼──────────────┤
│ 10 │ 4849 │
├────────────┼──────────────┤
│ 25 │ 4851 │
├────────────┼──────────────┤
│ 50 │ 4854 │
├────────────┼──────────────┤
│ 75 │ 4865 │
├────────────┼──────────────┤
│ 90 │ 5127 │
├────────────┼──────────────┤
│ 97.5 │ 5193 │
├────────────┼──────────────┤
│ 99 │ 5205 │
├────────────┼──────────────┤
│ 99.9 │ 5213 │
├────────────┼──────────────┤
│ 99.99 │ 5213 │
├────────────┼──────────────┤
│ 99.999 │ 5213 │
└────────────┴──────────────┘
200 requests in 20.04s, 24.4 kB read
В этом исследовании добавлена эмуляция запросов к API. API отвечает медленно на каждый второй запрос.
Запросы так же встают в очередь, в подробной latency по перцентилям ярко видно, что чем позже пришел запрос, тем дольше будет ждать пользователь.
Команда для эмуляции большой нагрузки:
autocannon -c 50 -d 10 http://localhost:3000/ -l -t 1000 --amount 200
Running 200 requests test @ http://localhost:3000/
50 connections
┌─────────┬────────┬─────────┬─────────┬─────────┬────────────┬────────────┬─────────┐
│ Stat │ 2.5% │ 50% │ 97.5% │ 99% │ Avg │ Stdev │ Max │
├─────────┼────────┼─────────┼─────────┼─────────┼────────────┼────────────┼─────────┤
│ Latency │ 797 ms │ 4882 ms │ 7923 ms │ 8026 ms │ 4856.98 ms │ 1850.24 ms │ 8096 ms │
└─────────┴────────┴─────────┴─────────┴─────────┴────────────┴────────────┴─────────┘
┌───────────┬───────┬───────┬────────┬─────────┬─────────┬───────┬───────┐
│ Stat │ 1% │ 2.5% │ 50% │ 97.5% │ Avg │ Stdev │ Min │
├───────────┼───────┼───────┼────────┼─────────┼─────────┼───────┼───────┤
│ Req/Sec │ 1 │ 1 │ 9 │ 10 │ 8.7 │ 1.88 │ 1 │
├───────────┼───────┼───────┼────────┼─────────┼─────────┼───────┼───────┤
│ Bytes/Sec │ 122 B │ 122 B │ 1.1 kB │ 1.22 kB │ 1.06 kB │ 229 B │ 122 B │
└───────────┴───────┴───────┴────────┴─────────┴─────────┴───────┴───────┘
Req/Bytes counts sampled once per second.
┌────────────┬──────────────┐
│ Percentile │ Latency (ms) │
├────────────┼──────────────┤
│ 0.001 │ 356 │
├────────────┼──────────────┤
│ 0.01 │ 356 │
├────────────┼──────────────┤
│ 0.1 │ 356 │
├────────────┼──────────────┤
│ 1 │ 466 │
├────────────┼──────────────┤
│ 2.5 │ 797 │
├────────────┼──────────────┤
│ 10 │ 2406 │
├────────────┼──────────────┤
│ 25 │ 3558 │
├────────────┼──────────────┤
│ 50 │ 4882 │
├────────────┼──────────────┤
│ 75 │ 6308 │
├────────────┼──────────────┤
│ 90 │ 7382 │
├────────────┼──────────────┤
│ 97.5 │ 7923 │
├────────────┼──────────────┤
│ 99 │ 8026 │
├────────────┼──────────────┤
│ 99.9 │ 8096 │
├────────────┼──────────────┤
│ 99.99 │ 8096 │
├────────────┼──────────────┤
│ 99.999 │ 8096 │
└────────────┴──────────────┘
200 requests in 23.04s, 24.4 kB read
Похожая картина как и без эмуляции API, но на подробной таблице latency по перцентилям видно, что первые пользователи начали получать ответы все-таки быстрее, чем последующие. В первом бенчмарке на каждом перцентиле были близкие latency.
В этом бенчмарке (и в других, если ставить небольшую нагрузку), заметно что на 75 и 90 перцентилях small tasks сервер отвечает пользователям заметно быстрее.
Команда для эмуляции большой нагрузки:
autocannon -c 50 -d 10 http://localhost:4000/ -l -t 1000 --amount 200
Running 200 requests test @ http://localhost:4000/
50 connections
┌─────────┬─────────┬─────────┬─────────┬─────────┬───────────┬───────────┬─────────┐
│ Stat │ 2.5% │ 50% │ 97.5% │ 99% │ Avg │ Stdev │ Max │
├─────────┼─────────┼─────────┼─────────┼─────────┼───────────┼───────────┼─────────┤
│ Latency │ 4016 ms │ 4641 ms │ 5561 ms │ 5575 ms │ 4847.4 ms │ 553.43 ms │ 5579 ms │
└─────────┴─────────┴─────────┴─────────┴─────────┴───────────┴───────────┴─────────┘
┌───────────┬─────┬──────┬───────┬─────────┬─────────┬─────────┬───────┐
│ Stat │ 1% │ 2.5% │ 50% │ 97.5% │ Avg │ Stdev │ Min │
├───────────┼─────┼──────┼───────┼─────────┼─────────┼─────────┼───────┤
│ Req/Sec │ 0 │ 0 │ 7 │ 25 │ 9.53 │ 8.8 │ 4 │
├───────────┼─────┼──────┼───────┼─────────┼─────────┼─────────┼───────┤
│ Bytes/Sec │ 0 B │ 0 B │ 854 B │ 3.05 kB │ 1.16 kB │ 1.07 kB │ 488 B │
└───────────┴─────┴──────┴───────┴─────────┴─────────┴─────────┴───────┘
Req/Bytes counts sampled once per second.
┌────────────┬──────────────┐
│ Percentile │ Latency (ms) │
├────────────┼──────────────┤
│ 0.001 │ 3828 │
├────────────┼──────────────┤
│ 0.01 │ 3828 │
├────────────┼──────────────┤
│ 0.1 │ 3828 │
├────────────┼──────────────┤
│ 1 │ 3832 │
├────────────┼──────────────┤
│ 2.5 │ 4016 │
├────────────┼──────────────┤
│ 10 │ 4160 │
├────────────┼──────────────┤
│ 25 │ 4386 │
├────────────┼──────────────┤
│ 50 │ 4641 │
├────────────┼──────────────┤
│ 75 │ 5460 │
├────────────┼──────────────┤
│ 90 │ 5533 │
├────────────┼──────────────┤
│ 97.5 │ 5561 │
├────────────┼──────────────┤
│ 99 │ 5575 │
├────────────┼──────────────┤
│ 99.9 │ 5579 │
├────────────┼──────────────┤
│ 99.99 │ 5579 │
├────────────┼──────────────┤
│ 99.999 │ 5579 │
└────────────┴──────────────┘
200 requests in 21.04s, 24.4 kB read
Разделение одной тяжелой задачи на много мелких начинает хорошо себя показывать в тех случаях, когда на каждый запрос сервер делает много различных не детерменированных по времени синхронных и асинхронных действий.
У сервера появляется возможность намного быстрее ответить на те запросы, которые делают минимум запросов к API, или вообще сразу отдают 404 страницу или редирект.
Но если большая часть запросов вызывает одинаковые синхронные действия, и одинаковые по длительности запросы к API, при больших нагрузках все пользователи получат одинаково плохое время ответа, что вряд ли является ожидаемым поведением - иногда таймауты ответа к серверу у небольшого количества пользователей могут быть более удачным вариантом.