Tag - rails

Entries feed

Sunday, May 6 2018

HAProxy in front of Unicorn

Unicorn, the "Rack HTTP server for fast clients", is really for "fast clients". Fast clients mean clients on the same local network (or even same machine) with very low latency and near-zero chance of packets loss. Slow clients are the usual web users: far away from the server. Unicorn has been designed to serve fast clients and if you try to use it to serve slow clients then the performance may be dismal. This is why the Unicorn developers recommend to use Nginx in front of Unicorn. To know more on the Unicorn philosophy, visit this page.

I recently tried to use HAProxy in front of Unicorn and was disappointed to see that:

  • the system was slow and unresponsive
  • a lot of 502 Gateway errors popped up for seemingly no reason (and this popped up unconsistently)

I came to the conclusion that the default configuration of HAProxy was not appropriate for Unicorn. After some web digging, I discovered the "http-buffer-request" option.

Here is what the HAProxy 1.8 documentation says about the "http-buffer-request" option :

It is sometimes desirable to wait for the body of an HTTP request before taking a decision. This is what is being done by "balance url_param" for example. The first use case is to buffer requests from slow clients before connecting to the server. Another use case consists in taking the routing decision based on the request body's contents. This option placed in a frontend or backend forces the HTTP processing to wait until either the wholebody is received, or the request buffer is full, or the first chunk is complete in case of chunked encoding. It can have undesired side effects with some applications abusing HTTP by expecting unbuffered transmissions between the frontend and the backend, so this should definitely not be used by default.

It seems to be the best fit with Unicorn's philosophy! Let's activate the option in each backend corresponding to a Unicorn-run application:

backend unicorn-app
	option http-buffer-request
	server unicorn-app 1.2.3.4:3000

and restart HAProxy:

/etc/init.d/haproxy reload

Sunday, September 17 2017

PG::NotNullViolation error on id column in PostgreSQL

Faced with this error:

PG::NotNullViolation: ERROR:  null value in column "id" violates not-null constraint

it took me some time to realize that my id column was not a serial but a plain integer...

To fix this, just execute:

ALTER TABLE my_table DROP COLUMN id;
ALTER TABLE my_table ADD COLUMN id SERIAL PRIMARY KEY;

Saturday, October 22 2016

Init script for Rails app served by Unicorn with RVM

Here is an init script for a Rails app served by Unicorn with RVM.

#!/bin/bash
### BEGIN INIT INFO
# Provides:          APP_NAME, with Unicorn serving
# Required-Start:    $all
# Required-Stop:     $network $local_fs $syslog
# Default-Start:     2 3 4 5
# Default-Stop:      0 1 6
# Short-Description: Start APP_NAME unicorn at boot
# Description:       Enable APP_NAME at boot time.
### END INIT INFO

set -u
set -e

# Change these to match your app:
APP_NAME="APP_NAME"
APP_ROOT="/path/to/app"
PID="/path/to/app/tmp/pids/unicorn.pid"
ENV="production"
RVM="2.3.0@gemset"
USER="user"

UNICORN_OPTS="-D -E $ENV -c $APP_ROOT/config/unicorn.rb"

SET_PATH="cd $APP_ROOT; rvm use $RVM > /dev/null;"
CMD="$SET_PATH unicorn $UNICORN_OPTS"

old_pid="$PID.oldbin"

cd $APP_ROOT || exit 1

sig () {
	test -s "$PID" && kill -$1 `cat $PID`
}

oldsig () {
	test -s $old_pid && kill -$1 `cat $old_pid`
}

start () {
        echo "Starting $APP_NAME unicorn..."
        sig 0 && echo -e >&2 "\e[31mAlready running" && exit 0
        su - $USER -c "$CMD" > /dev/null
        echo -e "$APP_NAME has \e[32mstarted\e[0m, PID is `cat $PID`"
}

stop () {
        echo "Stopping $APP_NAME unicorn (signal QUIT)..."
        sig QUIT && echo -e "$APP_NAME has \e[32mstopped" && exit 0
        echo >&2 "Not running"
}

case ${1-help} in
start)
	start
	;;
stop)
	stop
	;;
