Der Blog

Höhere Qualität und Zuverlässigkeit durch kontinuierliche Integration

von Ranjib Dey 14. April 2014 | 8 min Lesezeit

Continuous Integration (CI) ist eine Softwareentwicklungspraxis, bei der Mitglieder ihre Arbeit häufig zusammenführen, um Probleme und Konflikte zu verringern. Jeder Push wird durch einen automatisierten Build (und Test) zur Fehlererkennung unterstützt. Durch häufigen gegenseitigen Check können Teams Software schneller und zuverlässiger entwickeln. Im Wesentlichen geht es bei CI darum, die Qualität des Codes zu überprüfen, um sicherzustellen, dass keine Fehler in die Produktionsumgebung gelangen. Wenn beim Testen Fehler gefunden werden, kann die Quelle leicht entdeckt und behoben werden. Indem Sie den Code nach jedem Commit häufig testen, können Sie die Untersuchung der Fehlerquelle auf einen kürzeren Zeitraum verkürzen. Das manuelle Testen des Codes ist jedoch mühsam und redundant. Viele Tests können wiederverwendet werden, daher haben wir mehrere automatisierte Tests erstellt, um häufiges Testen zu erleichtern. Da diese Tests außerdem iterativ sind, erstellen wir, sobald ein Fehler gefunden wird, einen Test, der bei zukünftigen Codeüberprüfungen danach sucht, sodass alte Fehler nie wieder eingeführt werden.

Vor einem automatisierten Build

DVCS Bei PagerDuty wird, nachdem wir entschieden haben, was wir erstellen müssen, ein JIRA-Ticket erstellt, um die Zusammenarbeit zu erleichtern und die Mitglieder über den Status auf dem Laufenden zu halten. Im Ticket geben wir Informationen darüber an, was diese Funktion oder dieser Fix bewirkt und welche bekannten Auswirkungen es hat. Dann erstellen wir lokale Zweige aus unserem Git-Repo für die Funktion, die wir entwickeln möchten, oder das Problem/den Fehler, den wir beheben möchten, und geben ihm denselben Namen wie dem JIRA-Ticket. Git ist ein Distributed Version Control System (DVCS), es gibt also kein einzelnes Quellrepository, aus dem wir den Code nehmen, sondern mehrere Arbeitskopien. Dies verhindert, dass es in herkömmlichen Single-Source-Repositories, die auf einer physischen Maschine basieren, einen einzelnen Ausfallpunkt gibt. Bei PagerDuty steht Redundanz im Vordergrund (mehrere Datenbanken, mehrere Hosting-Anbieter, mehrere Kontaktanbieter für mehrere Kontaktmethoden usw.), sodass ein DVCS es uns erleichtert, lokal zu entwickeln, selbst wenn es Probleme gibt. Bazaar und Mercury sind einige weitere DVCS, die Sie sich vielleicht ansehen möchten.

Schreiben Sie zuerst Tests

Obwohl es schön wäre, für alles, was wir bauen, automatisierte Tests zu haben, braucht es Zeit, sie zu erstellen. Unsere Tests werden erstellt, bevor der Code geschrieben wird, damit wir sie zur Steuerung unserer Designs verwenden und vermeiden können, Code zu schreiben, der schwer zu testen ist. Diese testgetriebene Entwicklung (TDD) verbessert das Softwaredesign und hilft uns, den Code einfacher zu warten. Wir priorisieren Testkriterien in der folgenden Reihenfolge, da sie den größten Einfluss auf Zuverlässigkeit und Ressourcen haben.

1. Sicherheit – Kritische Fehler, die unsere Arbeitsabläufe blockieren, fallen in diese Kategorie. Wenn der Fix einen geschäftskritischen Codepfad verändert, möchten wir sicherstellen, dass wir alles getestet haben.

