Пару недель назад, я решил взять простейший пример HTTP сервера на Go и измерить его производительность. Потом я смело взял Phoenix, прогнал на тех же тестах, и расстроился. Результаты были не в пользу Elixir/Erlang (45133 RPS у Go и всего 3065 RPS у Phoenix). Но Phoenix — это тяжело. Надо что-то хотя бы примерно равное по простоте и логике разработки тому, что есть на Go: когда есть путь — "/" и handler для него. Логичной аналогией мне показалось решение cowboy + plug, где у нас есть Router, который так же ловит "/" и отвечает на него. Результаты убили — Elixir/Erlang опять оказался медленнее:
Golang
sea@sea:~/go$ wrk -t10 -c100 -d10s http://127.0.0.1:4000/
...
452793 requests in 10.03s, 58.30MB read
Requests/sec: 45133.28
Transfer/sec: 5.81MB
elixir cowboy plug
sea@sea:~/http_test$ wrk -t10 -c100 -d10s http://127.0.0.1:4000/
...
184703 requests in 10.02s, 28.57MB read
Requests/sec: 18441.79
Transfer/sec: 2.85MB
Как жить дальше? Две недели я не спал и не ел (почти). Все, во что я верил все эти годы: совершенство vm erlang, ФП, зеленые процессы, было растоптано разорвано, сожжено и пущено по ветру. Немного отойдя от шока, успокоившись, и подтерев сопли я решил разобаться, в чем дело.
И сервер и тестовая программа были запущены на одной виртуальной машине, внутри VirtualBOX, с выделенными двумя ядрами.
если окажется, что работать придется в таких или похожих условиях, то Go действительно покажет более высокие результаты
Тестовый компьютер
Как я тестировал. Я работаю на ноутбуке с Windows 7 x64, процессор i7, 8 Gb RAM, а Linux — в моем случае Ubuntu 16, я запускаю внутри VirtualBOX. Для нее я выделил 1 Gb RAM и 2 ядра. Внутри этой виртуальной машины я запустил HTTP сервер, и на этой же машине запускал тестирование ab и wrk. При таком раскладе, одна и та же машина получается нагружена и сервером и тестом; передача данных по сети не накладывает ограничений, потому что передачи по сети нет.
В итоге мы получили полный разгром:
Go:
sea@sea:~/go$ wrk -t10 -c100 -d10s http://127.0.0.1:4000/
Running 10s test @ http://127.0.0.1:4000/
10 threads and 100 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 65.18ms 109.44ms 1.05s 86.48%
Req/Sec 4.60k 5.87k 25.40k 86.85%
452793 requests in 10.03s, 58.30MB read
Requests/sec: 45133.28
Transfer/sec: 5.81MB
Elixir cowboy:
sea@sea:~/http_test$ wrk -t10 -c100 -d10s http://127.0.0.1:4000/
Running 10s test @ http://127.0.0.1:4000/
10 threads and 100 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 8.94ms 11.38ms 123.57ms 86.53%
Req/Sec 1.85k 669.61 4.99k 71.70%
184703 requests in 10.02s, 28.57MB read
Requests/sec: 18441.79
Transfer/sec: 2.85MB
Go:
sea@sea:~/go$ wrk -t10 -c1000 -d10s http://127.0.0.1:4000/
Running 10s test @ http://127.0.0.1:4000/
10 threads and 1000 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 61.16ms 231.88ms 2.00s 92.97%
Req/Sec 7.85k 8.65k 26.13k 79.49%
474853 requests in 10.09s, 61.14MB read
Socket errors: connect 0, read 0, write 0, timeout 1329
Requests/sec: 47079.39
Transfer/sec: 6.06MB
Elixir cowboy:
sea@sea:~/http_test$ wrk -t10 -c1000 -d10s http://127.0.0.1:4000/
Running 10s test @ http://127.0.0.1:4000/
10 threads and 1000 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 123.00ms 303.25ms 1.94s 88.91%
Req/Sec 2.06k 1.85k 11.26k 71.80%
173220 requests in 10.09s, 26.79MB read
Socket errors: connect 0, read 0, write 0, timeout 43
Requests/sec: 17166.03
Transfer/sec: 2.65MB
Единственное, что можно было сказать в защиту Erlang/Elixir — это меньшее количество timeout'ов. Сборка приложение в HiPE не улучшило показателей. Но обо всем по порядку
Тестовое окружение v.2
Стало ясно, что тестировать лучше на отдельно стоящей системе, а не в виртуальной машине. Тестируемый http сервер было бы лучше разместить на более слабой машине, а тестирующая машина должна быть более мощной, чтобы она могла по полной завалить сервер, доведя его до предела технических возможностей. Желательно иметь быструю сеть, чтобы не упереться в ее ограничения.
Поэтому, в качестве тестирующей машины я решил оставить свой ноутбук на i7. А в качестве сервера решил помучать Orange PI One. Я предположил, что я скорее "упрусь" в ее производительность, чем в ограничение скорости обмена по сети. Orange PI One подключена к роутеру по UTP со скоростью 100 Мбит/с.
На сайте производителя указано, что на Orange PI One установлен процессор A7 Quad Core с частотой 1200 МГц. Но из за ошибок разработчиков, вся система страдает от алертов ядра по перегреву, поэтому, я зажал скорость работы процессора до 600 МГц. Так будет еще интереснее. Система работает стабильно, но даже с ничего не делая, ее load average: 2.00, 2.01, 2.05. Установлена Ubuntu 14. Памяти 512Мб, поэтому, на всякий случай я подключил swap раздел в файл на флешке.
Для того, чтобы перебросить проект на Go и на Elixir на Orange PI, я сразу создал два проекта на Github:
https://github.com/UA3MQJ/go-small-http
https://github.com/UA3MQJ/elx-small-http-cowboy
Golang на Orange PI поставился без проблем. А вот с Erlang/Elixir пришлось немного поработать. Но эта работа была проведена уже давно. Сборка проектов и запуск прошли без проблем. В качестве тестов я взял инструмент, который будет работать под Windows — это Jmeter.
Первые же тесты при следующих параметрах:
показали, что силы… равны!
RPS — Go:
RPS — Elixir:
Resp Time — Go:
Resp Time — Elixir:
Интересные наблюдения
Go всегда работало с одним ядром:
В то время, как Elixir со всеми сразу:
В этом сферическом тесте получилось так, что Elixir выиграл.
Go:
sea@sea:~$ wrk -t10 -c100 -d10s http://192.168.1.16:4000/
Running 10s test @ http://192.168.1.16:4000/
10 threads and 100 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 19.04ms 7.70ms 81.05ms 70.53%
Req/Sec 531.09 78.11 828.00 77.10%
52940 requests in 10.02s, 6.82MB read
Requests/sec: 5282.81
Transfer/sec: 696.46KB
Elixir cowboy:
sea@sea:~$ wrk -t10 -c100 -d10s http://192.168.1.16:4000/
Running 10s test @ http://192.168.1.16:4000/
10 threads and 100 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 14.27ms 10.54ms 153.60ms 95.81%
Req/Sec 753.20 103.47 1.09k 80.40%
74574 requests in 10.04s, 11.53MB read
Requests/sec: 7429.95
Transfer/sec: 1.15MB
Go:
sea@sea:~$ wrk -t100 -c100 -d10s http://192.168.1.16:4000/
Running 10s test @ http://192.168.1.16:4000/
100 threads and 100 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 60.14ms 137.57ms 1.52s 94.28%
Req/Sec 38.45 20.62 130.00 60.30%
34384 requests in 10.10s, 4.43MB read
Requests/sec: 3404.19
Transfer/sec: 448.79KB
Elixir cowboy:
sea@sea:~$ wrk -t100 -c100 -d10s http://192.168.1.16:4000/
Running 10s test @ http://192.168.1.16:4000/
100 threads and 100 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 13.32ms 5.25ms 90.37ms 73.31%
Req/Sec 75.51 22.04 191.00 67.49%
75878 requests in 10.10s, 11.74MB read
Requests/sec: 7512.75
Transfer/sec: 1.16MB
Go:
sea@sea:~$ wrk -t100 -c500 -d10s http://192.168.1.16:4000/
Running 10s test @ http://192.168.1.16:4000/
100 threads and 500 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 93.81ms 18.63ms 328.78ms 84.98%
Req/Sec 53.13 11.12 101.00 77.60%
52819 requests in 10.10s, 6.80MB read
Requests/sec: 5232.01
Transfer/sec: 689.77KB
Elixir cowboy:
sea@sea:~$ wrk -t100 -c500 -d10s http://192.168.1.16:4000/
Running 10s test @ http://192.168.1.16:4000/
100 threads and 500 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 93.24ms 96.80ms 1.26s 94.47%
Req/Sec 62.95 23.33 292.00 79.87%
61646 requests in 10.10s, 9.53MB read
Requests/sec: 6106.38
Transfer/sec: 0.94MB
А что, если запустить ерланговый сервер с использованием HiPE?
Для этого сначала нужно нагуглить, как это сделать. Как это сделать в Erlang — ясное дело. Но про Elixir пришлось "погуглить". К тому же, опробовавшие HiPE пишут, что часто в HiPE получается даже медленее, чем в стандартном. Это связано и с тем, что зависимости могут быть собраны без HiPE (а надо и их собирать в том же режиме), плюс нужно оценивать системный счетчик переключений контекста, и если переключений будет много, то это отрицательно скажется на производительности и покажет худшие результаты.
Соберем зависимости проекта с компилятором HiPE
$ ERL_COMPILER_OPTIONS="[native,{hipe, [verbose, o3]}]" mix deps.compile --force
Соберем проект
$ ERL_COMPILER_OPTIONS="[native,{hipe, [verbose, o3]}]" mix compile
Тесты показали, что HiPE не дает прироста, а наоборот, показывает худшие результаты.
Elixir cowboy:
sea@sea-tpro:~$ wrk -t100 -c500 -d10s http://192.168.1.16:4000/
Running 10s test @ http://192.168.1.16:4000/
100 threads and 500 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 93.24ms 96.80ms 1.26s 94.47%
Req/Sec 62.95 23.33 292.00 79.87%
61646 requests in 10.10s, 9.53MB read
Requests/sec: 6106.38
Transfer/sec: 0.94MB
Elixir cowboy (HiPE):
sea@sea-tpro:~$ wrk -t100 -c500 -d10s http://192.168.1.16:4000/
Running 10s test @ http://192.168.1.16:4000/
100 threads and 500 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 111.84ms 160.53ms 1.89s 95.42%
Req/Sec 59.19 29.68 383.00 81.63%
56425 requests in 10.10s, 8.72MB read
Socket errors: connect 0, read 0, write 0, timeout 34
Requests/sec: 5587.39
Transfer/sec: 0.86MB
Go наносит ответный удар
Почему же Go работал в один процессор? Может быть, пакет с Go, что идет по умолчанию старой версии, когда еще Go работал на один процессор? Так и есть!
sea@OrangePI:~$ go version
go version go1.2.1 linux/arm
Придется обновить и повторить!
Собранную Go версии 1.7.3 для armv7 удалось найти по адресу https://github.com/hypriot/golang-armbuilds/releases
Сервер на Golang, собранный на этой версии, загружает уже все 4 ядра:
Elixir cowboy:
sea@sea:~$ wrk -t100 -c500 -d10s http://192.168.1.16:4000/
Running 10s test @ http://192.168.1.16:4000/
100 threads and 500 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 93.24ms 96.80ms 1.26s 94.47%
Req/Sec 62.95 23.33 292.00 79.87%
61646 requests in 10.10s, 9.53MB read
Requests/sec: 6106.38
Transfer/sec: 0.94MB
sea@sea:~/tender_pro_bots$ wrk -t100 -c500 -d10s http://192.168.1.16:4000/
Running 10s test @ http://192.168.1.16:4000/
100 threads and 500 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 70.17ms 57.84ms 754.39ms 90.61%
Req/Sec 84.09 31.01 151.00 73.20%
78787 requests in 10.10s, 10.14MB read
Requests/sec: 7800.43
Transfer/sec: 1.00MB
Go вырывается вперед!
Go и fasthttp
После рекомендации сменить версию Golang, мне советовали fasthttp и gccgo. Начнем с первого.
https://github.com/UA3MQJ/go-small-fasthttp
Поглядим загрузку. Видим, что загружены все 4 ядра, но не на 100%.
А теперь wrk
Go fasthttp:
sea@sea:~/tender_pro_bots$ wrk -t10 -c100 -d10s http://192.168.1.16:4000/
Running 10s test @ http://192.168.1.16:4000/
10 threads and 100 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 43.56ms 85.95ms 738.51ms 89.71%
Req/Sec 676.18 351.12 1.17k 70.80%
67045 requests in 10.04s, 9.78MB read
Requests/sec: 6678.71
Transfer/sec: 0.97MB
Elixir cowboy:
sea@sea:~$ wrk -t10 -c100 -d10s http://192.168.1.16:4000/
Running 10s test @ http://192.168.1.16:4000/
10 threads and 100 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 14.27ms 10.54ms 153.60ms 95.81%
Req/Sec 753.20 103.47 1.09k 80.40%
74574 requests in 10.04s, 11.53MB read
Requests/sec: 7429.95
Transfer/sec: 1.15MB
Go fasthttp:
sea@sea:~/tender_pro_bots$ wrk -t100 -c100 -d10s http://192.168.1.16:4000/
Running 10s test @ http://192.168.1.16:4000/
100 threads and 100 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 8.95ms 3.08ms 42.23ms 75.69%
Req/Sec 112.61 16.65 320.00 70.18%
112561 requests in 10.10s, 16.42MB read
Requests/sec: 11144.39
Transfer/sec: 1.63MB
Elixir cowboy:
sea@sea:~$ wrk -t100 -c100 -d10s http://192.168.1.16:4000/
Running 10s test @ http://192.168.1.16:4000/
100 threads and 100 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 13.32ms 5.25ms 90.37ms 73.31%
Req/Sec 75.51 22.04 191.00 67.49%
75878 requests in 10.10s, 11.74MB read
Requests/sec: 7512.75
Transfer/sec: 1.16MB
Go fasthttp:
sea@sea-tpro:~/tender_pro_bots$ wrk -t100 -c500 -d10s http://192.168.1.16:4000/
Running 10s test @ http://192.168.1.16:4000/
100 threads and 500 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 46.44ms 10.69ms 327.50ms 93.21%
Req/Sec 107.71 15.10 170.00 82.06%
107349 requests in 10.10s, 15.66MB read
Requests/sec: 10627.97
Transfer/sec: 1.55MB
Elixir cowboy:
sea@sea:~$ wrk -t100 -c500 -d10s http://192.168.1.16:4000/
Running 10s test @ http://192.168.1.16:4000/
100 threads and 500 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 93.24ms 96.80ms 1.26s 94.47%
Req/Sec 62.95 23.33 292.00 79.87%
61646 requests in 10.10s, 9.53MB read
Requests/sec: 6106.38
Transfer/sec: 0.94MB
Golang вместе с fasthttp оказался быстрее самого себя и быстрее Elixir cowboy.
О правильности измерений
Если быть более внимательным, то можно выяснить, что cowboy и Go — отвечают разным количеством байт. Это связано с разными HTTP Headers'ами, которые они выдают.
Выдача go:
HTTP/1.1 200 OK
Date: Thu, 30 Mar 2017 14:37:08 GMT
Content-Length: 18
Content-Type: text/plain; charset=utf-8
Выдача cowboy:
HTTP/1.1 200 OK
server: Cowboy
date: Thu, 30 Mar 2017 14:38:17 GMT
content-length: 18
cache-control: max-age=0, private, must-revalidate
Как видим, cowboy выдает еще и дополнительную строку "server: Cowboy", что обязательно как-то сказывается на количестве переданных байт в случае с cowboy. Переданных данных получается больше.
Выводы
А выводы каждый для себя сделает свои. Go'шники порадуются за Go, а Эрлангисты и Эликсирщики — за свой продукт. Каждый останется при своем. Приверженец Erlang увидел скорость Go, которая оказалась выше не на порядок, и даже не в 2 раза (но чуть меньше), при этом он не откажется от всех возможностей Erlang даже ради 10 кратного прироста. В то же время Go'шник врядли заинтересуется Erlang, видя меньшую скорость и слышав про все возможные сложности при изучении функционального программирования.
В современном мире, время программиста стоит дорого, иногда даже больше, чем стоимость оборудования. Требуется тестирование не только в "сферических" RPS на "сферической" задаче, но и время разработки, сложность доработки и сопровождения. Экономическая целесообразность. Но иногда так хочется втопить на все лошадинные силы мегагерцы и устроить несколько заездов в отличной компании! Отлично покатались.
Автор: Алексей