force-stop)
	echo "Force stopping $APP_NAME unicorn (signal TERM)..."
	sig TERM && echo "$APP_NAME has stopped" &&exit 0
	echo -e >&2 "\e[31mNot running"
	;;
reload)
	echo "Reloading $APP_NAME unicorn (signal USR2)..."
	sig USR2 && echo -e "$APP_NAME has \e[32mreloaded" && exit 0
	echo -e >&2 "\e[31mCouldn't reload\e[0m, starting instead"
	start
	;;
status)
	sig 0 && echo -e "$APP_NAME \e[32mrunning\e[0m with PID `cat $PID`" && exit 0
	echo -e "$APP_NAME is \e[31mnot running!"
	;;
*)
 	echo >&2 "Usage: $0 <start|stop|reload|status|force-stop>"
 	exit 0
 	;;
esac

Beware that the reload calls USR2. In the documentation of Unicorn, it is said that USR2 should be followed by QUIT. Otherwise the good signal to reload is HUP.

However, I use the following unicorn.rb config file inspired from Github's and you will see that it specifies that the new instance of Unicorn shall send a QUIT signal to the old one!

rails_root = "/path/to/app"
rails_env = ENV['RAILS_ENV'] || 'production'

# 4 workers, may be changed to 1 for the tests
worker_processes (rails_env == 'production' ? 4 : 1) 

# Load rails+app into the master before forking workers
# for super-fast worker spawn times
preload_app true

# Restart any workers that haven't responded in 30 seconds
timeout 30

before_fork do |server, worker|
  ##
  # When sent a USR2, Unicorn will suffix its pidfile with .oldbin and
  # immediately start loading up a new version of itself (loaded with a new
  # version of our app). When this new Unicorn is completely loaded
  # it will begin spawning workers. The first worker spawned will check to
  # see if an .oldbin pidfile exists. If so, this means we've just booted up
  # a new Unicorn and need to tell the old one that it can now die. To do so
  # we send it a QUIT.
  # Using this method we get 0 downtime deploys.

  old_pid = rails_root + '/tmp/pids/unicorn.pid.oldbin'
  if File.exists?(old_pid) && server.pid != old_pid
    begin
      Process.kill("QUIT", File.read(old_pid).to_i)
    rescue Errno::ENOENT, Errno::ESRCH
      # someone else did our job for us
    end
  end
end

after_fork do |server, worker|
  # Unicorn master loads the app then forks off workers - because of the way
  # Unix forking works, we need to make sure we aren't using any of the parent's
  # sockets, e.g. db connection
  ActiveRecord::Base.establish_connection
end

Sunday, May 17 2015

Rename a file before saving with Paperclip

I was recently trying to rename a file before having it saved by Paperclip. As it was not obivous to me, I would like to share the solution with you.

In the model file with the paperclip "definitions", let's add:

before_post_process :rename_file

and (in my case, the paperclip attachement name is "file")

def rename_file
  self.file.instance_write :file_name, function_to_change_a_string(file_file_name)
end

And it should work. Easy once found :-)

Saturday, May 16 2015

Unicorn vs Thin pour propulser une application Ruby on Rails, stress test pour choisir !

Unicorn et Thin sont deux serveurs pour Ruby assez fréquemment utilisés et cités dans la littérature informatique. A titre d'exemple, Unicorn est utilisé notamment chez Github: une bonne référence ! Thin est souvent cité pour sa robustesse et sa légèreté. Lequel choisir ?