2. Strategisch – Umfassende Neuordnung des Codes, Hinzufügen neuer Funktionen. Diese Tests neigen dazu, entsprechende Spezifikationen in unsere Testsuite einzufügen. Dies betrifft sowohl Happy-Path-Szenarien als auch bekannte Regressionen. Zum Beispiel das Hinzufügen verschiedener Arten von Diensten/Mikrodiensten (ein neuer persistenter Speicher) oder eines neuen Tools (das eine sich wiederholende, lang andauernde manuelle Arbeit automatisiert).

3. Konsistenz – Als wachsendes Team müssen wir sicherstellen, dass der erstellte Code für Neulinge leicht verständlich ist und auf ihm aufgebaut werden kann. Diese Übung ist eine bewährte Best Practice für Codequalität, Fehlerbehandlung und die Identifizierung von Leistungsproblemen. Jeder, der Chef kennt, sollte in der Lage sein, unsere Codebasis zu verstehen. Beispielsweise die Isolierung unserer Anpassungen und deren Erfassung als separate Patches/Bibliotheken, die dann an Upstream-Projekte gesendet werden. In diesen Szenarien schreiben wir Spezifikationen für die Integrationsschicht (also den Verbindungsteil, der Erweiterungen mit externen Bibliotheken wie Community-Kochbüchern, Gems, Tools usw. verbindet).

4. Geteiltes Wissen – Jede Funktionalität hat gültige oder bestimmte Domänenannahmen. Wir verwenden Tests, um festzustellen, was diese Domänen sind, um die Grenzen einer Funktion zu kennen. Diese sind sehr spezifisch für unsere eigene Infrastruktur, ihre Abhängigkeiten und ihre Gesamttopologie. Ein Beispiel ist, wie wir suchgesteuerte dynamische Konfigurationsdateien für verschiedene Dienste generieren (wie wir Suchergebnisse immer sortieren, bevor wir sie verwenden). Wir schreiben Tests, um diese Annahmen zu validieren und durchzusetzen, was auch von nachgelagerten Toolchains genutzt wird (wie Namenskonventionen über Server, Umgebungen usw. hinweg).

Unsere Testsuite

Die von uns geschriebenen Tests lassen sich in 5 Kategorien einteilen. Der gesamte erstellte Code muss, mit Ausnahme von Belastungstests, vor der Bereitstellung Tests in der unten angegebenen Reihenfolge bestehen, um Qualität und Zuverlässigkeit zu gewährleisten.

Semantische Tests: Wir verwenden Lint-Checks für die allgemeine Semantik des Codes und allgemeine Best Practices und Rubocop für Ruby Linting und Lebensmittelkritiker für Chef-spezifisches Linting. Dies sind codebewusste Tools, daher funktionieren diese Tools je nach der Sprache, in der Sie schreiben, möglicherweise für Sie oder nicht. Lint-Tools werden nach jedem Commit global angewendet und wir müssen dafür keinen zusätzlichen Code schreiben.

Es gibt mehrere Fälle, in denen Lint-Tests tatsächliche Fehler entdeckt haben, abgesehen von Hinweisen auf Stilfehler. Beispielsweise kann foodcritic Chef-Ressourcen erkennen, die bei Aktualisierung keine Benachrichtigung senden.

Komponententests: Wir schreiben Unit-Tests für fast jedes Stück Code. Wenn wir Chef-Rezepte entwickeln, werden zuerst Chefspec-Tests geschrieben. Wenn wir reine Ruby-Bibliotheken schreiben, werden zuerst Rspec-Tests geschrieben. Lint- und Unit-Tests suchen nicht nach Funktionalität. Sie testen, ob der Code gut oder schlecht gestaltet ist.

Ein gutes Design macht es anderen Mitgliedern leicht, den Code zu verstehen. Darüber hinaus zeigen diese Tests, wie einfach es ist, den Code zu entkoppeln. Die Technologie ändert sich ständig und der Code muss flexibel sein. Wenn Ubuntu oder Nginx aus Sicherheitsgründen einen Patch veröffentlichen, wie einfach ist es, diese Änderung zu akzeptieren?

