MADDOX Systemarchitektur

MADDOX als Assistenzsystem für Maschinenbediener wird in industriellen Produktionsumgebungen eingesetzt. Dabei erfordern die vielfältigen Randbedingungen zum einen ein breites Lösungsspektrum, beispielsweise für Maschinenschnittstellen. Zum anderen soll sich die Software möglichst flexibel in die vorhanden operativen Prozesse und die Systemlandschaft beim Kunden integrieren lassen. Unser Assistenzsystem ist somit nicht das eine Produkt, das nach jeder Änderung einfach neu gebaut wird und allen Kunden zum Download bereit gestellt wird. Bei Peerox haben wir erkannt, dass es besondere Lösungen bei der Systemarchitektur braucht, um diesen besonderen Anforderungen gerecht zu werden.

Wir passen MADDOX somit den Gegebenheiten bei unseren Kunden an. Um bei einem neuen Kunden zu starten, müssen wir die Anbindung an die Maschinen vor Ort vornehmen. Stehen dazu Schnittstellen in Form von gängigen Industriestandards (bspw. OPC UA) bereit, ist es oft mit einer einmaligen Konfiguration getan. In anderen Fällen müssen wir die Schnittstelle neu programmieren. Hinzu kommen kundenindividuelle Features, die wir als Plugins erstellen und durch einen einfachen Schalter in der Konfiguration an- und ausschalten können.

Und nicht zuletzt ist MADDOX das Produkt einer Ausgründung aus dem Fraunhofer IVV in Dresden. Es entsteht weiterhin in Kooperation mit den Forschern der Arbeitsgruppe Digitalisierung und Assistenzsysteme, sowie im Verbund mit externen Dienstleistern.

Die Realisierung kundenindividueller Anforderungen und die Koordinierung der Entwicklung mit unseren Partnern zwingt uns dazu, unsere Prozesse gut im Griff zu haben, aber auch die Architektur entsprechend zu modularisieren und die Schnittstellen gut zu entwerfen, sodass jedes Team effektiv arbeiten kann.

Um dabei nicht den Überblick zu verlieren, haben wir frühzeitig ein paar wichtige Weichen gestellt, damit die Technik uns möglichst nicht im Weg steht und wir uns auf die harten Probleme stürzen können. In diesem Text beleuchten wir einige der Techniken und Technologien, die wir nutzen:

  • aufeinander aufbauende Python-Projekte
  • Service-Architektur im Verbund mit einem Message Broker
  • automatisierte Tests
  • isomorphe Builds dank Docker
  • CI/CD und Configuration Management

Los geht’s.

Aufeinander aufbauende Python-Projekte

MADDOX ist in Python implementiert. Der Quellcode wird dabei nicht in einem einzigen Repository verwaltet. Kernbestandteile werden modular entwickelt, sodass sie wiederverwendet werden können. Tatsächlich gibt es sogar zwei dieser Kernkomponenten: Unsere eigene Kernkomponente bildet unter anderem ein Backend mit einem API-Gateway, welches auf dem bekannten Web-Framework Flask basiert, sowie die Nutzerverwaltung und unsere Wissensdatenbank. Die andere Kernkomponente entsteht am Fraunhofer IVV Dresden, mit dem wir uns, wie erwähnt, in einer engen Kooperation befinden. Dort werden Funktionalitäten zur Maschinendatenerfassung und -verarbeitung, sowie Algorithmen zum Maschinellen Lernen als Gegenstand von Forschungsprojekten kontinuierlich weiterentwickelt.

Die Komponenten, die wir modular einsetzen, lassen sich mit Hausmitteln der Python-Community erzeugen, verwalten und nutzen: Mit setuptools erstellen wir ein Bibliotheks-Archiv, das wir auf einen internen Python Package Index (PyPI) hochladen und von dort genauso wie externe Bibliotheken mit dem Python Paket-Manager (pip) in die abhängigen Projekte einbinden. Zum Bereitstellen einer MADDOX-Installation ergibt sich somit eine geschachtelte Projektstruktur.