Avant d'envisager le déploiement d'une application en production (prévision de charge : quelques centaines d'utilisateurs simultanément), j'ai souhaité les comparer.

Protocole de test

  • Linux Debian Jessie, Core M 5Y10, 4 Go de RAM, disque dur SSD ; les tests sont effectués en local sur la machine et le réseau n'intervient donc pas comme facteur dans le test.
  • Le stress test est réalisé avec Gatling (dont j'ai déjà décrit le fonctionnement ici) qui simule l'arrivée de 200 utilisateurs en 120 secondes sur l'application, chaque utilisateur exécute 8 requêtes sur l'application.
  • L'application Ruby on Rails (Rails 4.2.1, Ruby 2.2.2) est prête à l'emploi en environnement de production, les 2 gems Thin et Unicorn sont installés et fonctionnels.
  • Quand cela est nécessaire, Pound est utilisé comme "load balancer".
  • Thin est exécuté avec la commande :
thin -s 4 -e production -p 8080 start

ou, quand on le lance avec un seu

thin -e production -p 8080 start
  • Unicorn est exécuté par la commande :
unicorn -c config/unicorn.rb -E production -p 8080

avec pour contenu du fichier de config unicorn.rb une configuration très proche de celle proposée par Github sur cet article :

rails_root = "/path/to/app"
rails_env = ENV['RAILS_ENV'] || 'production'

# 4 workers, may be changed to 1 for the tests
worker_processes (rails_env == 'production' ? 4 : 1) 

# Load rails+app into the master before forking workers
# for super-fast worker spawn times
preload_app true

# Restart any workers that haven't responded in 30 seconds
timeout 30

before_fork do |server, worker|
  ##
  # When sent a USR2, Unicorn will suffix its pidfile with .oldbin and
  # immediately start loading up a new version of itself (loaded with a new
  # version of our app). When this new Unicorn is completely loaded
  # it will begin spawning workers. The first worker spawned will check to
  # see if an .oldbin pidfile exists. If so, this means we've just booted up
  # a new Unicorn and need to tell the old one that it can now die. To do so
  # we send it a QUIT.
  # Using this method we get 0 downtime deploys.

  old_pid = rails_root + '/tmp/pids/unicorn.pid.oldbin'
  if File.exists?(old_pid) && server.pid != old_pid
    begin
      Process.kill("QUIT", File.read(old_pid).to_i)
    rescue Errno::ENOENT, Errno::ESRCH
      # someone else did our job for us
    end
  end
end

after_fork do |server, worker|
  # Unicorn master loads the app then forks off workers - because of the way
  # Unix forking works, we need to make sure we aren't using any of the parent's
  # sockets, e.g. db connection
  ActiveRecord::Base.establish_connection
end

Les résultats

Match 1 : Unicorn vs Thin, avec un seul processus pour chacun

Quand un seul processus (un seul "worker" dans le vocable propre à Unicorn) est activé, l'arrivée ininterrompue de nouveaux utilisateurs charge considérablement l'application...

Voici les résultats du "stress test" : benchmark_1process.png

On observe que :

  • Thin parvient à servir tout le monde, mais au prix de temps de réponse assez longs
  • Les temps de réponse sont un petit plus courts avec Unicorn mais en revanche Unicorn échoue à servir un certain nombre d'utilisateurs - le résultat du second graphe montre que l'unique worker d'Unicorn arrête de répondre aux requêtes à plusieurs reprises et pendant des durées assez longues... je n'explique pas bien ce comportement... J'ai pensé à l'impact du paramètre 'timeout 30' mais je n'ai pas observé de différence lorsque j'ai porté la valeur du timeout à 300 (j'ai même observé une amplification du phénomène)...
  • Thin semble donc vainqueur sur ce match avec un seul processus... mais voyons vite avec plusieurs processus ce qui sera plus proche d'un déploiement réel !

Match 2 : Unicorn vs Thin, avec 4 processus pour chacun

Nous démarrons cette fois Unicorn avec 4 "workers" et 4 instances de Thin. Afin d'accéder de manière aléatoire aux différentes instances de Thin, nous utilisons cette fois Pound comme "load balancer". Pound a également été utilisé avec Unicorn pour rendre les conditions des tests équivalentes. Pound n'est toutefois "en théorie" pas nécessaire pour Unicorn qui répartit automatiquement les requêtes sur les différents "workers".

Place aux résultats : benchmark-4processus.png

On constate que :

  • Thin et Unicorn parviennent à servir tous les utilisateurs plus efficacement (normal, la multiplication des processus profite des plusieurs coeurs de la machine hôte)
  • Mais Unicorn est plus véloce que Thin : 97% des requêtes sont servies en moins de 800ms par Unicorn alors que seules 63% sont servies dans le même délai par Unicorn
  • On remarque également que les 4 processus d'Unicorn jugulent le flux des nouveaux utilisateurs avec au pic 24 utilisateurs en attente là où Thin faiblit peu à peu jusqu'à atteindre un pic à plus de 50 utilisateurs en attente dans l'application (pour rappel, dans le scénario testé, 200 utilisateurs se connectent régulièrement au cours des 120 secondes à l'application et exécutent 8 requêtes)
  • C'est donc une victoire d'Unicorn qui se tire mieux cette fois de la situation !

Conclusion

Je suis tenté de préférer Unicorn pour servir un site Ruby on Rails chargé. Cependant, il conviendra d'offrir suffisamment de workers à Unicorn pour qu'il puisse faire le travail correctement !

Quelques enseignements annexes :

L'impact de Pound sur les temps de réponse

Sans Pound, le temps de réponse moyen de Unicorn est de 117 ms. Avec Pound, toute autre chose étant égale par ailleurs, le temps de réponse moyen est de 161 ms. Il semble donc que le passage par Pound augmente le temps moyen de réponse d'environ 40 ms.

La multiplication des workers pour Unicorn : bonne ou mauvaise idée ?

La machine de test dispose de 4 coeurs. J'ai testé 4 workers et 16 workers avec Unicorn. Les résultats sont très similaires : respectivement 131 et 130 secondes pour effectuer le scénario de test complet. Les temps de réponse moyens sont là encore très proches : 113 ms vs 117 ms. Je ne pense pas que les différences constatées soient significatives. Les écart-types sur le temps de réponse diffèrent plus : 216 ms avec 4 workers et 265 ms pour 16 workers... Compte-tenu du nombre de coeurs sur la machine de test (4), l'utilisation de 16 workers ne semble pas permettre des gains substantiels. Un petit excès du nombre de workers par rapport au nombre de coeurs est peut-être à conseiller toutefois.

Tuesday, March 10 2015

Rails 4.2, Thin et "Internal server error"

Un récent appel à une application développée avec Rails 4.2, et servie par Thin, m'a conduit à une erreur "500 Internal Server Error" dont je ne trouvais de trace dans aucun log de l'application, et aucun log pertinent pour le "load balancer" (pound) placé frontalement au-dessus de cette application.

Le plus étrange était de ne voir aucune trace dans les logs de Rails et notamment de ne voir aucune confirmation de la bonne réception de la requête par l'application Rails.

Après avoir exploré, ad nauseum, tous les potentiels blocages en amont de l'application Rails, j'ai fini par remplacer Thin par Webrick pour servir l'application : et là miracle, un message d'erreur plus explicite m'a été retourné en lieu et place de l'erreur 500 : "Unexpected error while processing request: Too many open files - Maximum file multiparts in content reached".

J'ai alors trouvé cette référence : http://stackoverflow.com/questions/27773368/rails-4-2-internal-server-error-with-maximum-file-multiparts-in-content-reached et celle-ci : https://github.com/rack/rack/pull/814

La première propose de contourner le problème en désactivant la limite sur le nombre d'éléments multipart en ajoutant cette ligne dans un fichier d'initialisation :

Rack::Utils.multipart_part_limit = 0

La seconde référence étudie la raison de ce comportement avec une mauvaise approche du comptage des balises dans le formulaire : "However, our form only had one file input. It had several hundred other form elements. Each was counted as a file.". Le commit en faute a été repéré et cela devrait être corrigé dans la prochaine version de Rails/Rake !

Sunday, August 17 2014

Nouvelle version d'atom (0.124) compilée pour Debian Jessie

Je viens de compiler la dernière version d'atom, l'éditeur de texte évolué créé par les équipes de GitHub. La version 0.124 est donc disponible sous la forme d'un paquet Debian : atom-0.124.0-amd64.deb.

Ce paquet devrait fonctionner sur Debian Jessie 64 bits.

Il a été généré en suivant les instructions de compuilation officielles et certains pré-requis doivent peut-être être remplis sur des machines qui n'ont jamais été confrontées à atom.

Bon téléchargement !

Thursday, March 27 2014

Ordonner à Rails d'accepter un certificat auto-signé

Par défaut, l'implémentation OpenSSL dans Rails interdit (et c'est tout à fait normal !) les certificats dont l'identité ne peut être vérifiée. C'est ennuyeux si vous utilisez (par exemple pour des tests ou en développement) des certificats auto-signés car alors impossible de récupérer les données convoitées par exemple sur votre serveur HTTPS ou bien sur votre IMAPS.

Voilà l'erreur que OpenSSL doit retourner dans ce cas :

SSL_connect returned=1 errno=0 state=SSLv3 read server certificate B: certificate verify failed

Pour contourner ce problème, on pourra inclure le code suivant dans son application :

require 'openssl'
OpenSSL::SSL::VERIFY_PEER = OpenSSL::SSL::VERIFY_NONE

Pour une application supportée par Rails, on pourra par exemple inclure ceci dans les "initializers", par ex. dans /config/initializers/bypass_ssl_identity_check.rb.

Saturday, January 18 2014

RVM et ruby regrettent l'absence de libyaml

Ce matin, après une installation fraîche de Ruby 1.9.3 et 2.1.0 avec RVM, j'avais la désagréble surprise de lire :

It seems your ruby installation is missing psych (for YAML output).
To eliminate this warning, please install libyaml and reinstall your ruby.
ERROR:  Loading command: install (LoadError)
	cannot load such file -- zlib
ERROR:  While executing gem ... (NoMethodError)
    undefined method `invoke_with_build_args' for nil:NilClass

Je dus exécuter les commandes suivantes : (i) forcer l'installation de libyaml par RVM

rvm pkg install libyaml

(ii) réinstaller les rubies installés :

rvm reinstall all --force

Wednesday, December 26 2012

ActiveRecord et le gem mysql2 rouspètent

Je rencontrai dernièrement un petit souci lors de la migration d'une application Rails depuis 3.0.1 vers 3.2.9 (la version stable la plus récente alors que j'écris ces lignes). En effet, au cours de la migration, j'étais invité à remplacer le gem mysql par mysql2 (donné comme plus rapide) et à ajouter le gem activerecord-mysql2-adapter.

Je m'exécutai et modifier le Gemfile en ce sens, mais au moment de lancer l'application - et de lancer des commandes sur la base de données via Rake - j'obtenais :

stack trace:
rake aborted!
undefined method accept' for nil:NilClass
/home/pab/.rvm/gems/ruby-1.9.3-p327@global/gems/activerecord-3.2.9/lib/active_record/connection_adapters/abstract/database_statements.rb:7:into_sql'

Et impossible d'aller plus loin...

Heureusement, les fils de discussion GitHub ont volé à mon secours : https://github.com/rails/rails/issues/7295

@nstuart yes, as @rafaelfranca says, you need to remove activerecord-mysql2-adapter from your Gemfile. The mysql2 adapter ships with Rails 3.1 and up, so you don't need this gem (in fact, that gem is causing your error).

et je comprenais qu'il fallait supprimer la ligne gem 'activerecord-mysql2-adapter' du Gemfile. Après une exécution de 'bundle install', tout rentra dans l'ordre.

Sunday, March 4 2012

Configurer Rails mailer

Rails 3 est arrivé avec tout le nécessaire pour envoyer des courriels facilement.

La configuration s'effectue dans les fichiers d'environnement : pour un paramétrage fonctionnel dans l'environnement de développement on se concentrera sur le fichier /config/environments/development.rb.

Si l'envoi est effectué par un smtp externe :

  #Settings for send emails
  config.action_mailer.delivery_method = :smtp
  config.action_mailer.smtp_settings = {
    :address => "smtp.myhost.tld",
    :port => 25,
    :domain => "emetteur.fr",
    :authentication => "plain",
    :user_name => "utilisateur",
    :password => "motdepasse"
  }

Si l'envoi est effectué par sendmail :

  config.action_mailer.delivery_method = :sendmail
  config.action_mailer.sendmail_settings = { 
    :location       => '/usr/sbin/sendmail', 
    :arguments      => '-i -t'
  }

La seconde méthode fonctionne avec le petit utilitaire 'sendmail' fourni avec nullmailer (pas un vrai 'sendmail', une émulation qui injecte le courriel dans la queue de nullmailer).