Blog

Wachstum einer Rails-Anwendung: Wie wir Deploy wieder schnell gemacht haben

von PagerDuty 18. Oktober 2012 | 8 Minuten Lesezeit

TL;DR; Wir haben unsere Bereitstellungszeit von 10 Minuten auf 50 Sekunden reduziert.

Als ich vor über einem Jahr zu PagerDuty kam, bestand unsere Anwendung im Wesentlichen aus einer einzigen Rails-Website. Seitdem haben wir die Architektur unseres Systems verändert und sind nun verteilter und serviceorientierter. Im Zentrum steht aber weiterhin eine stetig wachsende Rails-Anwendung zur Verwaltung von Benutzerpräferenzen und Zeitplänen.

Wie so oft bei Rails war die Anwendung sehr groß geworden, was zu diversen Problemen führte; insbesondere die Bereitstellungszeit bereitete mir Kopfzerbrechen. Anfangs dauerte die Bereitstellung des Codes in der Produktionsumgebung etwa 30 Sekunden. Später verlängerte sich die Bereitstellungszeit auf 6 bis 10 Minuten.

Früher war das ein Problem, weil 1) es unsere Entwicklung massiv verlangsamte und 2) die Bereitstellung keinen Spaß mehr machte.

Wir haben uns bemüht, unsere Bereitstellungszeit zu verkürzen, und möchten Ihnen gerne mitteilen, was wir dabei gelernt haben und wie wir das geschafft haben.

Der Stapel

Wir verwenden derzeit:

  • Ruby on Rails 3.2.8
  • CoffeeScript & SASS, kompiliert durch die Asset-Pipeline von Rails.
  • Capistrano 2.9.0
  • Ruby 1.9.3

Zuerst alles messen

Der erste Schritt zur Optimierung von Code besteht darin, genau zu messen, wo Zeit verschwendet wird. Wir haben die Standardkonfiguration von Capistrano angepasst, um einen klaren Überblick darüber zu erhalten, was so lange dauerte.

Wir haben die meisten unserer wiederverwendbaren Capistrano-Rezepte veröffentlicht, die Sie hier nutzen können: PagerDuty

Eine dieser Erweiterungen fügt am Ende jedes Capistrano-Laufs einen Leistungsbericht hinzu. Der vollständige Code ist hier zu finden.

So sah ein Leistungsbericht früher aus.

 ** Leistungsbericht ** ========================================================== ** Produktion 0s ** Multistage: Sicherstellen 0s ** Git:Branch-Ist-Tag-Validierung 25s ** HipChat:Benachrichtigung auslösen 0s ** Datenbank:Pflichtmigrationen prüfen 2s ** Bereitstellung ** ..Bereitstellung:Aktualisieren ** ....HipChat:Client einrichten 0s ** ....HipChat:Bereitstellungsstart benachrichtigen 18s ** ....Bereitstellung:Code aktualisieren ** ......Datenbank:Symlink 3s ** ......NewRelic:Symlink 3s ** ......Bundle:Installieren 4s ** ......Bereitstellung:Assets:Symlink 1s ** ......Bereitstellung:Aktualisierung abschließen 4s ** ......deploy:assets:precompile 230s ** ....deploy:update_code 264s ** ....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 ** deploy 470s 

Mithilfe dieses Berichts konnte ich viel leichter erkennen, was lange dauerte und was optimiert werden konnte.

Nachfolgend eine detaillierte Beschreibung der einzelnen Rezepte für langsam zubereitete Capistrano-Spieße und wie wir die Zubereitung beschleunigt haben.

Plausibilitätsprüfungen

Bei PagerDuty verwenden wir immer Git-Tags anstelle von Revisionen für die Bereitstellung. git:validate_branch_is_tag Die Aufgabe dient der Überprüfung, ob die SHA-Prüfsumme, die wir bereitstellen, tatsächlich einem Git-Tag entspricht. Warum dauert das Ganze 25 Sekunden? Wir haben festgestellt, dass dies daran liegt, dass wir alte Tags nie löschen würden. Durch das einfache Entfernen der alten Tags konnte die Dauer auf 4 Sekunden reduziert werden.

Diese Verbesserung ist zwar nicht die bedeutendste oder interessanteste, aber sie verdeutlicht den Nutzen des Leistungsberichts. Ohne ihn wäre es schwer zu erkennen gewesen, dass diese Aufgabe länger dauerte als nötig, da die 25 Sekunden im Rauschen der Capistrano-Ausgabe untergingen.

Vermögenswerte

Die PagerDuty Website ist sehr ressourcenintensiv. Wir haben viel CoffeeScript- und SASS-Code, der in JavaScript und CSS kompiliert werden muss, sowie viele Drittanbieterbibliotheken (z. B. Backbone.js, jQuery), die bei jedem Deployment komprimiert werden.

Schienen erledigt das alles für uns. Dieser Prozess verläuft jedoch recht langsam.

Früher dauerte das Kompilieren und Bündeln aller Dateien über 200 Sekunden. Ein Blick auf unsere Deployment-Historie zeigte jedoch, dass nur ein kleiner Teil der Deployments die Assets tatsächlich verändert. Daher sollte es nicht nötig sein, alles jedes Mal neu zu kompilieren. Rails legt sehr genau fest, wo Assets gespeichert werden dürfen. Durch die Kombination dieses Wissens mit der Versionskontrolle können wir feststellen, ob eine Neukompilierung der Assets erforderlich ist.

Der interessante Code lautet wie folgt:

 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 

Wenn sich Dateien in den Verzeichnissen, die Assets enthalten können, geändert haben, betrachten wir die Assets als geändert und kompilieren sie neu. In unserem Fall tritt dies nur bei einem geringen Anteil der Deployments auf, was eine deutliche Beschleunigung ermöglicht.