Die Aufsplittung in mehrere Projekte ergibt sich ganz natürlich aus den zum Teil individuellen Anforderungen der Kundenprojekte sowie unserer Organisationsstruktur. Daraus resultiert eine gewisse inhärente Komplexität, die es uns jedoch ermöglicht, kundenindividuelle und trotzdem skalierbare Lösungen zu schaffen. Wir sind dadurch außerdem in der Lage, Experten für Web-Frontends oder Machine Learning nahtlos direkt in den Entwicklungsprozess mit einzubeziehen.

Service-Architektur und Message Broker

Durch die unterschiedlichen Umgebungen bei den verschiedenen Kunden waren wir gezwungen, unsere Software-Architektur möglichst flexibel zu halten. Ein und dieselbe Komponente zum Vorverarbeiten von Maschinen-Sensor-Daten kann beim einem Kunden kaum etwas zu tun haben und beim nächsten sehr hoher Last ausgesetzt sein. Während es im ersten Fall keinen Unterschied macht, ob sie im Hauptprozess mitläuft, kann sie im andern potentiell zum Flaschenhals werden und läuft idealerweise parallelisiert in einem eigenen Prozess oder sogar auf einer eigenen Maschine.

In frühen Entwicklungsversionen hatten wir zunächst ein monolithisches System erstellt, welches mehrere Prozesse mittels einer eigenen Prozess-Runtime verwaltet hat. Allerdings haben wir schnell festgestellt, dass wir dasselbe auch mit geringerem Aufwand unter Verwendung von Containern erreichen können. Schließlich sind Container eigene Prozesse und liefern einige weitere Eigenschaften mit, die wir uns gerne zunutze machen (s. Abschnitt “isomorphe Builds und Infrastructure as Code”).

In der Folge haben wir unsere komplette Software auf Container umgestellt und sind damit bei einer modularen Service-Architektur gelandet. So gibt es mehrere Module zum Auslesen und Übersetzen von Sensorwerten und Maschinenzuständen, eines zum Verwalten von Wissensbausteinen, mehrere Machine-Learning-Module, und so weiter. Wir nutzen das auch in den Kundenprojekten, um kundenspezifische Features nach Bedarf in Form von eigenen Plugin-Modulen hinzuzuschalten.

Die Module kommunizieren untereinander über einen Message Broker, senden und empfangen also Nachrichten auf einem gemeinsamen Kanal, ohne einander kennen zu müssen. Bis auf die gemeinsamen Nachrichtentypen und die gemeinsame Datenbank sind die Module also komplett entkoppelt. Anders als bei einer Punkt-zu-Punkt-Topologie beschränkt sich der Mehraufwand beim Verteilen der verschiedenen Module auf unterschiedliche System damit darauf, jedem Modul die Adresse des Message-Brokers als Konfigurationsvariable mitzugeben.

Mit der Service-Architektur nutzen wir ein Stück weit das beste aus den beiden Welten Service-orientierte und Monolithische Architektur: Die Architektur ist sehr formbar, aber das ist nur mit so viel Komplexität erkauft, wie unbedingt notwendig.

Automatisierte Tests

Zu den Vorteilen von automatisierten Tests muss man sicherlich nicht allzu viele Wörter verlieren.

Bei uns ist der durch Tests abgefangene potentielle Schaden durch die in vorhergehenden Abschnitten beschriebene Projektlandschaft deutlich höher, als wenn wir nur ein zentrales Mono-Repo hätten. Genauso wie ein Bug, der erst im Produktiv-Betrieb und nicht schon während der Entwicklung gefunden wird, höhere Aufwände und damit Kosten verursacht, gilt das auch für einen Fehler, der erst in einem abhängigen Projekt (bspw. im Kundenprojekt) gefunden wird, aber seine Entstehung in einer zugrundeliegenden internen Bibliothek hat. So gesehen profitieren wir von jedem Test, der Bugs an Ort und Stelle abfängt, mehrfach.

Trotzdem sind wir hier nicht dogmatisch. Eine einhunderprozentige Code-Abdeckung und ein Test-First-Dogma birgt Vorteile, doch es bringt in Anbetracht von sich häufig ändernden Anforderungen natürlich auch entsprechenden Aufwand mich sich, der  im wirtschaftlich-zeitlichen Rahmen abgebildet werden muss. Derzeit verwenden wir daher eher Integration-Tests für den erwarteten Pfad durch den Code sowie einige Sonderfälle und fangen so den Großteil der Fehler ab.

Die Tests begleiten uns bereits während der Entwicklung. Sie werden aber auch im Rahmen des Build-Prozesses durch unsere Pipelines permanent ausgeführt. Damit stellen wir sicher, dass uns eine Pipeline auch dann auffängt, wenn man bspw. mal vergessen hat, sich an die Konvention zu halten, oder wenn es zu einer unvorhergesehenen Regression kommt.

Außerdem arbeiten wir mit Code-Reviews und Mentoring an einem hohen Standard für Code-Design. Viele Fehler entstehen überhaupt erst dadurch, dass Entwickler gar nicht genau verstehen, was die Kollegen (oder man selber vor wenigen Wochen) vorher implementiert haben. Mit entsprechenden Stil-Mitteln kann Quellcode les- und formbarer machen. Es ist uns wichtig, uns und unsere Arbeit durch entsprechendes Refactoring kontinuierlich zu verbessern.

Isomorphe Builds dank Docker

Wenn man mit mehreren Projekten hantiert, kann es schnell zur Konfigurationshölle werden, wenn jede Entwicklerumgebung, jede Test-Instanz und jede Kundeninstallation einzigartig ist. Alleine die Unterschiede der verschiedenen Betriebssysteme untereinander können zu allerhand Problemen führen, genauso wie unterschiedliche Versionen von Programmbibliotheken auf der Host-Maschine sowie andere Konfigurationen.

Stattdessen nutzen wir Docker als etablierte Technologie zur Virtualisierung auf Ebene des Betriebssystems, um auf jeder Maschine dieselben Randbedingungen herzustellen. Es ermöglicht es uns, mit einer einheitlichen Umgebung zu arbeiten (Container), deren Zustand wir genau kontrollieren können.

CI/CD und Configuration Management

Container werden aus Container-Images erstellt. Diese Images werden bei uns in einer CI-Pipeline im Rahmen eines Build-Prozesses erstellt und können anschließend in eine eigens betriebene Image-Registry aufgenommen werden. Hierfür verwenden wir Gitlab. Es stellt bei uns neben den Quellcode-Repositories auch die Registry für Container Images bereit und bringt außerdem Funktionalität zum Ausführen von CI/CD Pipelines mit.

Die Container einer MADDOX Instanz werden jeweils aus einem Image erzeugt und können dabei mittels Umgebungsvariablen parametrisiert werden. Außerdem können kundenindividuelle Initialisierungsdaten in den Containern verwendet werden. Diese Mechanismen nutzen wir für unsere Zwecke beim Betrieb der MADDOX-Instanzen: Die verschiedenen Kundenkonfigurationen werden separat gepflegt und stehen in einem eigens dafür vorgesehenen Projekt zur Verfügung. Ansible als Technologie aus dem Umfeld des Configuration Managements erlaubt es uns, den Betrieb einer MADDOX Instanz (Inbetriebnahme, Wartung, Aktualisierung) zentral zu steuern. Sie bringt somit Images, Initialisierungsdaten und Umgebungsvariablen zusammen und ist unser Steuerungsinstrument.

Der Trade-off ist an der Stelle eine durchaus erhöhte Anzahl an beweglichen Teilen und Technologien. Ein Stück weit wird die Komplexität beim Verwalten von Betriebssystemen, Paketen und Umgebungsvariablen ersetzt durch die Komplexität von Konfigurations-Manangement und Pipelines. Hinzu kommt das benötigte Know-how, um mit den gewählten Werkzeugen, sowie mit Containern und Images arbeiten zu können. Der Vorteil ist, dass die Komplexität nicht linear mit jeder weiteren Instanz steigt. Mit jedem weiteren Kunden profitieren wir daher vom eingeschlagenen Weg: Eine besondere Lösung für eine besondere Herausforderung.