- PagerDuty /
- Blog /
- Non classé /
- Développer une application Rails : comment nous avons accéléré le déploiement à nouveau
Blog
Développer une application Rails : comment nous avons accéléré le déploiement à nouveau
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 le rendre plus distribué et orienté services, mais une application Rails en constante évolution reste au cœur de l'ensemble pour gérer les préférences et les plannings des utilisateurs.
Comme c'est souvent le cas avec Rails, l'application était devenue très volumineuse, ce qui a commencé à poser de multiples problèmes ; le temps de déploiement, en particulier, me posait problème. Au début, déployer le code en production prenait environ 30 secondes. Puis, les déploiements ont atteint un point où ils 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 fait 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 de l'optimisation d'un code consiste à mesurer les pertes de temps. Nous avons personnalisé la configuration par défaut de Capistrano afin d'identifier précisément ce qui prenait autant de temps.
Nous avons publié la plupart de nos recettes de capistrano réutilisables et vous pouvez en profiter ici : 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 ** deploy ** ..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 ** ....déployer:lien symbolique ** ......git:mise à jour_étiquette_pour_l'étape 3s ** ....déployer:lien symbolique 5s ** ..déployer:mise à jour 288s ** ..déployer:nettoyage 3s ** ..newrelic:notification_déploiement 2s ** ..déployer:redémarrage 1s ** ..unicorn:application:redémarrage 1s ** ..déployer:redémarrage_bg_task 0s ** ..déployer:arrêt_bg_task 4s ** ..déployer:démarrage_bg_task 24s ** ..bluepill:démarrage_arrêt_roulement 124s ** ..déployer:mise à jour_cron 2s ** ..test_déployer:déclencheur_web 14s ** ..cap_gun:e-mail 0s ** ..hipchat:notifier_déploiement_terminé 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. git:valider_la_branche_est_une_étiquette 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 supprimions jamais les anciennes balises, le simple fait de les supprimer réduisait ce temps à 4 secondes.
Cette amélioration n'est ni la plus significative ni la plus intéressante, mais elle démontre l'utilité du rapport de performances. Sans lui, il était difficile de constater que cette tâche prenait plus de temps que nécessaire, les 25 secondes étant noyées dans le bruit de sortie du Capistrano.
Actifs
Le site web PagerDuty est très gourmand en ressources. Nous utilisons 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.
Rails gère tout cela pour nous , mais ce processus est assez lent.
Auparavant, compiler et empaqueter tout prenait plus de 200 secondes. Mais en examinant notre historique de déploiement, nous avons constaté 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 très précis quant à l'emplacement de stockage des ressources. En combinant ces connaissances et le contrôle de source, 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 #{latest_release} && #{source.local.log(current_revision, source.local.head)} #{asset_changing_files.join(' ')} | wc -l').to_i > 0 end
Si des fichiers des répertoires pouvant contenir des ressources sont modifiés, nous les considérons comme corrompus et 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 aspect lent est le redémarrage des 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 n'importe quel travailleur en cas de panne ou de consommation excessive de CPU ou de mémoire.
Ces processus sont assez lents à démarrer et, comme ils sont essentiels à notre pipeline de notifications, nous ne souhaitons pas les arrêter tous d'un coup et perdre la possibilité d'envoyer des alertes pendant quelques secondes. Nous partitionnons nos machines en trois groupes et redémarrons les processus un par un.
C’était un processus synchrone et très lent.
Nous avons réalisé qu'il n'y avait aucune raison d'exécuter ce processus de manière synchrone pendant le déploiement. Tant que le processus redémarrait correctement, nous n'avions pas besoin d'attendre. Pour vous aider, nous avons commencé à utiliser Surveillance , que nous avons trouvé être une solution robuste et puissante.
Le problème avec Monit était qu'il s'exécutait sur chaque hôte, mais ignorait l'existence des autres hôtes. Notre stratégie de déploiement progressif a donc dû être mise à jour. Désormais, au lieu de partitionner les serveurs eux-mêmes, nous partitionnons les processus sur chaque hôte. Ainsi, si trois processus de travail sont exécutés sur chaque hôte, nous arrêtons l'un des anciens et en démarrons un nouveau. Une fois le nouveau exécuté, nous répétons le processus pour chaque 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 deploy_test:web_trigger Tâche. Cette tâche sert de test de détection pour nos déploiements. Elle crée un incident PagerDuty et l'attribue au déployeur. Ce dernier s'assure que l'appel téléphonique aboutit et qu'il peut résoudre l'incident.
Cela était lent car le script de test devait charger l'intégralité de l'environnement Rails. La solution consistait à ne pas exécuter les opérations de manière synchrone. Grâce à 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 écran -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 ** deploy ** ..deploy:update ** ....hipchat:set_client 0 s ** ....hipchat:notify_deploy_started 1 s ** ....deploy:update_code ** ......db:symlink 1 s ** ......newrelic:symlink 1 s ** ......bundle:install 4 s ** ......deploy:assets:symlink 0 s ** ......deploy:finalize_update 1 s ** ......deploy:assets:precompile ** ........deploy:assets:cdn_deploy 0 s ** ......deploy:assets:precompile 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 ** deploy 46s ** ==========================================================
Nous avons donc ramené notre temps de déploiement sous la minute. Ces améliorations significatives simplifient le déploiement pour les développeurs et les encouragent ainsi à déployer plus souvent.
L'avenir
Un problème sur lequel je travaille encore, et qui n'est pas encore totalement résolu, concerne le temps de compilation des ressources. Il faut ajouter plusieurs minutes au temps de déploiement si les ressources ont changé. J'ai quelques pistes pour améliorer ce problème. First Rails perd beaucoup de temps à compiler les ressources des fournisseurs (jQuery par exemple) déjà 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 demander à notre serveur d'intégration continue de surveiller les modifications apportées aux ressources de notre dépôt Git et de les compiler de manière asynchrone. Le script de déploiement pourrait alors simplement copier les ressources compilées du serveur d'intégration continue 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 été modifié.
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 travaillez pas 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