Простейший HTTP сервер на Golang и Elixir. Сравнение производительности

от автора

image
Пару недель назад, я решил взять простейший пример 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 vs cowboy wrk -t10 -c100 -d10s http://127.0.0.1:4000/

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 vs cowboy wrk -t10 -c1000 -d10s http://127.0.0.1:4000/

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 Мбит/с.

image

На сайте производителя указано, что на Orange PI One установлен процессор A7 Quad Core с частотой 1200 МГц. Но из за ошибок разработчиков, вся система страдает от алертов ядра по перегреву, поэтому, я зажал скорость работы процессора до 600 МГц. Так будет еще интереснее. Система работает стабильно, но даже с ничего не делая, ее load average: 2.00, 2.01, 2.05. Установлена Ubuntu 14. Памяти 512Мб, поэтому, на всякий случай я подключил swap раздел в файл на флешке.

image<h4>Раунд 2</h4>

Для того, чтобы перебросить проект на 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.

Первые же тесты при следующих параметрах:

image

показали, что силы… равны!

RPS — Go:
image

RPS — Elixir:
image

Resp Time — Go:
image

Resp Time — Elixir:
image

Интересные наблюдения

Go всегда работало с одним ядром:
image

В то время, как Elixir со всеми сразу:
image

В этом сферическом тесте получилось так, что Elixir выиграл.

go vs cowboy wrk -t10 -c100 -d10s http://192.168.1.16:4000/

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 vs cowboy wrk -t100 -c100 -d10s http://192.168.1.16:4000/

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 vs cowboy wrk -t100 -c500 -d10s http://192.168.1.16:4000/

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

cowboy vs cowboy(HiPE) wrk -t100 -c500 -d10s http://192.168.1.16:4000/

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 ядра:
image

cowboy vs Go1.7.4 wrk -t100 -c500 -d10s http://192.168.1.16:4000/

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%.
image

А теперь wrk

go vs cowboy wrk -t10 -c100 -d10s http://192.168.1.16:4000/

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 vs cowboy wrk -t100 -c100 -d10s http://192.168.1.16:4000/

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 vs cowboy wrk -t100 -c500 -d10s http://192.168.1.16:4000/

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 на "сферической" задаче, но и время разработки, сложность доработки и сопровождения. Экономическая целесообразность. Но иногда так хочется втопить на все лошадинные силы мегагерцы и устроить несколько заездов в отличной компании! Отлично покатались.

image

ссылка на оригинал статьи https://habrahabr.ru/post/324818/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *