Das Projekt läuft gut. Das Team hat alles im Griff. Der Kunde ist zufrieden. Vielleicht sogar so zufrieden, dass die Software anderen Kunden angeboten wird. Und so wird aus dem Projekt ein Produkt.
Wenn das Projekt vom eigenen Erfolg überholt wird
Also alles gut? Nein, denn sehr oft ergeben sich gerade aus der Transformation vom Projekt zum Produkt Probleme, die den Produkterfolg gefährden. Diese Transformationsprobleme können sich auf vielfältige Art und Weise zeigen. In diesem Artikel legen wir unser Augenmerk auf das Thema Performance. Zwei praktische Beispiele stehen exemplarisch für die Herausforderungen, die sich für die Performance beim Schritt von der Projekt- zur Produktentwicklung ergeben. Im Folgenden beleuchten wir diese im Detail. Auch wenn hier primär Produkte betrachtet werden, finden sich viele der Herausforderungen auch in anderen Projekten, zum Beispiel durch sich ändernde Nutzungskontexte während der Laufzeit eines agilen Softwareprojekts.
Von Konfigurationen …
Ein Konfigurationsmanager verteilt Konfigurationen an Drittsysteme. Das System besteht aus weltweit verteilten Instanzen, die in einem Netz organisiert sind. Etwa 95 Prozent der Anfragen lesen Konfigurationsdaten, die restlichen 5 Prozent dienen zum Schreiben von neuen oder veränderten Konfigurationsdaten. Die schreibenden Zugriffe standen jedoch trotz ihrer eher geringen Anzahl besonders im Fokus. Denn werden Konfigurationsdaten geschrieben, müssen diese zeitnah im gesamten Netz verteilt werden. So gibt es die Möglichkeit, Konfigurationen im Problemfall weltweit „sofort” in oder außer Kraft zu setzen, wobei sich aus dem „sofort” die Anforderung ergibt, dass die Verzögerung bei der Verteilung möglichst gering sein soll. Zu berücksichtigen ist zudem, dass zu Releasewechseln der Drittsysteme große Datenmengen eingespielt werden, um die Konfigurationen für die neue Version bereitzustellen.
Das System wurde ursprünglich für einen Erstkunden spezifisch für seine Netzwerktopologie entwickelt. Neben dem Wurzelknoten in der Zentrale und den Blattknoten in den Filialen gibt es eine Reihe von Zwischenknoten, da die Netzwerksegmentierung keinen direkten Zugriff vom Wurzel- auf die Blattknoten zulässt (Abbildung 1, links). Als ein zweiter Kunde für das System gewonnen werden konnte, gab dies den Anlass für die Transformation in ein Produkt. Pro Elternknoten (Wurzel- und Zwischenknoten) sah die ursprüngliche Spezifikation eine maximale Anzahl Kindknoten vor.
Der Zweitkunde nutzt im Gegensatz zum Erstkunden keine Zwischenknoten, sodass alle Blattknoten direkt unterhalb des Wurzelknotens im Netz eingehängt sind (siehe Abbildung 1, Mitte), wobei die Anzahl der Blattknoten zunächst in etwa der erlaubten Anzahl entsprach.
Parallel zum Rollout beim Zweitkunden wurde das System bei einem Drittkunden installiert, der die Topologie des Zweitkunden übernahm, bei einem Men-gengerüst, das eher dem des Erstkunden entsprach (siehe Abbildung 1, rechts). Dieser Kunde rollte zudem das System schnell mit vielen Blattknoten – mehr als ursprünglich pro Elternknoten spezifiziert – aus, in der Annahme, dass das System das Mengengerüst abdeckt, da es beim Erstkunden stabil lief.
Abb. 1: Netzwerktopologie Konfigurationsmanager: beim Erstkunden (links), beim Zweitkunden (Mitte), beim Drittkunden (rechts)
Wie kommen die Daten ins Netz?
Die auf den Erstkunden optimierte Netzwerktopologie mit Zwischenknoten stellte sich erst nach der Transformation in ein Produkt als Ausnahme- und nicht etwa als Regelfall heraus. Alle weiteren Kunden wählten die Topologie des Zweitkunden ohne Zwischenknoten, mit mehr oder weniger vielen Blattknoten. Da in der ursprünglichen Topologie die Zwischenknoten auch zur Lastverteilung und Ausfallsicherheit genutzt werden, waren Probleme durch Überlast des Wurzelknotens vorprogrammiert. Insbesondere der schnelle Rollout beim Drittkunden führte in eine Lage, in der schnell reagiert werden musste, es zeigte sich, dass mit jedem neuen Knoten Konfigurationsdaten langsamer im Netz verteilt wurden.
Um das System an die geänderten Anforderungen anzupassen, musste zunächst das Szenario beim jeweiligen Kunden nachgestellt werden. Es galt, sowohl weiterhin die Netzwerktopologie des Erstkunden bei gleichbleibender Performance zu unterstützen als auch den Regelfall für das Produkt adäquat zu abzubilden.
Ein Netzwerk wie beim Erstkunden war im Rahmen von Performance-Messungen bereits simuliert worden, mit einem Wurzelknoten, einigen wenigen Zwischenknoten und sechzig bis hundert Blattknoten, wobei alle Knoten analog zur Ausstattung des Kunden jeweils in einer Windows-VM auf einem ESX-Host liefen. Als Testdaten wurde ein Konfigurationspaket verwendet, so wie es für die Konfiguration einer neuen Softwareversion typisch war, da dies die maximale Menge von im Netz verteilten Daten sehr gut repräsentiert. Gemessen wurde die Durchlaufzeit dieses Pakets durch das Netzwerk, also wie schnell Daten bei den Blattknoten ankommen. Netzwerkprobleme wurden während der Tests durch Drosselung des Netzwerkverkehrs (mit [NetLimiter]) oder wahrscheinlichkeitsgesteuerte Paketverluste und Verbindungsabbrüche (mit [clumsy]) simuliert.
Doch wie testet man Netzwerke mit einigen Tausend, gar Zehntausenden von Knoten? Dafür war das bestehende Setup nicht geeignet. Da die Verteilung der Daten im Netz vom Wurzelknoten ausgeht, ist es nicht möglich, diese durch Anfragen von außen mit einem Lasttestwerkzeug wie [JMeter], [Gatling] oder [Locust] zu simulieren. Auch das Anmieten von passenden Ressourcen für echte Blattknoten in einer Public-Cloud wurde bei der großen Anzahl aus Kostengründen verworfen. Als Lösung kamen schließlich sogenannte Mini-Mocks zu Einsatz, die um alle nicht notwendige Funktionalität reduziert wurden, sich jedoch dem Wurzelknoten gegenüber als vollwertige Kindknoten ausgaben.
Als Problem stellten sich nach der Analyse der Messdaten und der Untersuchung mit einem Profiler [JProfiler] insbesondere die Punkt-zu-Punkt-Verbindungen von einem Elternknoten zu seinen Kindknoten heraus. Im Laufe der Weiterentwicklung wurden verschiedene Ansätze verfolgt und wieder verworfen.
Die erste Implementierung basierte auf einer Messaging-Lösung auf Basis von JMS (Java Messaging Service), wobei die Messaging-Lösung die Verteilung übernahm. Diese wurde zunächst ergänzt durch eine zusätzliche Stage, in der Daten in der Datenbank zwischengespeichert wurden, um das Einspielen und Speichern der Daten von der Verteilung zu entkoppeln. Die Entkopplung verringerte insbesondere die Wartezeit bis zur Rückmeldung des erfolgreichen Einspielens von Daten an den Client. Sie brachte aber keinerlei Verbesserung für das Verteilungsproblem, im Gegenteil – durch die zusätzliche Speicherung und das Auslesen der Daten für die Weiterleitung an die anderen Knoten verlangsamte sich die Verarbeitung sogar noch.
Nachdem der erste Verbesserungsversuch nur bedingt tragfähig war, wurde in einem zweiten Schritt die Messaging-Lösung durch Webservice-Calls ersetzt. Die Kontrolle der Verteilung der Daten an die Kindknoten konnte so deutlich besser gesteuert werden und brachte gerade im Fall vieler Offline-Knoten einen Performance-Gewinn, indem Kontaktversuche zu diesen in immer größeren Abständen erfolgten und Knoten nun gezielt als offline markiert und von der Verarbeitung ausgenommen werden konnten. Trotzdem kam diese Implementierung bei zunehmender Anzahl von Kindknoten an ihre Grenzen.
An diesem Punkt wurde noch einmal komplett neu gedacht. Bisher war davon ausgegangen worden, dass der Elternknoten die Datenpakete für die Kindknoten filtern muss, da nicht alle Daten überall benötigt wurden. Das belastete den Wurzelknoten enorm und führte zu einem steigenden Aufwand für jeden weiteren Blattknoten im Netz. Bei einer Topologie mit Zwischenknoten, bei der die Anzahl von Kindknoten pro Elternknoten relativ konstant und eher klein ist, fällt dies weniger ins Gewicht als beim Regelfall mit einer hohen Schwankungsbreite der direkt mit dem Wurzelknoten verbundenen Blattknoten. Verwirft man die Filterung im Elternknoten und verlagert sie in die Kindknoten, hat dies zwei Effekte:
- Erstens verteilt sich der Aufwand der Filterung nun auf viele Instanzen des Systems statt nur auf eine.
- Zweitens lässt die Verteilung nun auch eine Art Broadcast zu, da der Wurzelknoten die Datenpakete nur noch einmalig bereitstellen muss und die Kindknoten diese selbstständig abholen können, wenn sie an dem Datenpaket interessiert sind.
Die Verteilung erfolgte nun über Apache Kafka [Kafka] und verlagerte die Schnittstelle zwischen dem Wurzelknoten und den Blattknoten damit in ein für solche Anwendungen optimiertes Drittsystem (siehe Abbildung 2).
Das Beispiel zeigt sehr gut, wie das System im Laufe seiner inzwischen etwa zehnjährigen Lebenszeit mehrere Evolutionsschritte vollzogen hat. Einige der realisierten Lösungen boten nur zeitweise Abhilfe. Insbesondere das Festhalten an nur scheinbar unumstößlichen Annahmen wie hier der Filterung der Daten vor der Weitergabe kann zuweilen einer besseren Lösung im Weg stehen.
Da ein gleichzeitiges Ausrollen einer neuen Verteilungslösung bei einem weltweit verteilten System nicht möglich ist, existierten aus Gründen der Abwärtskompatibilität zum Teil zwei verschiedene Implementierungen gleichzeitig im Netz. Verbesserungen in der Performance wurden für den Kunden so manchmal nur mit deutlicher Verzögerung sichtbar. Es zeigt sich gerade in diesem Fall, dass auf Performance-Probleme besser frühzeitig reagiert werden sollte.
Abb. 2: Netzwerktopologie Konfigurationsmanager mit Kafka
… und Baustellen
Ein Baustellenkoordinationssystem dient der gemeinsamen Planung von Baumaßnahmen verschiedener Organisationen. Das System hilft dabei, unnötige Arbeiten wie das mehrfache Aufreißen des Straßenbelags in der gleichen Straße zu vermeiden, erhöhte Staugefahr, zum Beispiel durch parallele Arbeiten auf Ausweichrouten, zu identifizieren und Synergien zu erkennen. Dabei wurde es ursprünglich konzipiert, um größere Baumaßnahmen langfristig zu planen und zu koordinieren. Das Client-Server-System verwaltet dabei nicht nur eigene Baumaßnahmen, sondern kann sich mit anderen Servern synchronisieren und zudem Baumaßnahmen aus Drittsystemen importieren. Baumaßnahmen werden in den Clients nicht nur tabellarisch, sondern auch auf einer Karte dargestellt, die in Echtzeit Filterung und Skalierung anbietet. Insbesondere auf Touchtischen läuft der Client in hoher Auflösung. Durch den Erfolg des Systems ergaben sich gleich mehrere neue Anforderungen. So sollte das System zusätzlich aktuelle Stauprognosen erstellen können und auch kurzfristig geplante Baumaßnahmen verwalten. Auch in diesem Fall blieben die neuen Anforderungen nicht ohne Einfluss auf die Performance des Systems.
Darf’s ein bisschen mehr sein?
Die neuen Anforderungen, insbesondere der Wunsch nach der Verwaltung kurzfristiger Baumaßnahmen, führt perspektivisch zu einer Verzehnfachung der gleichzeitig zu verarbeitenden Datenmengen. Dabei muss nicht nur der Client diese Datenmenge korrekt und performant anzeigen können, sondern auch die Auslieferung der Daten an den Client muss ausreichend schnell erfolgen. Zusätzlich führte die neue Anforderung dazu, dass mehr Nutzer das System verwenden würden als bisher.
Statt zu warten, bis das System diese Grenze erreicht, um dann auf mögliche Performance-Probleme zu reagieren, entschloss sich das Entwicklungsteam, diese Anforderungen proaktiv mit dem bestehenden System zu testen und notwendige Änderungen bereits einzuplanen und umzusetzen.
Da unklar war, wie Server und Client auf die deutlich erhöhte Anzahl von Baumaßnahmen reagieren würden, mussten diese in entsprechend hoher Anzahl in das System gebracht werden. Dafür wurde ein Simulator implementiert, der eine variable Anzahl von Baumaßnahme im System erzeugen konnte, um mit unterschiedlichen Datenmengen testen zu können.
Abbildung 3 zeigt die aktuell bestehende Anzahl von Baumaßnahmen (links) im Gegensatz zu den Baumaßnahmen aus der Simulation mit der höchsten Anzahl (rechts) im Client auf der Karte, wobei jeder Punkt für eine Baumaßnahme steht. Verschiedene festgelegte Szenarien wurden mit Clients gegen Server mit unterschiedlicher Anzahl von Baumaßnahmen durchgespielt, um die vom Nutzer erfahrene Performance miteinander zu vergleichen.
Abb. 3a : zur aktuellen Anzahl von Baumaßnahmen
Abb. 3b: Simulierte Baumaßnahmen
Dabei wurden typische Aufgaben wie Verschieben und Skalieren der Karte, Filtern, Öffnen, Bearbeiten von Baumaßnahmen oder Erstellen von Koordinationsszenarien herangezogen.
Vor der Messung mit einem Lasttestwerkzeug (hier: Gatling) gegen den Server galt es, verschiedene Fragen zu beantworten und Festlegungen zu treffen.
Was sind die zu messenden Szenarien?
Es galt, realistische, für den Endnutzer relevante Durchläufe durch das System zu erstellen. Statt nur einen bestimmten API zu bespielen, wurde eine typische Stunde der Nutzerinteraktion mit dem System herangezogen. Diese sogenannte Average-Hour wurde im Lasttestwerkzeug als Szenario abgebildet.
Wie viele Anfragen pro Zeiteinheit muss das System verarbeiten?
Dabei diente sowohl die typische als auch die maximale Anzahl als Grundlage für die durch das Werkzeug simulierte Last.
Wie schnell soll die maximale Anzahl der Nutzer erreicht werden?
Auch im realen System greifen in der Regel nicht alle Nutzer gleichzeitig auf das System zu, allerdings erfolgen zu bestimmten Zeitpunkten, zum Beispiel bei der Synchronisation mit anderen Servern, Updates der Daten in allen geöffneten Clients. Dies wurde im Werkzeug durch die Ramp-up-Strategie definiert.
Wie ist das Zielsystem ausgestattet?
Um im Test die unterschiedlichen Umgebungen zu simulieren – bei den Endkunden sind sowohl Windows-Server als auch in Docker betriebene Linux-Server im Einsatz –, wurden Testumgebungen per [Terraform] in einer Public-Cloud automatisiert aufgesetzt.
Abbildung 4 zeigt beispielhaft zwei Szenarien für eine typische Stunde im Lebenszyklus des Baustellenkoordinationssystems. Eine skalierbare Anzahl von Nutzern liest und ändert Daten und koordiniert geplante Baustellen, wobei die Aktionen aufgrund der Ramp-up-Einstellungen zeitversetzt erfolgen. Zusätzlich wird in periodischen Abständen die Datensynchronisation simuliert, um deren Einfluss auf die Performance zu ermitteln.
Die Auswertung der Messdaten für Client und Server ergab, dass insbesondere der Server bei den zu erwartenden Mengengerüsten und Nutzerzahlen an seine Grenze kommen würde. Es ging also primär darum, Optimierungen auf Serverseite zu verfolgen, da hier das größte Potenzial für substanziellen Performance-Gewinn lag. Anschließend wurden auch hier mit einem Profiler potenzielle Schwachstellen im Code identifiziert, um diese gezielt anzugehen. Insbesondere das Lesen komplexer Objekte aus der Datenbank führte zu Problemen im Antwortverhalten. Obwohl zur Verringerung der Datenmenge Daten, die auf der Oberfläche aktuell nicht sichtbar sind, erst bei Bedarf nachgeladen werden, wird unter anderem für die Darstellung von Baumaßnahmen auf der Karte bereits initial eine Reihe von Daten benötigt, beispielsweise Zeiträume oder Verkehrsführungen. Um Datenbankzugriffe zu reduzieren, wurden zwei Cache-Varianten realisiert:
- der Hibernate-Cache und
- ein eigener DTO-Cache für die betroffenen Objekte.
Abb. 4: Szenarien für die Average-Hour im Baustellenkoordinationssystem
Der DTO-Cache stellt die Objekte dabei bereits inklusive der Aufbereitung der Daten zur Auslieferung an die Clients zur Verfügung. Beide Caches können durch Konfiguration wahlweise einzeln oder gemeinsam aktiviert und somit getestet werden. In Tabelle 1 misst der Lasttest den Effekt der unterschiedlichen Cache-Varianten. Es zeigt sich, dass der Hibernate-Cache im Vergleich zum System ohne Cache keinen signifikanten Vorteil bringt, sondern nur der implementierte DTO-Cache, der deshalb eingesetzt wird. Das Beispiel zeigt, dass Caching eine gute Lösung für Performance-Probleme sein kann. Aus unserer Erfahrung sollten Caches jedoch gezielt nur bei Bedarf eingesetzt werden, da sie die Komplexität des Systems durch zusätzliche Trigger für das Aufbauen und Invalidieren des Caches erhöhen.
30 User | |||
---|---|---|---|
Cache-Variante | Ø | Min | Max |
ohne Caching | 14656 | 4002 | 23737 |
DTO-Caching | 3711 | 1040 | 6300 |
Hibernate-Caching | 13152 | 3817 | 22065 |
beide Caches | 3735 | 940 | 6539 |
50 User | |||
Cache-Variante | Ø | Min | Max |
ohne Caching | 20864 | 4265 | 37254 |
DTO-Caching | 5982 | 1314 | 10405 |
Hibernate-Caching | 20624 | 4238 | 37507 |
beide Caches | 5933 | 1226 | 10252 |
Fazit
Die vorgestellten Systeme stehen exemplarisch für die Performance-Herausforderungen, denen ein System während der Weiterentwicklung unterworfen ist. Oft werden bei der Erstimplementierung Annahmen getroffen, die bei geänderten Anforderungen nicht mehr tragfähig sind.
Im Idealfall werden Veränderungen mit dem Entwicklungsteam abgestimmt, manchmal ist die Veränderung schleichend und die Entwicklung erfährt nur durch Zufall davon. Spezifizierte Grenzen des Systems werden ausgereizt, weil es möglich ist. Der Kontakt zum Kunden darf deshalb nicht abreißen, durch direkte Kommunikation, aber auch durch technische Lösungen. Wenn es für den Kunden akzeptabel ist, ermöglichen moderne Monitoringsysteme, Daten gezielt und anonymisiert zu sammeln, entweder zentral beim Produktentwickler oder lokal beim Kunden mit der Möglichkeit des Zugriffs durch die Entwicklung. Die Monitoringdaten können bei der Erkennung von Engpässen helfen und sich anbahnende Überlastungen des Systems, zum Beispiel durch immer mehr Knoten im Netz, frühzeitig aufdecken. Für beide Systeme, Konfigurationsmanager sowie Baustellenkoordinationssystem, sind Monitoringsysteme angebunden. Auch wenn sich Performance-Messungen und Lösungen für Performance-Probleme im Einzelfall unterscheiden, so zeichnen sich doch Muster ab. Für die Optimierung finden sich in den vorgestellten Beispielen typische Muster wieder: weniger Datenbankzugriffe durch Caching, weniger Netzwerkverkehr durch optimierte Verteilung von Daten, Nachladen von Daten im Hintergrund und natürlich Einsatz effizienterer Algorithmen – immer unter Berücksichtigung, was für das System aktuell das Angemessene ist.
Performance-Optimierung ist eine stetige Aufgabe, die wie das System selbst einer ständigen Aufmerksamkeit bedarf und Änderungen unterworfen ist. Gemeldete Performance-Probleme sollten ernst genommen und zeitnah behoben werden. Ein wichtiger Aspekt bei der Vorbeugung ist die realistische Einschätzung von Betriebsumgebung, Mengengerüsten und Kundenverhalten. Ergänzt durch Performance-Messungen, idealerweise in die Build-Pipeline eingebunden, sind dies wichtige Bausteine zur Sicherstellung der Softwarequalität und damit der Kundenzufriedenheit. Die Performance steht jedoch im Spannungsfeld mit anderen Qualitätsmerkmalen. Performance-Optimierungen können die Komplexität der Software erhöhen und damit die Wartbarkeit erschweren – gerade bei einem Produkt mit langer Lebenszeit und stetigem Änderungsbedarf ein nicht unerheblicher Faktor. Optimierungen sollten deshalb mit Fingerspitzengefühl erfolgen, denn nicht immer ist auch das letzte bisschen mehr Performance das Beste für das Produkt.
Weitere Informationen
[clumsy]
https://jagt.github.io/clumsy/
[Gatling]
https://gatling.io/
[JMeter]
https://jmeter.apache.org/
[JProfiler]
https://www.ej-technologies.com/products/jprofiler/overview.html
[Kafka]
https://kafka.apache.org/
[Locust]
https://locust.io/
[NetLimiter]
https://www.netlimiter.com/
[Terraform]
https://www.terraform.io/