Der Blog

Eine Rails-Anwendung weiterentwickeln: So haben wir die Bereitstellung wieder beschleunigt

von PagerDuty 18. Oktober 2012 | 8 min Lesezeit

Kurz zusammengefasst: Wir haben unsere Bereitstellungszeit von 10 Minuten auf 50 Sekunden verkürzt.

Als ich vor über einem Jahr zu PagerDuty kam, bestand unsere Anwendung im Wesentlichen aus einer einzigen Rails-Site. Seitdem haben wir die Architektur unseres Systems geändert, um es stärker verteilt und serviceorientiert zu gestalten, aber im Mittelpunkt steht immer noch eine ständig wachsende Rails-App zur Verwaltung von Benutzereinstellungen und Zeitplänen.

Wie so oft bei Rails war die Anwendung sehr groß geworden, was zu zahlreichen Problemen führte. Besonders die Bereitstellungszeit bereitete mir Kummer. Anfangs dauerte die Bereitstellung von Code für die Produktion etwa 30 Sekunden. Dann kam ein Punkt, an dem die Bereitstellung zwischen 6 und 10 Minuten dauern konnte.

Dies war früher ein Problem, weil 1) es unsere Entwicklung massiv verlangsamte und 2) die Bereitstellungen keinen Spaß mehr machten.

Wir haben einige Anstrengungen unternommen, um unsere Bereitstellungszeit zu verkürzen, und wir möchten Ihnen mitteilen, was wir dabei gelernt haben und wie wir es erreicht haben.

Der Stapel

Wir verwenden derzeit:

  • Ruby on Rails 3.2.8
  • CoffeeScript und SASS kompiliert von der Asset-Pipeline von Rails
  • Capistrano 2.9.0
  • Rubin 1.9.3

Messen Sie zunächst alles

Der erste Schritt zur Optimierung eines Codes besteht darin, tatsächlich zu messen, wo Sie Zeit verschwenden. Wir haben die Standardkonfiguration von Capistrano angepasst, um einen klaren Überblick darüber zu erhalten, was so lange dauert.

Wir haben die meisten unserer wiederverwendbaren Capistrano-Rezepte veröffentlicht und Sie können sie hier nutzen: https://github.com/ PagerDuty/pd-cap-recipes

Eine dieser Erweiterungen hängt am Ende jedes Capistrano-Laufs einen Leistungsbericht an. Den vollständigen Code finden Sie hier

So sah ein Leistungsbericht früher aus.

 ** Leistungsbericht ** ============================================================= ** Produktion 0 s ** mehrstufig: sicherstellen 0 s ** git: validate_branch_is_tag 25 s ** hipchat: Benachrichtigung auslösen 0 s ** db: auf ausstehende Migrationen prüfen 2 s ** bereitstellen ** ..bereitstellen: aktualisieren ** ....hipchat: client festlegen 0 s ** ....hipchat: notification_deploy_started 18 s ** ....bereitstellen: code aktualisieren ** ......db: symbolischer Link 3 s ** ......newrelic: symbolischer Link 3 s ** ......bundle: installieren 4 s ** ......bereitstellen: asset: symbolischer Link 1 s ** ......bereitstellen: update abschließen 4 s ** ......Bereitstellung:Assets:Vorkompilierung 230s ** ....Bereitstellung:Update_Code 264s ** ....Bereitstellung:Symlink ** ......git:Update_Tag_für_Stadium 3s ** ....Bereitstellung:Symlink 5s ** ..Bereitstellung:Update 288s ** ..Bereitstellung:Bereinigung 3s ** ..newrelic:Benachrichtigung_Bereitstellung 2s ** ..Bereitstellung:Neustart 1s ** ..Unicorn:App:Neustart 1s ** ..Bereitstellung:bg_task_Neustart 0s ** ..Bereitstellung:bg_task_stop 4s ** ..Bereitstellung:bg_task_start 24s ** ..Bluepill:Rolling_Stop_Start 124s ** ..Bereitstellung:Cron_Update 2s ** ..Bereitstellung_Test:Web_Trigger 14s ** ..cap_gun:email 0s ** ..hipchat:notify_deploy_finished 0s ** Bereitstellung 470s 

