Développer une application Rails : comment nous avons réussi à accélérer le déploiement
En résumé; Nous avons réduit notre temps de déploiement de 10 minutes à 50 secondes.
Lorsque j'ai rejoint PagerDuty il y a plus d'un an, notre application consistait essentiellement en un seul site Rails. Depuis, nous avons modifié l'architecture de notre système pour qu'il soit plus distribué et orienté vers les services, mais il existe toujours une application Rails en constante évolution au centre de tout cela pour gérer les préférences et les horaires des utilisateurs.
Comme c'est souvent le cas avec Rails, l'application était devenue très volumineuse et cela a commencé à causer de nombreux problèmes ; le temps de déploiement en particulier me posait problème. Au début, le déploiement du code en production prenait environ 30 secondes. Puis, les déploiements pouvaient prendre entre 6 et 10 minutes.
C'était un problème car 1) cela ralentissait considérablement notre développement et 2) les déploiements n'étaient plus amusants.
Nous avons déployé des efforts pour réduire notre temps de déploiement et nous aimerions partager avec vous ce que nous avons appris et comment nous y sommes parvenus.
La pile
Nous utilisons actuellement :
- Ruby on Rails 3.2.8
- CoffeeScript et SASS compilés par le pipeline d'actifs Rails
- Capistrano 2.9.0
- Ruby 1.9.3
Premièrement, mesurez tout
La première étape pour optimiser un code consiste à mesurer les pertes de temps. Nous avons personnalisé la configuration par défaut de Capistrano pour nous aider à avoir une idée claire de ce qui prenait autant de temps.
Nous avons publié la plupart de nos recettes de capistrano réutilisables et vous pouvez en profiter ici : https://github.com/ PagerDuty/pd-cap-recipes
L’une de ces extensions ajoute un rapport de performances à la fin de chaque exécution de Capistrano. Code complet ici
Voici à quoi ressemblait autrefois un rapport de performance.
** Rapport de performances ** ========================================================== ** production 0 s ** multistage:ensure 0 s ** git:validate_branch_is_tag 25 s ** hipchat:trigger_notification 0 s ** db:check_for_pending_migrations 2 s ** déploiement ** ..deploy:update ** ....hipchat:set_client 0 s ** ....hipchat:notify_deploy_started 18 s ** ....deploy:update_code ** ......db:symlink 3 s ** ......newrelic:symlink 3 s ** ......bundle:install 4 s ** ......deploy:assets:symlink 1 s ** ......deploy:finalize_update 4 s ** ......deploy:assets:precompile 230 s ** ....deploy:update_code 264 s ** ....deploy:symlink ** ......git:update_tag_for_stage 3s ** ....deploy:symlink 5s ** ..deploy:update 288s ** ..deploy:cleanup 3s ** ..newrelic:notice_deployment 2s ** ..deploy:restart 1s ** ..unicorn:app:restart 1s ** ..deploy:bg_task_restart 0s ** ..deploy:bg_task_stop 4s ** ..deploy:bg_task_start 24s ** ..bluepill:rolling_stop_start 124s ** ..deploy:cron_update 2s ** ..deploy_test:web_trigger 14s ** ..cap_gun:email 0s ** ..hipchat:notify_deploy_finished 0s ** déployer 470s
Grâce à ce rapport, il m’a été beaucoup plus facile de déterminer ce qui prenait beaucoup de temps et ce qui pouvait être optimisé.
Voici une description de chaque recette de Capistrano lente et de ce que nous avons fait pour la rendre plus rapide.
Vérifications de santé mentale
Chez PagerDuty, nous déployons toujours des balises git plutôt que des révisions. Le git:validate_branch_is_tag La tâche consiste à vérifier que le SHA que nous déployons est bien une balise git. Pourquoi cela prend-il 25 secondes ? Nous avons réalisé que, comme nous ne supprimerions jamais les anciennes balises, le simple fait de les élaguer accélérait ce processus à 4 secondes.
Cette amélioration n'est pas la plus significative ni la plus intéressante, mais elle montre l'utilité du rapport de performances. Sans lui, il était difficile de voir que cette tâche prenait plus de temps que nécessaire puisque les 25 secondes se perdaient dans le bruit de la sortie du Capistrano.
Actifs
Le site Web PagerDuty est très riche en ressources. Nous avons beaucoup de code CoffeeScript et SASS qui doit être compilé en JavaScript et CSS, ainsi que de nombreuses bibliothèques tierces (par exemple Backbone.js, jQuery) qui sont compressées à chaque déploiement.
Des rails gère tout cela pour nous , mais ce processus est assez lent.
Auparavant, il fallait plus de 200 secondes pour compiler et regrouper tout. Mais en examinant notre historique de déploiement, nous avons réalisé que seule une petite fraction des déploiements modifie réellement les ressources. Il ne devrait donc pas être nécessaire de tout recompiler à chaque fois. Rails est assez précis quant à l'emplacement de stockage des ressources. En combinant ces connaissances et le contrôle des sources, nous pouvons déterminer si une recompilation des ressources est nécessaire.
Le code intéressant est le suivant :
def assets_dirty? r = safe_current_revision return true if r.nil? from = source.next_revision(r) asset_changing_files = ['vendor/assets/', 'app/assets/', 'lib/assets', 'Gemfile', 'Gemfile.lock'] asset_changing_files = asset_changing_files.select do |f| File.exists? f end capture('cd #{dernière_version} && #{source.local.log(current_revision, source.local.head)} #{asset_changing_files.join(' ')} | wc -l').to_i > 0 end
Si l'un des fichiers des répertoires pouvant contenir des assets subit des modifications, nous considérons que les assets sont sales et nous les recompilons. Dans notre cas, cela ne se produit que sur une petite minorité de déploiements, ce qui permet une accélération très intéressante.
Emplois en arrière-plan
L'autre partie lente consiste à redémarrer les travailleurs en arrière-plan. Ces travailleurs effectuent diverses tâches dans l'infrastructure PagerDuty , notamment l'envoi d'alertes à nos utilisateurs.
La tâche la plus lente était pilule bleue : arrêt_roulement_démarrage . Bluepill est un gestionnaire de processus qui redémarre tout travailleur en cas de panne ou de consommation excessive de CPU ou de mémoire.
Ces travailleurs sont assez lents à démarrer et comme ils sont essentiels à notre pipeline de notifications, nous ne voulons pas les arrêter tous en même temps et perdre la possibilité d'envoyer des alertes pendant quelques secondes. Ce que nous avons l'habitude de faire, c'est de partitionner toutes nos machines en 3 groupes et de redémarrer les processus de travail un groupe à la fois.
C’était un processus synchrone et très lent.
Nous avons réalisé qu'il n'y avait aucune raison d'effectuer ce processus de manière synchrone pendant le déploiement. Tant que le processus redémarrait correctement, nous n'avions pas besoin de les attendre. Pour vous aider, nous avons commencé à utiliser Monit , que nous avons trouvé être une solution robuste et puissante.
Le problème avec Monit est qu'il s'exécute sur chaque hôte, mais ne connaît pas les autres hôtes, donc notre stratégie de déploiement continu devait être mise à jour. Maintenant, au lieu de partitionner les serveurs eux-mêmes, nous partitionnons les processus réels sur chaque hôte. Ainsi, si nous avons 3 processus de travail en cours d'exécution sur chaque hôte, nous arrêtons l'un des anciens et en démarrons un nouveau. Une fois le nouveau en cours d'exécution, nous répétons le processus pour chaque autre ancien processus.
Dans le cas peu probable où le redémarrage échoue, Monit est connecté à notre infrastructure de surveillance et nous sommes invités à résoudre le problème.
Tests
La dernière tâche que je voulais optimiser était la déployer_test : déclencheur_web tâche. Cette tâche agit comme un test de fumée pour nos déploiements. Il crée un nouvel incident PagerDuty et l'attribue au déployeur. Le déployeur s'assure que l'appel téléphonique passe et qu'il peut résoudre l'incident.
Cela a été lent car le script de test doit charger l'environnement Rails dans son intégralité. La solution a été de ne pas faire les choses de manière synchrone. En utilisant screen, nous pouvons facilement exécuter ce script en arrière-plan.
namespace :deploy_test do desc 'Créer un incident pour un service avec une politique d'escalade qui appellera l'utilisateur qui vient de déployer' task 'web_trigger', :roles => :test, :on_error => :continue do username = `git config user.username`.strip run 'cd #{current_path} && RAILS_ENV=#{rails_env} ./script/deploy/test_incident.sh #{username}', :pty => true end end
#!/bin/bash screen -m -d bundle exec rails runner -e $RAILS_ENV script/deploy/test_incident.rb $1
Les résultats finaux
** Rapport de performances ** ========================================================== ** production 0 s ** git:validate_branch_is_tag 4 s ** hipchat:trigger_notification 0 s ** db:check_for_pending_migrations 2 s ** déploiement ** ..déploiement:mise à jour ** ....hipchat:définir_client 0 s ** ....hipchat:notifier_déploiement_démarré 1 s ** ....déploiement:code_mise_à_jour ** ......base de données:lien symbolique 1 s ** ......newrelic:lien symbolique 1 s ** ......bundle:installation 4 s ** ......déploiement:actifs:lien symbolique 0 s ** ......déploiement:finaliser_mise_à_jour 1 s ** ......déploiement:actifs:précompilation ** ........déploiement:actifs:cdn_deploy 0 s ** ......déploiement:actifs:précompilation 0 s ** ....deploy:update_code 24s ** ....deploy:symlink ** ......git:update_tag_for_stage 8s ** ....deploy:symlink 9s ** ..deploy:update 35s ** ..deploy:cleanup 1s ** ..newrelic:notice_deployment 5s ** ..deploy:restart 0s ** ..deploy:bg_task_default_action 0s ** ..deploy_test:web_trigger 0s ** ..cap_gun:email 1s ** ..hipchat:notify_deploy_finished 0s ** déployer 46s ** ================================================================
Nous avons donc ramené notre temps de déploiement sous la minute. Ces améliorations concrètes facilitent le déploiement pour les développeurs et les encouragent ainsi à déployer plus souvent.
L'avenir
Une chose sur laquelle je travaille encore, et qui n'est pas entièrement résolue, est le temps de compilation des ressources. Vous devez ajouter plusieurs minutes au temps de déploiement si les ressources ont changé. Je peux penser à quelques moyens d'améliorer cela. First Rails perd beaucoup de temps à compiler des ressources de fournisseurs (jQuery par exemple) qui sont disponibles pré-minifiées. Cela réduirait le temps de compilation, mais nécessiterait de modifier le fonctionnement du pipeline de ressources.
L'autre solution serait de faire en sorte que notre serveur d'intégration continue surveille notre dépôt git pour détecter les modifications des ressources et les compile de manière asynchrone. Le script de déploiement pourrait alors simplement copier les ressources compilées du serveur CI vers notre CDN, ce qui devrait être beaucoup plus rapide. De plus, si une seule machine est responsable de la compilation des ressources, elle peut conserver un cache de la version compilée de chaque fichier et ne pas recompiler ce fichier s'il n'a pas changé.
Conclusion
Nos déploiements sont à nouveau sous contrôle. Les principaux enseignements sont les suivants :
- Profilez vos déploiements pour découvrir pourquoi ils sont lents
- Ne faites pas de travail lorsque vous n'en avez pas besoin
- Faites autant de choses que possible de manière asynchrone
- Utilisez la surveillance pour garantir que les tâches asynchrones réussissent en temps opportun