Recherche de site Web

Comment optimiser les travailleurs Unicorn dans une application Ruby on Rails


Présentation de la Licorne

Si vous êtes développeur Rails, vous avez probablement entendu parler de Unicorn, un serveur HTTP capable de gérer plusieurs requêtes simultanément.

Unicorn utilise des processus forkés pour atteindre la concurrence. Comme les processus forkés sont essentiellement des copies les uns des autres, cela signifie que l'application Rails n'a pas besoin d'être sûre pour les threads.

C'est génial car il est difficile de garantir que notre propre code soit sûr pour les threads. Si nous ne pouvons pas garantir que notre code soit sûr pour les threads, alors les serveurs web concurrents tels que Puma et même les implémentations alternatives de Ruby qui exploitent la concurrence et le parallélisme comme JRuby et Rubinius seraient exclus.

Par conséquent, Unicorn offre à nos applications Rails la concurrence même lorsqu'elles ne sont pas sûres pour les threads. Cependant, cela a un coût. Les applications Rails exécutées sur Unicorn ont tendance à consommer beaucoup plus de mémoire. Sans prêter attention à la consommation de mémoire de votre application, vous pourriez bien vous retrouver avec un serveur cloud surchargé.

Dans cet article, nous explorerons quelques façons d'exploiter la concurrence de Unicorn, tout en contrôlant la consommation de mémoire.

Utilisez Ruby 2.0 !

Si vous utilisez Ruby 1.9, vous devriez sérieusement envisager de passer à Ruby 2.0. Pour comprendre pourquoi, nous devons comprendre un peu le concept de fork.

Fourche et Copie sur écriture (CoW)

Lorsqu'un processus enfant est créé par fork, il est exactement la même copie que le processus parent. Cependant, il n'est pas nécessaire de copier la mémoire physique réelle. Puisqu'ils sont des copies exactes, les deux processus, enfant et parent, peuvent partager la même mémoire physique. Ce n'est que lorsqu'une écriture est effectuée que nous copions le processus enfant dans la mémoire physique.

Alors, comment cela se rapporte-t-il à Ruby 1.9/2.0 et Unicorn ?

Rappelez-vous que Unicorn utilise le forking. En théorie, le système d'exploitation pourrait tirer parti de CoW. Malheureusement, Ruby 1.9 ne le permet pas. Plus précisément, l'implémentation du ramassage de déchets de Ruby 1.9 ne le permet pas. Une version extrêmement simplifiée est la suivante — lorsque le ramasse-miettes de Ruby 1.9 se déclenche, une écriture aurait déjà été effectuée, rendant ainsi CoW inutile.

Sans entrer dans trop de détails, il suffit de dire que le ramasse-miettes de Ruby 2.0 corrige cela, et nous pouvons maintenant exploiter CoW.

Ajustement de la configuration de Unicorn

Il y a quelques paramètres que nous pouvons ajuster dans config/unicorn.rb pour tirer le maximum de performance de Unicorn.

worker_processes

Cela définit le nombre de processus de travail à lancer. Il est important de savoir combien de mémoire un processus prend. Cela afin que vous puissiez budgétiser en toute sécurité le nombre de travailleurs, afin de ne pas épuiser la RAM de votre VPS.

délai d'attente

Cela devrait être réglé sur un petit nombre : généralement, 15 à 30 secondes est un nombre raisonnable. Cette configuration définit la durée avant qu'un travailleur ne dépasse le temps imparti. La raison pour laquelle vous souhaitez définir un nombre relativement faible est d'empêcher une requête longue de retarder le traitement des autres requêtes.

preload_app

Cela devrait être défini sur true. Le définir sur true réduit le temps de démarrage des processus workers Unicorn. Cela utilise CoW pour précharger l'application avant de forker d'autres processus workers. Cependant, il y a un gros piège. Nous devons faire attention à ce que tous les sockets (comme les connexions à la base de données) soient correctement fermés et rouverts. Nous faisons cela en utilisant before_fork et after_fork.

Voici un exemple :

before_fork do |server, worker|
  # Disconnect since the database connection will not carry over
  if defined? ActiveRecord::Base
    ActiveRecord::Base.connection.disconnect!
  end
  
  if defined?(Resque)
    Resque.redis.quit
    Rails.logger.info('Disconnected from Redis')
  end
end

after_fork do |server, worker|
  # Start up the database connection again in the worker
  if defined?(ActiveRecord::Base)
    ActiveRecord::Base.establish_connection
  end
  
  if defined?(Resque)
    Resque.redis = ENV['REDIS_URI']
    Rails.logger.info('Connected to Redis')
  end
end

Dans cet exemple, nous nous assurons que la connexion est fermée et rouverte lorsque les travailleurs sont forkés. En plus des connexions à la base de données, nous devons nous assurer que les autres connexions nécessitant des sockets sont traitées de manière similaire. Ce qui précède inclut la configuration pour Resque.

Maîtriser la consommation de mémoire de vos travailleurs licornes

Évidemment, tout n'est pas rose et plein de licornes (jeu de mots voulu !). Si votre application Rails fuit de la mémoire, Unicorn va empirer les choses.

