Rendimiento Phoenix vs Rails

23 Jan 2019

Muchas personas afirman que Phoenix (el framework web de Elixir más usado) es más rápido que Rails (el framework web de Ruby). ¿Pero cuánto más rápido? La respuesta es por supuesto: Depende!

Lo que haré a continuación es crear dos sitios web sencillos uno en Phoenix y otro en Rails usando los generadores por defecto. Compararé su rendimiento y trataré de defender la idea que Phoenix es una buena opción por sobre Rails, incluso en aplicaciones web simples en las que las capacidades de tiempo real no sean una prioridad.

Para evitar polémicas, quiero aclarar que esto va a ser una comparación super informal. Simplemente haré pruebas y compartiré los resultados de 2 aplicaciones puntuales en mi computadora. Estos resultados pueden ser diferentes en otras computadoras y haciendo cambios en las aplicaciones, por ejemplo usando algún tipo de caché.

Usaré Rails 5.2 con Ruby 2.3 y Phoenix 1.3 con Elixir 1.6.

Click aquí para saltar el setup de las aplicaciones

Rails setup:

El primer paso es crear la aplicación web, correr los generadores y preparar la base de datos:

rails new prueba-rails -d postgresql
cd prueba-rails
rails g scaffold Articulo nombre descripcion:text contenido:text

Para ser justos en la comparación correremos el código en modo producción. Modificamos el archivo database.yml con nuestras credenciales y ejecutamos:

RAILS_ENV=production rake assets:precompile
RAILS_ENV=production rake db:create
RAILS_ENV=production rake db:migrate

Corremos el comando RAILS_ENV=production rake secret y agregamos la clave secreta al archivo config/secrets.yml.

El siguiente paso es crear 20 artículos para mostrar en el índice de artículos. Para esto, modificar el archivo seed.rb y ejecutar el comando RAILS_ENV=production rake db:seed.

DESCRIPCION = 'Lorem ipsum ultricies libero sit amet massa vulputate venenatis. Morbi lobortis elementum eros.'

CONTENIDO = 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vestibulum volutpat odio elit, id sollicitudin augue ullamcorper sit amet. Curabitur id pulvinar libero. Donec ultricies libero sit amet massa vulputate venenatis. Morbi lobortis elementum eros, eget pulvinar tortor fringilla ullamcorper. Quisque tellus neque, aliquam sit amet ultricies ac, interdum at est. Nullam lectus arcu, bibendum ac accumsan interdum, malesuada vel purus. Quisque odio erat, scelerisque at auctor ac, auctor nec est. Nulla facilisi.'

20.times do |n|
  Articulo.create(
    nombre: "Articulo #{n}",
    descripcion: DESCRIPCION,
    contenido: CONTENIDO
  )
end

Resultados:

En una terminal, ejecutar el comando rails s y en otra el comando

ab -n 1000 -c 50 http://0.0.0.0:3000/articulos

Server Software:
Server Hostname:        0.0.0.0
Server Port:            3000

Document Path:          /articulos
Document Length:        20402 bytes

