Close
Build and testing environment at Celonis

Entwicklungs- und Testumgebung bei Celonis: Unsere Erfahrungen im Laufe der Zeit

maximillian springer celonis blog
von Maximilian Springer
November 11, 2019
Lesezeit: 10 Minuten

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.

Entwicklungssystem

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.

AbhÀngigkeitsmanagement

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.

Entwicklung

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.

Tests

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.

Bereitstellung

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.

Fazit

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.

maximillian springer celonis blog
Maximilian Springer
Software Engineering, Celonis

Student, Software Engineer, C++/Databases by choice, Build infrastructure/DevOps by accident.

Abonnieren Sie unseren monatlichen Newsletter

We've received your submission
Please fill in all the fields

Indem Sie dieses Formular absenden, stimmen Sie der Speicherung und Verarbeitung Ihrer personenbezogenen Daten durch Celonis gemĂ€ĂŸ unserer Datenschutzrichtlinie zu.
Lieber Besucher, wir haben festgestellt, dass Sie eine veraltete Browser-Version verwenden. Teile dieser Seite werden von Ihrem Browser nicht korrekt dargestellt. FĂŒr eine korrekte Funktionsweise der Seite empfehlen wir Ihnen, einen alternativen Browser zu verwenden oder Ihren Browser auf eine unterstĂŒtzte Version anzuheben.