Hintergrundjobs

Der andere zeitaufwändige Faktor ist das Neustarten der Hintergrundprozesse. Diese Prozesse führen verschiedene Aufgaben in der PagerDuty Infrastruktur aus, unter anderem das Versenden von Benachrichtigungen an unsere Benutzer.

Die langsamste Aufgabe war bluepill:rolling_stop_start Bluepill ist ein Prozessmanager, der jeden Worker neu startet, falls dieser abstürzt oder zu viel CPU oder Speicher verbraucht.

Diese Worker benötigen relativ lange zum Starten, und da sie für unsere Benachrichtigungskette unerlässlich sind, möchten wir sie nicht alle gleichzeitig abschalten und dadurch für einige Sekunden die Möglichkeit verlieren, Benachrichtigungen zu senden. Bisher haben wir unsere Maschinen in drei Gruppen aufgeteilt und die Worker-Prozesse gruppenweise neu gestartet.

Dies war ein synchroner und sehr langsamer Prozess.

Wir erkannten, dass es keinen Grund gibt, diesen Prozess während der Bereitstellung synchron auszuführen. Solange der Prozess korrekt neu startete, mussten wir nicht auf ihn warten. Um dies zu vereinfachen, begannen wir mit der Verwendung von Monit , was sich für uns als robuste und leistungsstarke Lösung erwiesen hat.

Das Problem mit Monit war, dass es zwar auf jedem Host läuft, aber die anderen Hosts nicht kennt. Daher musste unsere Strategie für die schrittweise Bereitstellung angepasst werden. Anstatt die Server selbst zu partitionieren, partitionieren wir nun die Prozesse auf jedem Host. Wenn beispielsweise drei Worker-Prozesse auf jedem Host laufen, beenden wir einen der alten und starten einen neuen. Sobald der neue Prozess läuft, wiederholen wir diesen Vorgang für jeden weiteren alten Prozess.

Für den unwahrscheinlichen Fall, dass der Neustart fehlschlägt, ist Monit in unsere Überwachungsinfrastruktur eingebunden und wir werden benachrichtigt, um das Problem zu beheben.

Tests

Die letzte Aufgabe, die ich optimieren wollte, war die deploy_test:web_trigger Diese Aufgabe dient als Funktionstest für unsere Bereitstellungen. Dabei wird ein neuer PagerDuty Vorfall erstellt und dem zuständigen Bereitstellungsbeauftragten zugewiesen. Dieser stellt sicher, dass der Anruf durchgestellt wird und der Vorfall behoben werden kann.

Das war langsam, weil das Testskript die gesamte Rails-Umgebung laden musste. Die Lösung bestand erneut darin, die Vorgänge nicht synchron auszuführen. Mit screen können wir dieses Skript problemlos im Hintergrund ausführen.

 namespace :deploy_test do desc 'Erstellt einen Incident für einen Dienst mit einer Eskalationsrichtlinie, die den Benutzer kontaktiert, der die Bereitstellung durchgeführt hat' 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 

Die Endergebnisse

 ** Leistungsbericht ** ========================================================== ** Produktion 0s ** git:validate_branch_is_tag 4s ** hipchat:trigger_notification 0s ** db:check_for_pending_migrations 2s ** Bereitstellung ** ..deploy:update ** ....hipchat:set_client 0s ** ....hipchat:notify_deploy_started 1s ** ....deploy:update_code ** ......db:symlink 1s ** ......newrelic:symlink 1s ** ......bundle:install 4s ** ......deploy:assets:symlink 0s ** ......deploy:finalize_update 1s ** ......deploy:assets:precompile ** ........deploy:assets:cdn_deploy 0s ** ......deploy:assets:precompile 0s ** ....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 ** ========================================================== 

Wir haben unsere Bereitstellungszeit also wieder auf unter eine Minute reduziert. Das sind solide Verbesserungen, die es Entwicklern erleichtern, Bereitstellungen durchzuführen und sie dadurch zu häufigeren Bereitstellungen anregen.

Die Zukunft

Eine Sache, an der ich noch arbeite und die noch nicht vollständig gelöst ist, ist die Kompilierungszeit der Assets. Wenn sich die Assets geändert haben, verlängert sich die Bereitstellungszeit um einige Minuten. Mir fallen einige Verbesserungsmöglichkeiten ein. Erstens verschwendet Rails viel Zeit mit dem Kompilieren von externen Assets (z. B. jQuery), die bereits vorminimiert verfügbar sind. Dies würde die Kompilierungszeit verkürzen, erfordert aber eine Änderung der Asset-Pipeline.

Eine andere Lösung wäre, unseren Continuous-Integration-Server unser Git-Repository auf Änderungen an den Assets überwachen und diese asynchron kompilieren zu lassen. Das Deployment-Skript könnte die kompilierten Assets dann einfach vom CI-Server auf unser CDN kopieren, was deutlich schneller sein sollte. Wenn ein einzelner Rechner für die Kompilierung der Assets zuständig ist, kann er zudem einen Cache der kompilierten Version jeder Datei speichern und diese Datei nicht erneut kompilieren, wenn sie sich nicht geändert hat.

Abschluss

Unsere Einsätze sind wieder unter Kontrolle. Die wichtigsten Erkenntnisse sind:

  • Analysieren Sie Ihre Bereitstellungen, um die Gründe für ihre Langsamkeit zu ermitteln.
  • Erledige keine Arbeit, wenn du sie nicht brauchst.
  • Erledigen Sie so viele Aufgaben wie möglich asynchron.
  • Nutzen Sie die Überwachung, um sicherzustellen, dass asynchrone Aufgaben rechtzeitig erfolgreich ausgeführt werden.