Funktionstests: Diese Tests dienen dazu, die Funktionsfähigkeit als Ganzes zu überprüfen, ohne dass Implementierungskenntnisse erforderlich sind und ohne dass Unterkomponenten verspottet oder abgespeckt werden müssen. Außerdem bemühen wir uns, die Funktionsspezifikationen so menschenlesbar wie möglich zu gestalten, in einfachem Englisch und ohne programmiersprachenspezifische Konstrukte.

Diese Tests helfen bei:

  • Bereitstellung neuer Server

  • Abbau des bestehenden Servers

  • Bereitstellung eines gesamten Clusters

  • ob eine Betriebssequenz funktioniert oder nicht

Wir gebrauchen Gurke und Aruba zum Schreiben von Funktionstests. Bei diesen Tests geht es nicht darum, wie der Code geschrieben ist, sondern nur darum, ob er funktioniert. Cucumber ist ein BDD-Tool, mit dem Spezifikationen in lesbarer Form geschrieben werden können (mit Gherkin), während Aruba eine Cucumber-Erweiterung ist, mit der Befehlszeilen-Anwendungen getestet werden können. Da die überwiegende Mehrheit unserer Tools eine Befehlszeilenschnittstelle (CLI) bietet, finden wir diese Testtools sehr praktisch und benutzerfreundlich.

Integrationstests: Diese Tests stellen sicher, dass alles funktioniert, wenn es mit allen anderen Diensten in einer produktionsähnlichen Topologie und einem produktionsähnlichen Verkehrsmuster kombiniert wird. Dies hilft uns auch dabei, zu beantworten, ob unsere Systemautomatisierungssuite perfekt neben verschiedenen Diensten und bei allen Änderungen funktioniert, die an ihnen oder anderen Diensten von Drittanbietern vorgenommen werden, die wir nutzen.

Belastungstests: Dies hilft uns dabei, zu bestimmen, mit welchem Verkehrsaufkommen wir zurechtkommen. Und die wichtigsten Leistungsengpässe schnell zu identifizieren. Wir führen eine Reihe von Einrichtungsaufgaben durch, um sicherzustellen, dass wir ein produktionsähnliches Datenvolumen haben. Im Allgemeinen sind diese Tests zeitaufwändig und ressourcenintensiv, daher werden sie regelmäßig für eine Reihe von Codeänderungen durchgeführt (Batching). Codeänderungen, bei denen wir der Meinung sind, dass die Leistung kein Problem darstellt (Konfigurationsänderungen, UI-Optimierungen), werden manchmal an diesen Tests vorbeigeführt.

Automatisierung der Bereitstellung und Plausibilitätsprüfung

HipChat_continuous_integration Nachdem der Code alle Tests bestanden hat, übergeben wir ihn an ein anderes Teammitglied, das ihn vor der Veröffentlichung auf Plausibilität prüft. Wir führen eine manuelle Code-Überprüfung durch, um eine zweite Meinung einzuholen und sicherzustellen, dass keine Fehler in die Produktion gelangen. Die Peer-Code-Überprüfung hilft sicherzustellen, dass keine Anforderungen übersehen wurden und der Code den Designstandards entspricht.

Wir folgen einer halbautomatischen Bereitstellung, bei der CI beim Testen hilft und projektspezifische Tools (wie Capistrano und Chef) bei der Bereitstellung helfen, der eigentliche Bereitstellungsprozess jedoch manuell ausgelöst wird. Das Bereitstellungstool selbst sendet eine Nachricht im PagerDuty HipChat-Raum, um alle zu informieren, wenn etwas bereitgestellt wird. Dann sendet es sowohl Benachrichtigungen vor als auch nach der Bereitstellung (als Sperr- und Entsperrdienstnachrichten). Dies hilft uns zu verstehen, was bereitgestellt wird, und gleichzeitige Bereitstellungen zu vermeiden.

Mit Continuous Integration schaffen wir eine grundlegende Softwarequalität, die erfüllt und aufrechterhalten werden muss, wodurch das Risiko unserer Veröffentlichungen gesenkt wird.