Close
Webinar mit Forrester: Warum Prozessexzellenz der SchlÞssel fÞr erstklassige GeschÃĪftsprozesse im digitalen Zeitalter ist
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
Gepostet in Celonis Developer Blog

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 clonehttps://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 addhttps://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.