Схватка Elixir и Ruby. Phoenix против Rails
Эта статья — демонстрация сравнительного анализа языков Elixir и Ruby: в ней пойдёт речь о том, какую производительность обеспечивают веб-фреймворки Phoenix и Rails при выполнении одних и тех же задач. Прежде чем перейти к примерам кода и результатам бенчмарков, ответим на несколько распространённых вопросов о тестах такого рода.
Если коротко, то Phoenix справился с заданием в 10,63 раза быстрее, чем Rails, обеспечив более низкую загрузку CPU.
ЧАВО
Не думаете ли вы, что пытаетесь сравнить несравнимое?
Вовсе нет. Приведённые тесты — прямое сопоставление полюбившихся особенностей Ruby (Rails) с Elixir (Phoenix). Elixir гарантирует предоставить то, что все так ценят в Ruby: скорость разработки, метапрограммирование, изящные API и DSL, но при этом работать быстрее, обеспечивая надёжную конкурентную модель и распределённую обработку данных. Цель данной статьи — выяснить, как уже полюбившиеся особенности Ruby без потери удобства программных интерфейсов реализуются в Elixir с точки зрения производительности веб-фреймворков.
Так ли необходимы бенчмарки?
Бенчмарки — не что иное, как средство заблаговременного обеспечения надёжности работы тестируемых программ. Но даже в этом случае бенчмарки всего лишь позволяют получить представление о каких-либо характеристиках. Поэтому мораль здесь такова: не стоит полагаться на бенчмарки, нужно всегда проводить проверки вручную.
Что конкретно вы сравниваете?
Elixir Phoenix Framework
- Phoenix 0.3.1
- Cowboy webserver (single Elixir node)
- Erlang 17.1
- Ruby on Rails
Rails 4.0.4
- Rails 4.0.4
- Puma webserver (4 воркера — по одному на каждое ядро)
- MRI Ruby 2.1.0
Мы проводим сравнение быстродействия двух эквивалентных приложений на Phoenix и на Rails, причём для большей наглядности определённые задачи рассматриваются отдельно. Для оценки выполним следующие пункты.
-
Сопоставим запрос к серверу и направим его к соответствующему действию контроллера, разбирая именованные параметры
-
Внутри контроллера рендерим представление (в зависимости от заголовка Accept в запросе), которое помещается в родительский макет. Представления рендерятся посредством встроенных в язык шаблонизаторов (ERB, EEx).
-
В представлении рендерим коллекцию данных, полученных из контроллера.
-
Отправляем ответ клиенту.
Вот и всё. Мы тестируем типовое сопоставление маршрутов и отображение стека представлений, что явно выходит за рамки примера «Hello World». В обоих приложениях производится рендеринг макета, представлений и их составляющих для проведения анализа реальной производительности при выполнении основной задачи веб-фреймворка. Чтобы не перегружать IO в обоих приложениях отключены кэширование представлений и логирование запросов. Для проведения бенчмаркинга мы использовали
Маршрутизаторы
Phoenix
defmodule Benchmarker.Router do
use Phoenix.Router
alias Benchmarker.Controllers
get "/:title", Controllers.Pages, :index, as: :page
end
Rails
Benchmarker::Application.routes.draw do
root to: "pages#index"
get "/:title", to: "pages#index", as: :page
end
Контроллеры
Phoenix
defmodule Benchmarker.Controllers.Pages do
use Phoenix.Controller
def index(conn, %{"title" => title}) do
render conn, "index", title: title, members: [
%{name: "Chris McCord"},
%{name: "Matt Sears"},
%{name: "David Stump"},
%{name: "Ricardo Thompson"}
]
end
end
Rails
class PagesController < ApplicationController
def index
@title = params[:title]
@members = [
{name: "Chris McCord"},
{name: "Matt Sears"},
{name: "David Stump"},
{name: "Ricardo Thompson"}
]
render "index"
end
end
Представления
Phoenix (EEx)
...
<h4>Team Members</h4>
<ul>
<%= for member <- @members do %>
<li>
<%= render "bio.html", member: member %>
</li>
<% end %>
</ul>
...
<b>Name:</b> <%= @member.name %>
Rails (ERB)
...
<h4>Team Members</h4>
<ul>
<% for member in @members do %>
<li>
<%= render partial: "bio.html", locals: {member: member} %>
</li>
<% end %>
</ul>
...
<b>Name:</b> <%= member[:name] %>
Результаты для локального сервера
Phoenix показал в 10,63 раза большую производительность и более устойчивое среднеквадратическое отклонение времени задержки. Такие результаты доказывают реальные преимущества конкурентной модели Elixir. Одна нода Elixir использует все необходимые ресурсы CPU и требуемый объём памяти, в то время как веб-сервер Puma (Rails) для реализации многопоточности создаёт по одному процессу на каждом ядре CPU.
Phoenix:
req/s: 12,120.00
Stdev: 3.35ms
Max latency: 43.30ms
Rails:
req/s: 1,140.53
Stdev: 18.96ms
Max latency: 159.43ms
Phoenix
$ mix do deps.get, compile
$ MIX_ENV=prod mix compile.protocols
$ MIX_ENV=prod elixir -pa _build/prod/consolidated -S mix phoenix.start
Running Elixir.Benchmarker.Router with Cowboy on port 4000
$ wrk -t4 -c100 -d30S --timeout 2000 "http://127.0.0.1:4000/showdown"
Running 10s test @ http://127.0.0.1:4000/showdown
4 threads and 100 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 8.31ms 3.53ms 43.30ms 79.38%
Req/Sec 3.11k 376.89 4.73k 79.83%
121202 requests in 10.00s, 254.29MB read
Requests/sec: 12120.94
Transfer/sec: 25.43MB
Rails
$ bundle
$ RACK_ENV=production bundle exec puma -w 4
[13057] Puma starting in cluster mode...
[13057] * Version 2.8.2 (ruby 2.1.0-p0), codename: Sir Edmund Percival Hillary
[13057] * Min threads: 0, max threads: 16
[13057] * Environment: production
[13057] * Process workers: 4
[13057] * Phased restart available
[13185] * Listening on tcp://0.0.0.0:9292
$ wrk -t4 -c100 -d30S --timeout 2000 "http://127.0.0.1:9292/showdown"
Running 10s test @ http://127.0.0.1:9292/showdown
4 threads and 100 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 21.67ms 18.96ms 159.43ms 85.53%
Req/Sec 449.74 413.36 1.10k 63.82%
11414 requests in 10.01s, 25.50MB read
Requests/sec: 1140.53
Transfer/sec: 2.55MB
Результаты для Heroku (1 dyno)
Phoenix оказался в 8,94 раза производительнее, снова показав намного более устойчивое среднеквадратическое отклонение времени задержки и в 3,74 раза меньшую загрузку CPU. Пытаясь получить на сервере Phoenix такую же нагрузку на CPU, как и на сервере Rails, мы столкнулись с нехваткой доступных сокетов. Возможно, Phoenix-приложение показало бы большую производительность, если бы сети клиентов имели более высокую пропускную способность. В случае использования удалённого сервера значение среднеквадратического отклонения имеет особую важность. Rails-приложение не смогло обеспечить устойчивое время отклика, показав результат времени задержки более 8 секунд. На практике, Phoenix-приложение в нагруженном состоянии должно работать намного более стабильно, чем приложение на Rails.
Phoenix:
req/s: 2,691.03
Stdev: 139.92ms
Max latency: 1.39s
Rails:
req/s: 301.36
Stdev: 2.06s
Max latency: 8.36s
Phoenix (Cold)
$ ./wrk -t12 -c800 -d30S --timeout 2000 "http://tranquil-brushlands-6459.herokuapp.com/showdown"
Running 30s test @ http://tranquil-brushlands-6459.herokuapp.com/showdown
12 threads and 800 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 317.15ms 139.55ms 970.43ms 81.12%
Req/Sec 231.43 66.07 382.00 63.92%
83240 requests in 30.00s, 174.65MB read
Socket errors: connect 0, read 1, write 0, timeout 0
Requests/sec: 2774.59
Transfer/sec: 5.82MB
Phoenix (Warm)
$ ./wrk -t12 -c800 -d180S --timeout 2000 "http://tranquil-brushlands-6459.herokuapp.com/showdown"
Running 3m test @ http://tranquil-brushlands-6459.herokuapp.com/showdown
12 threads and 800 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 318.52ms 139.92ms 1.39s 82.03%
Req/Sec 224.42 57.23 368.00 68.50%
484444 requests in 3.00m, 0.99GB read
Socket errors: connect 0, read 9, write 0, timeout 0
Requests/sec: 2691.03
Transfer/sec: 5.65MB
Загрузка
load_avg_1m=2.78
sample#memory_total=34.69MB
sample#memory_rss=33.57MB
sample#memory_cache=0.09MB
sample#memory_swap=1.03MB
sample#memory_pgpgin=204996pages sample#memory_pgpgout=196379pages
Rails (Cold)
$ ./wrk -t12 -c800 -d30S --timeout 2000 "http://dry-ocean-9525.herokuapp.com/showdown"
Running 30s test @ http://dry-ocean-9525.herokuapp.com/showdown
12 threads and 800 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 2.85s 1.33s 5.75s 65.73%
Req/Sec 22.68 7.18 61.00 69.71%
8276 requests in 30.03s, 18.70MB read
Requests/sec: 275.64
Transfer/sec: 637.86KB
Rails (Warm)
$ ./wrk -t12 -c800 -d180S --timeout 2000 "http://dry-ocean-9525.herokuapp.com/showdown"
Running 3m test @ http://dry-ocean-9525.herokuapp.com/showdown
12 threads and 800 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 3.07s 2.06s 8.36s 70.39%
Req/Sec 24.65 9.97 63.00 67.10%
54256 requests in 3.00m, 122.50MB read
Socket errors: connect 0, read 1, write 0, timeout 0
Requests/sec: 301.36
Transfer/sec: 696.77KB
Загрузка
sample#load_avg_1m=10.40
sample#memory_total=235.37MB
sample#memory_rss=235.35MB
sample#memory_cache=0.02MB
sample#memory_swap=0.00MB
sample#memory_pgpgin=66703pages
sample#memory_pgpgout=6449pages
Обобщённые результаты
Elixir сочетает в себе удобство и скорость разработки Ruby с конкурентной моделью и отказоустойчивостью Erlang. Программируя на Elixir, можно одновременно пользоваться преимуществами двух этих языков, в связи с чем я призываю вас ознакомиться с Phoenix. Придётся ещё попотеть над доработкой Phoenix, чтобы он мог сравняться с мощной экосистемой Rails, но это только начало. На этот год у нас уже большие планы.
Если вы хотели бы собственноручно провести такое же тестирование, то созданные приложения находятся в свободном доступе
За предоставленную информацию об установке приложений Heroku и тестировании удалённых серверов благодарим