Seit vier Jahren entwickelt Celonis eine eigene Query Engine zur Beantwortung von Process-Mining-Abfragen. Die Anforderungen an eine Entwicklungs- und Testinfrastruktur haben sich seit den Anfรคngen als kleines C++-Projekt auf mehreren Plattformen deutlich verรคndert. Entwicklungs- und Testlรถsungen sind in C++ nicht so etabliert wie in anderen Programmiersprachen wie Java; darum haben wir es nach und nach mit verschiedenen Strategien probiert: Im Folgenden prรคsentieren wir Ihnen unsere Erfahrungen gruppiert anhand von fรผnf Kategorien:ย Entwicklungssystem, Abhรคngigkeitsmanagement, Entwicklung, Tests und Bereitstellung.
CMake ist heute de facto das Standardentwicklungssystem fรผr plattformunabhรคngige C++-Projekte. Da es als Meta-Entwicklungssystem fungiert, lassen sich plattformspezifische Build-Dateien erzeugen. Zwar werden neuere Entwicklungssysteme immer beliebter, doch sind mehr Entwickler mit CMake vertraut, ermรถglichen mehr Abhรคngigkeiten eine nahtlose Integration und ist die Software deutlich reifer geworden.
Anfรคnglich verwendeten wir CMake fรผr UNIX-Builds (Linuxย & macOS), fรผr Windows hingegen nutzten wir direkt die Visual-Studio-Projektdateien. Dafรผr gab es mehrere Grรผnde:
Erstens musste die Verzeichnisstruktur fรผr Source- und Header-Dateien manuell eingerichtet werden, da Visual Studio diese Struktur nicht selbst erkennen konnte. Das Ergebnis war eine Liste von Source- und Header-Dateien in der IDE. Das Verfahren wurde erst mit CMakeย 3.8 vereinfacht.
Zweitens ist das Importieren vorhandener VS-Projektdateien kein leichtes Unterfangen. Viele der Einstellungen lieรen sich nicht einfach in CMake-Anweisungen รผbersetzen. Ein Beispiel dafรผr ist eine Multi-Core-Kompilierung, bei der nicht ganz klar ist, welchen Effekt die Einstellungen in der Benutzeroberflรคche von Visual Studio tatsรคchlich haben und wie sie sich in Visual-Studio-Dateien einrichten bzw. spรคter via MSBuild aufrufen lassen. Eine sorgfรคltige Portierung aller Compiler- und Abhรคngigkeitseinstellungen schien die Mรผhe nicht wert.
Als grรถรere รnderungen an der Abhรคngigkeits- und Projektstruktur sowieso zahlreiche Anpassungen an den verschiedenen Entwicklungssystemen erforderlich machten, fรผhrten wir die beiden Entwicklungssystemansรคtze endlich zusammen und verwendeten nur noch CMake.
Zu Beginn des Projekts wurden Abhรคngigkeiten zusammen mit dem Code in das Repository eingecheckt. Das ist eine einfache Methode, mit der Entwickler die Mรถglichkeit erhalten, den Code mit den erforderlichen Abhรคngigkeiten auszuchecken, ohne sich Gedanken รผber Details machen zu mรผssen. Fรผr Windows gab es sogar vordefinierte, eingecheckte Binรคrdateien. Die Erkenntnis, dass damit zahlreiche Nachteile verbunden sind, kam relativ schnell. Das Repository wurde grรถรer, Zusammenfรผhrungskonflikte wurden komplizierter, und die Verknรผpfung von Code und Abhรคngigkeiten war ein aktuelles Risiko.
Um diese Herausforderungen pragmatisch zu lรถsen, wechselten wir zum Klonen von Abhรคngigkeiten und vordefinierten Binรคrdateien via Shell-Skript. Dabei verloren wir die automatische Synchronizitรคt der eingecheckten Variante, da fรผr Abhรคngigkeitsversionen an einem bestimmten Commit-Zeitpunkt nicht mehr strikt garantiert werden konnte, dass beide Versionen identisch sind.
Das Shell-Skript hรคtte eine Abhรคngigkeit zu einem spezifischen Commit klonen mรผssen. Das Referenzieren von Verzweigungen oder Tags war deswegen riskant, weil sich diese spรคter รคndern konnten, was eine Reproduktion รคlterer Builds erschweren wรผrde. Abhรคngigkeitsmanagement รคhnelte also stark Git-Submodulen. In beiden Fรคllen wรผrde der Entwickler einen weiteren Befehl ausfรผhren mรผssen (oder zumindest einen Befehl zum Klonen anpassen); bei Git-Submodulen wurde die Versionsverwaltung fรผr Abhรคngigkeiten jedoch strikt von Git รผbernommen.
Darum entschieden wir uns schnell fรผr den Einsatz von Git-Submodulen. Beim Hinzufรผgen einer Abhรคngigkeit als Git-Submodul wรผrde diese am spezifischen Commit hinzugefรผgt, auf den HEAD zu dem Zeitpunkt verwiesen hat, selbst wenn HEAD eine symbolische Referenz war. So sind Commits des Codes mit Commits der Abhรคngigkeit verbunden und sollten in der Regel die Mรถglichkeit bieten, รคltere Versionen der Software zu reproduzieren.
Beispiel:
$ git clone https://github.com/boostorg/boost.git
$ cd boost
$ git symbolic-ref HEAD
refs/heads/master
$ git show-ref refs/heads/master
3d189428991b0434aa1f2236d18dac1584e6ab84 refs/heads/master
$ cd ../test-repo
$ git submodule addย https://github.com/boostorg/boost.git
Wenn diese รnderungen nun als Commit รผbernommen werden und eine andere Person den Code zu einem spรคteren Punkt auscheckt, wird Folgendes angezeigt:
$ cd test-repo
$ git submodule update —init
$ cd boost
$ git symbolic-ref HEAD
fatal: ref HEAD is not a symbolic ref
$ git show-ref HEAD
3d189428991b0434aa1f2236d18dac1584e6ab84 refs/remotes/origin/HEAD
Wir sehen also, dass HEAD die Informationen รผber eine symbolische Referenz verloren hat und sich nur an 3d189428991b0434aa1f2236d18dac1584e6ab84 erinnert.
Zwar ist das theoretisch eine tolle Garantie, doch wurde das Verfahren von Entwicklern nur selten genutzt. Das Aktualisieren von Abhรคngigkeiten war ein Prozess, der mehrere Schritte und Commits in verschiedenen Repositories beinhaltete. Intelligentere Git-Befehle schienen entweder nicht besonders gut dokumentiert oder oftmals eine Funktion spรคterer Git-Versionen zu sein.
Auรerdem erwiesen sich Git-Submodule damals als nicht besonders zuverlรคssig und beliebt, da durch CI sowie den Mangel an Tools und Support Probleme entstanden. Ein Wechseln von Verzweigungen sorgte bei Git-Submodulen ebenfalls fรผr Schwierigkeiten, sodass die Gesamterfahrung fรผr Entwickler eher schlecht war. Zudem kam es zu Engpรคssen bei der Aktualisierung von Abhรคngigkeiten, da dies in der Regel nur von wenigen Personen vorgenommen wurde. Darรผber hinaus wurde die Integration mit den Entwicklungssystemen, die eine andere Bereitstellung erforderten, nicht angegangen.
Angesichts dieser Erfahrungen schien es eine gute Idee zu sein, sich um ein Abhรคngigkeitsmanagement zu bemรผhen, das die Verwaltung von Versionen, Artefakten und Erstellung zusammenfรผhren wรผrde. Aus diesem Grund stiegen wir auf Conan um. Conan ist ein Python-basierter C++-Paket-Manager, der alle drei Anforderungen erfรผllt. Jedes Paket wird von einer Python-Datei beschrieben, die sรคmtliche Metadaten und Befehle zum Erstellen, Testen und Bereitstellen enthรคlt. Vordefinierte Pakete lassen sich in einem Artefakt-Repository so bereitstellen, dass Entwickler und die kontinuierliche Integration keine konstante Erstellung vornehmen mรผssen. Auรerdem verfรผgt Conan รผber eine gut ausgereifte CMake-Integration, die das Importieren von Paketen als CMake-Zielen besonders leicht macht.
Am Anfang eines Softwareprojekts ist die Entwicklung noch kein Problem. Wenn das Team auf wenige Personen begrenzt ist und es keine richtigen Freigabeprozesse gibt, wird die Software meist von den Entwicklern selbst entwickelt. So haben wir auch angefangen. Freizugebende Builds wurden von einem der Entwickler entwickelt. Deswegen kam es schnell zu Engpรคssen. Angesichts der steigenden Zahl von Entwicklern und Tag fรผr Tag mehr รผbermittelter Aufgaben fรผhrten รnderungen oftmals zu Regressionen, die erst sehr spรคt erkannt wurden.
Darum gewann eine kontinuierliche Entwicklung relevanter Verzweigungen an Bedeutung. Da wir fรผr Repositories Produkte von Atlassian (Bitbucket Server) sowie Ticket Management (Jira) verwendeten, war die logische Wahl fรผr einen Entwicklungsserver das entsprechende Atlassian-Produkt Bamboo. Das bietet den Vorteil, dass Builds mit Verzweigungen und Tickets mit Builds verknรผpft sind (wenn Verzweigungen richtig benannt werden).
Angesichts einer wachsenden Heterogenitรคt der erforderlichen Entwicklungsumgebungen fรผr immer mehr Projekte, die mit dem Bamboo-Server entwickelt werden sollten, wurde rasch der Einsatz von Docker-basierten Builds nรถtig. Darum begannen wir, den von Bamboo seit Versionย 6.4 bereitgestellten Docker Runner intensiv zu nutzen. Wรคhrend anfรคnglich nur die Hauptentwicklungsverzweigung sowie mehrere Release-Verzweigungen entwickelt wurden, weiten wir den Umfang der entwickelten Verzweigungen nun immer weiter aus. Je nach Projekt kann es sich dabei um Verzweigungen, die รผber offene Pull-Anfragen verfรผgen, bzw. um sรคmtliche Verzweigungen handeln, die im Push-Verfahren in das Remote-Repository รผbermittelt werden.
Das Entwickeln von Verzweigungen ist nur dann ein effektives Mittel gegen Regressionen, wenn es ein umfassendes Testprotokoll zum Testen der einzelnen Builds gibt. Andernfalls wรคre lediglich eine Suche nach Kompilierungsfehlern mรถglich. Da die Query Engine ein Datenbankprodukt ist, besteht die offensichtliche Testmethode aus dem Laden einiger Daten und dem anschlieรenden Ausfรผhren verschiedener Abfragen fรผr diese Daten. Auf diese Weise begannen wir mit dem Testen; diese Methode macht noch heute den grรถรten Teil der inzwischen รผber 2.300ย Tests aus.
Doch nicht alle Abschnitte lassen sich mit Abfragen effizient erreichen. Fรผr die Routinen im Datenmanagement, interne Puffer oder Komprimierung wurden zusรคtzliche Verfahren benรถtigt, die grรผndlich getestet werden mussten. Darum entschieden wir uns fรผr den Einsatz von Catch (und spรคter Catch2) als C++-Test-Frameworkย โ besonders mit Blick auf diese internen, aber extrem wichtigen Bestandteile unserer Software. Auรerdem begannen wir damit, Abdeckungsdaten von llcm-cov zu nutzen, um jenen Abschnitt des Codes zu ermitteln, den wir mit unserem Test ausfรผhren. Dadurch erhalten wir Hinweise darauf, wo wir Tests noch ausbauen mรผssen.
Darรผber hinaus werden jede Nacht Tests unter Verwendung von Sรคuberern ausgefรผhrt. So kรถnnen wir Probleme, die durch nicht definiertes Verhalten bzw. spezifische Adress- oder Threading-Bedingungen entstehen, verhindern.
Zu Beginn des Projekts war die Query Engine eine einzelne ausfรผhrbare Datei, deren Integration und Nutzung von der Software รผbernommen werden musste, in die sie eingebettet war.
Bei Linux konnte jeder Entwickler einfach die ausfรผhrbare Datei lokal erstellen und dann die Query Engine verwenden. Bei Windows und macOS war das nicht mรถglich, da es vor der Einrichtung eines Entwicklungsservers keine Option fรผr Entwickler gab, Entwicklungsumgebungen fรผr diese Plattformen einzurichten.
Darum entschieden wir uns fรผr ein Einchecken der Binรคrdateien in das Repository. Das hieร, dass Entwickler, die die Query Engine einer der beiden Plattformen in einem anderen Projekt verwenden wollten, nur noch den neuesten Code auschecken mussten, so eine aktuelle Version davon eingecheckt wurde. Dies fรผhrte rasch dazu, dass reihenweise Binรคrdateien in das Repository committet wurden. Dadurch dauerten Klon- und andere Vorgรคnge im Repository deutlich lรคnger. Da die Query Engine in einem Java-Projekt die meiste Zeit Verwendung findet, entschieden wir uns nun fรผr die Erstellung eines entsprechenden Maven-Pakets. So werden die plattformspezifischen ausfรผhrbaren C++-Dateien separat verpackt.
Unsere empfohlenen und aktuellen Best Practices fรผr die Einrichtung eines C++-Entwicklungssystems sehen wie folgt aus:
Sie sollten in fast jedem Fall CMake verwenden.
Sie sollten wahrscheinlich Conan verwenden.
Ab einer Teamgrรถรe von drei Personen sollten Sie wahrscheinlich รผber einige automatische Builds verfรผgen.
Fehlgeschlagene Tests sollten den Build stoppen.
Grundsรคtzlich versuchen wir, uns an zwei Dinge zu erinnern, wenn wir im Zusammenhang mit dem Entwicklungssystem รnderungen vornehmen: Iterative รnderungen und Entwicklererfahrung.
Wir wollen รnderungen ausschlieรlich iterativ vornehmen, damit die Builds stets so stabil wie mรถglich sind. Das wird umso wichtiger, je grรถรer das Team und je hรถher die Komplexitรคt der Software ist.
Auรerdem sollte die Entwicklererfahrung der Hauptfaktor bei den Entscheidungsprozessen fรผr รnderungen am Entwicklungssystem sein. Unsere Erfahrung mit Git-Submodulen hat gezeigt, dass die Einfรผhrung von Technologie, die keine gute Benutzererfahrung bietet, Folgen fรผr die individuelle Produktverantwortung von Entwicklern sowie die Agilitรคt des ganzen Teams im Ganzen hat.
Student, Software Engineer, C++/Databases by choice, Build infrastructure/DevOps by accident.
Abonnieren Sie unseren monatlichen Newsletter
Sehen Sie die Event-Highlights und erleben Sie die Zukunft der Business Execution.