Mit diesem Bericht konnte ich viel einfacher erkennen, was lange dauerte und was optimiert werden konnte.

Nachfolgend finden Sie eine Aufschlüsselung der einzelnen langsamen Capistrano-Rezepte und was wir getan haben, um es schneller zu machen.

Plausibilitätsprüfungen

Bei PagerDuty verwenden wir immer Git-Tags statt Revisionen. git:validate_branch_is_tag Aufgabe ist eine Plausibilitätsprüfung, um zu bestätigen, dass der von uns bereitgestellte SHA tatsächlich ein Git-Tag ist. Warum dauert das so lange 25 Sekunden? Wir haben festgestellt, dass wir alte Tags nie löschen würden. Das einfache Bereinigen der alten Tags beschleunigte dies auf 4 Sekunden.

Diese Verbesserung ist nicht die bedeutendste oder interessanteste, aber sie zeigt die Nützlichkeit des Leistungsberichts. Ohne ihn war es schwer zu erkennen, dass diese Aufgabe länger als nötig dauerte, da die 25 Sekunden im Rauschen der Capistrano-Ausgabe verloren gingen.

Vermögenswerte

Die PagerDuty -Website ist sehr assetintensiv. Wir haben viel CoffeeScript- und SASS-Code, der in JavaScript und CSS kompiliert werden muss, sowie viele Bibliotheken von Drittanbietern (z. B. Backbone.js, jQuery), die bei jeder Bereitstellung komprimiert werden.

Schienen übernimmt das alles für uns , aber dieser Prozess ist ziemlich langsam.

Früher dauerte es über 200 Sekunden, alles zu kompilieren und zu bündeln. Aber als wir uns unseren Bereitstellungsverlauf ansahen, stellten wir fest, dass nur ein kleiner Teil der Bereitstellungen die Assets tatsächlich ändert. Es sollte also nicht nötig sein, jedes Mal alles neu zu kompilieren. Rails ist ziemlich genau, was den Speicherort von Assets angeht. Indem wir dieses Wissen und die Quellcodeverwaltung kombinieren, können wir feststellen, ob eine Neukompilierung der Assets erforderlich ist.

Der interessante Code ist dieser:

 def asset_dirty? r = sichere_aktuelle_Revision gibt true zurück, wenn r.nil? von = Quelle.nächste_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 an einer der Dateien in den Verzeichnissen, die Assets enthalten können, Änderungen ergeben, betrachten wir die Assets als fehlerhaft und kompilieren sie neu. In unserem Fall geschieht dies nur bei einer kleinen Minderheit der Bereitstellungen, sodass eine sehr interessante Beschleunigung möglich ist.

Hintergrundjobs

Der andere langsame Teil ist der Neustart der Hintergrund-Worker. Diese Worker führen verschiedene Aufgaben in der PagerDuty Infrastruktur aus, einschließlich des tatsächlichen Sendens von Warnmeldungen an unsere Benutzer.

Die langsamste Aufgabe war bluepill:rolling_stop_start . Bluepill ist ein Prozessmanager, der alle Worker neu startet, falls sie abstürzen oder zu viel CPU oder Speicher verbrauchen.

Der Start dieser Worker dauert ziemlich lange und da sie für unsere Benachrichtigungspipeline von entscheidender Bedeutung sind, möchten wir sie nicht alle auf einmal abschalten und für einige Sekunden die Möglichkeit verlieren, Warnmeldungen zu senden. Normalerweise teilen wir alle unsere Maschinen in drei Gruppen auf und starten die Worker-Prozesse gruppenweise neu.

Dies war ein synchroner und sehr langsamer Prozess.

Wir haben festgestellt, dass es keinen Grund gibt, diesen Prozess synchron während der Bereitstellung durchzuführen. Solange der Prozess korrekt neu gestartet wurde, mussten wir nicht darauf warten. Um zu helfen, haben wir begonnen, Überwachen , was wir als robuste und leistungsstarke Lösung kennengelernt haben.

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

Im unwahrscheinlichen Fall, dass der Neustart fehlschlägt, wird Monit an unsere Überwachungsinfrastruktur angeschlossen und wir werden zur Lösung des Problems angefragt.