Concurrency Level:      50
Time taken for tests:   6.940 seconds
Complete requests:      1000
Failed requests:        0
Total transferred:      21065000 bytes
HTML transferred:       20402000 bytes
Requests per second:    144.08 [#/sec] (mean)
Time per request:       347.024 [ms] (mean)
Time per request:       6.940 [ms] (mean, across all concurrent requests)
Transfer rate:          2963.95 [Kbytes/sec] received

Connection Times (ms)
              min  mean[+/-sd] median   max
Connect:        0    0   0.4      0       3
Processing:    83  340  32.1    344     431
Waiting:       82  339  32.2    343     431
Total:         85  340  31.9    344     432

Percentage of the requests served within a certain time (ms)
  50%    344
  66%    354
  75%    359
  80%    362
  90%    369
  95%    374
  98%    383
  99%    390
 100%    432 (longest request)

La respuesta promedio fue de 340 ms.

Phoenix setup:

El primer paso es crear nuestra aplicación web, correr los generadores y preparar la base de datos:

mix phx.new prueba_phoenix
cd prueba_phoenix
MIX_ENV=prodmix ecto.create
mix phx.gen.html CMS Articulo articulos nombre descripcion:text contenido:text
    Add the resource to your browser scope in lib/prueba_phoenix_web/router.ex:
        resources "/articulos", ArticuloController

Phoenix recomienda agregar una linea al archivo de rutas. Una vez agregada la linea, correr el comando MIX_ENV=prod mix ecto.migrate.

Modificar el archivo priv/repo/seeds.exs:

defmodule PruebaPhoenix.Seeder do
  alias PruebaPhoenix.Repo
  alias PruebaPhoenix.CMS.Articulo

  @descripcion "Lorem ipsum ultricies libero sit amet massa vulputate venenatis. Morbi lobortis elementum eros."

  @contenido "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vestibulum volutpat odio elit, id sollicitudin augue ullamcorper sit amet. Curabitur id pulvinar libero. Donec ultricies libero sit amet massa vulputate venenatis. Morbi lobortis elementum eros, eget pulvinar tortor fringilla ullamcorper. Quisque tellus neque, aliquam sit amet ultricies ac, interdum at est. Nullam lectus arcu, bibendum ac accumsan interdum, malesuada vel purus. Quisque odio erat, scelerisque at auctor ac, auctor nec est. Nulla facilisi."

  def crear_articulo(n) do
    Repo.insert!(%Articulo{
      nombre: "Articulo #{n}",
      descripcion: @descripcion,
      contenido: @contenido
    })
  end

end

(1..20) |> Enum.each(fn(n) -> PruebaPhoenix.Seeder.crear_articulo(n) end)

Crear los artículos ejecutando el comando MIX_ENV=prod PORT=4000 mix run priv/repo/seeds.exs.

Ejecutamos:

mix deps.get --only prod
MIX_ENV=prod mix compile

Precompilamos los assets:

cd assets
brunch build --production
cd ..
mix phx.digest

Resultado de Phoenix:

En una terminal:

MIX_ENV=prod PORT=4000 mix phx.server

y en otra terminal

ab -n 1000 -c 50 http://0.0.0.0:4000/articulos

Server Software:        Cowboy
Server Hostname:        0.0.0.0
Server Port:            4000

Document Path:          /articulos
Document Length:        25114 bytes

Concurrency Level:      50
Time taken for tests:   1.074 seconds
Complete requests:      1000
Failed requests:        0
Total transferred:      25616000 bytes
HTML transferred:       25114000 bytes
Requests per second:    930.75 [#/sec] (mean)
Time per request:       53.720 [ms] (mean)
Time per request:       1.074 [ms] (mean, across all concurrent requests)
Transfer rate:          23283.21 [Kbytes/sec] received

Connection Times (ms)
              min  mean[+/-sd] median   max
Connect:        0    0   0.4      0       5
Processing:    10   53  19.6     50     116
Waiting:        9   51  19.1     48     116
Total:         10   53  19.7     50     118

Percentage of the requests served within a certain time (ms)
  50%     50
  66%     59
  75%     66
  80%     70
  90%     80
  95%     89
  98%     99
  99%    103
 100%    118 (longest request)

La respuesta promedio fue de 53 ms.

Comparación

La aplicación Phoenix demoró 1 segundo en completar los 1000 llamados, con un promedio de 53 ms por respuesta. Mientras que la aplicación Rails demoró casi 7 segundos, con un promedio de 340 ms por respuesta. Es decir, la aplicación Phoenix de ejemplo corre entre 6 y 7 veces más rápido que la aplicación Rails.

Mis conclusiones

Como ya mencioné antes, esto fue sólo una pequeña prueba. Posiblemente se pueda mejorar mucho la performance de la aplicación Rails agregando algún tipo de Caché. Aunque para ser justos, eso aplica también para la aplicación Phoenix.

Si bien no creo que la velocidad sea la principal ventaja de Phoenix por sobre Rails, creo que la velocidad sí es importante: Significa una mejor experiencia para el usuario, pero además mejora la experiencia al desarrollar y ayuda a simplificar el código.

En la mayoría de las aplicaciones Rails que he construido me he visto obligado a ensuciar el código con ElasticSearch, Sidekiq, Redis, RabbitMQ y distintos tipos de caché, porque la aplicación no era lo suficientemente rápida.

Mi experiencia con Phoenix es muy distinta: usando las herramientas por defecto suelo obtener un rendimiento más que suficiente. No suelo pensar en usar otras herramientas, que agregan complejidad y puntos de fallo al sistema. En consecuencia el código termina siendo más fácil de mantener.

¡Pero reitero! Esto fue una prueba super informal. Mi consejo: hacé las pruebas por vos misma/o y animate a usar Phoenix para tu próximo proyecto! Pero te advierto: ¡Va a ser difícil!

Siempre aprender un lenguaje y un framework nuevo cuesta. Vas a encontrarte con un montón de errores nuevos, y vas a tener una gran resistencia de entrada. Recordá que la primera vez que tocaste Rails también fue dificil. Que todo lo que hoy sabés y te resulta tan claro, lo tuviste que aprender en algún momento. ¡Y te costó!

Esto también va a costar. Mi sugerencia es que arranques con un proyecto pequeño. ¿Te animás?