Chacun de ces processus forkés consomme de la mémoire, car ils sont des copies de l'application Rails. Par conséquent, bien avoir plus de workers signifierait que notre application pourrait gérer plus de requêtes entrantes, nous sommes limités par la quantité de RAM physique disponible sur notre système.

Il est facile pour une application Rails de fuir de la mémoire. Même si nous parvenons à combler toutes les fuites de mémoire, il reste encore le ramassage des déchets moins qu'idéal à gérer (je fais référence à l'implémentation MRI).

Ce qui précède montre une application Rails exécutant Unicorn avec des fuites de mémoire.

Au fil du temps, la consommation de mémoire continuera de croître. Utiliser plusieurs travailleurs Unicorn ne ferait qu'accélérer le rythme auquel la mémoire est consommée, jusqu'au point où il n'y aura plus de RAM disponible. L'application s'arrêterait alors — entraînant des foules d'utilisateurs et de clients mécontents.

Il est important de noter que ce n'est pas la faute de Unicorn. Cependant, c'est un problème auquel vous seriez confronté tôt ou tard.

Entrez dans le Tueur de Licornes Employé

L'une des solutions les plus simples que j'aie rencontrées est la gem unicorn-worker-killer.

Depuis le README :

La gem unicorn-worker-killer fournit le redémarrage automatique des workers Unicorn basé sur

  1. nombre maximum de requêtes, et
  2. taille de la mémoire du processus (RSS), sans affecter aucune requête.

Cela améliorera considérablement la stabilité du site en évitant une exhaustion de mémoire inattendue aux nœuds de l'application.

Notez que je suppose que vous avez déjà Unicorn installé et en fonctionnement.

Étape 1 :

Ajoutez unicorn-worker-killer à votre Gemfile. Placez-le en dessous de la gem unicorn.

group :production do 
  gem 'unicorn'
  gem 'unicorn-worker-killer'
end

Étape 2 :

Exécutez bundle install.

Étape 3 :

Voici la partie amusante. Localisez et ouvrez votre fichier config.ru.

# --- Start of unicorn worker killer code ---

if ENV['RAILS_ENV'] == 'production' 
  require 'unicorn/worker_killer'

  max_request_min =  500
  max_request_max =  600

  # Max requests per worker
  use Unicorn::WorkerKiller::MaxRequests, max_request_min, max_request_max

  oom_min = (240) * (1024**2)
  oom_max = (260) * (1024**2)

  # Max memory size (RSS) per worker
  use Unicorn::WorkerKiller::Oom, oom_min, oom_max
end

# --- End of unicorn worker killer code ---

require ::File.expand_path('../config/environment',  __FILE__)
run YourApp::Application

Tout d'abord, nous vérifions que nous sommes dans l'environnement production. Si c'est le cas, nous procéderons à l'exécution du code qui suit.

unicorn-worker-killer tue les workers sous deux conditions : nombre maximum de requêtes et mémoire maximale.

Demandes Max

Dans cet exemple, un travailleur est tué s'il a traité entre 500 et 600 requêtes. Remarquez qu'il s'agit d'une plage. Cela minimise les occurrences où plus d'un travailleur est terminé simultanément.

Mémoire Max

Ici, un travailleur est tué s'il consomme entre 240 et 260 Mo de mémoire. C'est une plage pour la même raison que ci-dessus.

Chaque application a des besoins en mémoire uniques. Vous devriez avoir une estimation approximative de la consommation de mémoire de votre application pendant son fonctionnement normal. De cette façon, vous pourriez donner une meilleure estimation de la consommation minimale et maximale de mémoire pour vos travailleurs.

Une fois que vous avez tout configuré correctement, lors du déploiement de votre application, vous remarquerez un comportement de mémoire beaucoup moins erratique :

Remarquez les coins dans le graphique. C'est le bijou qui fait son travail !

Conclusion

Unicorn offre à votre application Rails un moyen indolore d'atteindre la concurrence, qu'elle soit sûre pour les threads ou non. Cependant, cela se fait au prix d'une consommation de RAM accrue. Équilibrer la consommation de RAM est absolument essentiel à la stabilité et à la performance de votre application.

Nous avons vu 3 façons d'optimiser vos workers Unicorn pour des performances maximales :

  1. L'utilisation de Ruby 2.0 nous offre un ramasse-miettes considérablement amélioré qui nous permet d'exploiter les sémantiques de copie sur écriture.

  2. Ajuster les différentes options de configuration dans config/unicorn.rb.

  3. Utiliser unicorn-worker-killer pour résoudre le problème de manière élégante en tuant et en redémarrant les workers lorsqu'ils deviennent trop volumineux.

Ressources

  • Une explication merveilleuse de la façon dont fonctionne le ramasse-miettes de Ruby 2.0 et la sémantique copy-on-write.

  • La liste complète des options de configuration d'Unicorn.

<div class=“author ”>Soumis par : <a href=“http://benjamintanweihao.github.io/ ”>Benjamin Tan</div>