Tests

Die letzte Aufgabe, die ich optimieren wollte, war die Bereitstellungstest: Web-Trigger Aufgabe. Diese Aufgabe dient als Test für unsere Bereitstellungen. Sie erstellt einen neuen PagerDuty Vorfall und weist ihn dem Bereitsteller zu. Der Bereitsteller stellt sicher, dass der Anruf durchkommt und dass er den Vorfall lösen kann.

Dies war langsam, da das Testskript die gesamte Rails-Umgebung laden muss. Die Lösung bestand erneut darin, die Dinge nicht synchron zu erledigen. Mit screen können wir dieses Skript problemlos im Hintergrund ausführen.

 Namespace: deploy_test do desc „Erstellen Sie einen Vorfall für einen Dienst mit einer Eskalationsrichtlinie, die den Benutzer anruft, der gerade bereitgestellt hat“ Task „web_trigger“,: Rollen =>: Test,: bei Fehler =>: fortsetzen do Benutzername = „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 endgültigen Ergebnisse

 ** Leistungsbericht ** ============================================================== ** Produktion 0 s ** git:validate_branch_is_tag 4 s ** hipchat:trigger_notification 0 s ** db:check_for_pending_migrations 2 s ** Bereitstellen ** ..Bereitstellen:Update ** ....hipchat:set_client 0 s ** ....hipchat:notify_deploy_started 1 s ** ....Bereitstellen:update_code ** ......db:symlink 1 s ** ......newrelic:symlink 1 s ** ......bundle:install 4 s ** ......Bereitstellen:assets:symlink 0 s ** ......Bereitstellen:finalize_update 1 s ** ......Bereitstellen:assets:precompile ** ........Bereitstellung:Assets:cdn_deploy 0s ** ......Bereitstellung:Assets:Vorkompilierung 0s ** ....Bereitstellung:Update_Code 24s ** ....Bereitstellung:Symlink ** ......git:Update_Tag_für_Stadium 8s ** ....Bereitstellung:Symlink 9s ** ..Bereitstellung:Update 35s ** ..Bereitstellung:Bereinigung 1s ** ..newrelic:Benachrichtigung_Bereitstellung 5s ** ..Bereitstellung:Neustart 0s ** ..Bereitstellung:bg_task_default_action 0s ** ..Bereitstellung_Test:Web_Trigger 0s ** ..cap_gun:E-Mail 1s ** ..hipchat:Benachrichtigung_Bereitstellung_fertig 0s ** Bereitstellung 46s ** ======================================================= 

Wir haben unsere Bereitstellungszeit also wieder auf unter eine Minute reduziert. Das sind solide Verbesserungen, die Entwicklern die Bereitstellung erleichtern und sie somit dazu ermutigen, häufiger bereitzustellen.

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, müssen Sie der Bereitstellungszeit viele Minuten hinzufügen. Mir fallen einige Möglichkeiten ein, dies zu verbessern. Erstens verschwendet Rails viel Zeit mit der Kompilierung von Anbieter-Assets (z. B. jQuery), die vorab minimiert verfügbar sind. Dies würde die Kompilierungszeit verkürzen, würde aber eine Änderung der Funktionsweise der Asset-Pipeline erfordern.

Die andere Lösung wäre, dass unser Continuous-Integration-Server unser Git-Repository auf Asset-Änderungen überwacht und diese asynchron erstellt. Das Bereitstellungsskript könnte dann einfach die kompilierten Assets vom CI-Server auf unser CDN kopieren, was viel schneller sein sollte. Wenn eine einzelne Maschine für die Kompilierung der Assets verantwortlich ist, kann sie außerdem einen Cache der kompilierten Version jeder Datei aufbewahren und diese Datei nicht neu kompilieren, wenn sie sich nicht geändert hat.

Abschluss

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

  • Erstellen Sie ein Profil Ihrer Bereitstellungen, um herauszufinden, warum sie langsam sind
  • Arbeiten Sie nicht, wenn Sie es nicht müssen
  • Erledigen Sie so viele Dinge wie möglich asynchron
  • Verwenden Sie die Überwachung, um sicherzustellen, dass asynchrone Aufgaben rechtzeitig erfolgreich sind