Bachelorarbeit Vergleich von Spring Boot und Buffalo für die Entwicklung von Microservices
←
→
Transkription von Seiteninhalten
Wenn Ihr Browser die Seite nicht korrekt rendert, bitte, lesen Sie den Inhalt der Seite unten
Bachelorarbeit Vergleich von Spring Boot und Buffalo für die Entwicklung von Microservices vorgelegt von Leander Ludwig Günther Dreier Erstprüfer Prof. Dr. rer. nat. Franz Regensburger Zweitprüfer Prof. Dr. Sebastian Apel Betreuer Christian Sarow Ausgabedatum 15. Juni 2021 Abgabedatum 05. August 2021 Studiengang Informatik Fakultät Informatik Technische Hochschule Ingolstadt
ii
Danksagung Ich möchte mich bei meinem Prüfer Franz Regensburger und bei meinem Betreuer Christian Sarow für ihre Geduld und Tipps bedanken, sowie speziell Christian für das Durchlesen mehrerer Entwürfe. Weiter möchte ich mich bei meinen Eltern und Freunden für die generelle Unterstützung bedanken, die mir die nötige Kraft gegeben hat, diese Arbeit fertigzustellen. Insbesondere möchte ich mich bei meinen Freunden Regina Schneider und Dominik Okwieka für das Durchlesen eines Entwurfs meiner Arbeit aus der Perspektive eines Außenstehenden bedanken. Zuletzt möchte ich mich noch bei meinem Freund und ehemaligen Kollegen Philip Schneider für die tolle Zusammenarbeit am EMU Projekt bedanken. iii
Abstract Die Relevanz der Microservicearchitektur für die Softwareentwicklung hat in den vergangenen Jahrzehnten – nicht zuletzt wegen der steigenden Verbreitung von Cloud-Computing – einen stetigen Zuwachs erfahren. Bei der Entwicklung eines Microservices – gerade auch im Bereich der Webanwendungen – werden häufig Frameworks eingesetzt, um repetitive Arbeit zu vermeiden. Ein Beispiel hierfür ist das auf dem Spring Framework basierende Spring Boot Projekt, welches von VMware für die Programmiersprache Java entworfen wurde und auf dem Markt häufig zum Einsatz kommt. Daneben existieren noch weitere Lösungen in verschiedenen Sprachen, wie beispielsweise das relativ neue und weniger bekannte Framework Buffalo des Entwicklers Mark Bates in der Programmiersprache Go. In dieser Arbeit werden Spring Boot und Buffalo bezüglich ihrer Eignung für die Entwicklung von Microservices miteinander verglichen. Als Vergleichsgrundlage wird dafür jeweils ein eigens geschriebenes Programm verwendet, welches typische Workloads in drei unterschiedlichen Diensten implementiert. Als Inspiration der Use-Cases und Quelle für Beispieldaten dient eine in der Sulzer GmbH von mir mitentwickelte interne Anwendung zur digitalen Verwaltung von Nutzerdaten. Die Evaluation erfolgt auf Basis des Analytischen Hierarchieprozesses nach Thomas L. Saaty und wird für drei verschiedene Anwendungsfälle getrennt vorgenommen. Betrachtet werden die durchschnittliche Laufzeit der Dienste und deren Standardabweichung, das Supportangebot durch den Anbieter des Frameworks, die Größe der zugehörigen Onlinecommunity, und Erfahrungen bei der Entwicklung mit dem Framework. Benchmarks zur Laufzeit werden sowohl auf Windows- wie auch auf Unix-Systemen durchgeführt. Während Spring Boot auf allen Geräten ähnliche Ergebnisse erzielt, laufen die Benchmarks für Buffalo auf Unix-Systemen durchschnittlich konsistenter ab. Dennoch geht Spring Boot für alle drei Anwendungsfälle als Gewinner hervor, wobei es 60, 6% bis 76, 0% der Punkte beanspruchen kann. v
Inhaltsverzeichnis Danksagung iii Abstract v 1 Einleitung 1 1.1 Relevanz und Motivation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1 1.2 Zielsetzung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2 1.3 Aufbau der Arbeit . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2 2 Bestehendes System (EMU) 5 2.1 Motivation von EMU . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 5 2.2 Eingesetzte Technologien . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 5 2.3 Nutzerstand . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 6 3 Testaufbau 7 3.1 Auswahl der Services . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 7 3.1.1 Datapoint-Service . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 8 3.1.2 Stats-Service . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 9 3.1.3 Json-Service . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 10 3.1.4 Bewertung der Services . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 12 3.2 Auswahl der Frameworks . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 14 3.2.1 Spring Boot . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 15 3.2.2 Buffalo . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 15 3.3 Schnittstellen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 16 4 Evaluation 19 4.1 Analytischer Hierarchieprozess nach Saaty . . . . . . . . . . . . . . . . . . . . . . . 19 4.2 Definition der Evaluationskriterien . . . . . . . . . . . . . . . . . . . . . . . . . . . 20 4.2.1 Harte Kriterien . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 20 4.2.2 Weiche Kriterien . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 21 4.3 Vergleich von Spring Boot und Buffalo anhand der Kriterien . . . . . . . . . . . . . 22 4.3.1 Laufzeit . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 22 4.3.2 Professioneller Support . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 27 4.3.3 Größe der Stack Overflow-Community . . . . . . . . . . . . . . . . . . . . . 30 4.3.4 Entwicklung mit Framework . . . . . . . . . . . . . . . . . . . . . . . . . . . 31 4.4 Definition der Anwendungsfälle und Gewichtung der Evaluationskriterien . . . . . . 35 5 Fazit 39 6 Ausblick 41 A Anhang – Diagramme und statistische Auswertungen zur Laufzeitmessung ix vii
Inhaltsverzeichnis B Anhang – Programmcode zur Datenbankkonfiguration xiii C Anhang – Sonstige Abbildungen und Tabellen xix Abkürzungsverzeichnis xxiii Abbildungsverzeichnis xxv Quellcodeverzeichnis xxvii Tabellenverzeichnis xxix Webseitenverzeichnis xxxi Literaturverzeichnis xxxv Eidesstattliche Erklärung xxxvii viii
1 Einleitung Write programs that do one thing and do it well. Write programs to work together [58]. Dough McIlroy über die Unix-Philosophie Im Verlauf der letzten Jahrzehnte ist die Bedeutung von verteilter Entwicklung und Software im Gegensatz zur Verwendung einer monolithischen Architektur stetig gestiegen. Bei dem klassischen, monolithischen Softwareaufbau werden verschiedenen Dienste zu einem Paket zusammengefasst, welches dann insgesamt alle erwünschten Aufgaben des Produkts leistet. Dieses Paket kann auf Grund von internen Abhängigkeiten meist nur im Ganzen deployt werden. Da auf diese Weise ent- wickelte Systeme häufig nicht den Qualitätsanforderungen von zum Beispiel Echtzeitanwendungen wie Videotelefonie genügen, wurde bereits im Jahr 1991 von der Object Management Group (OMG) die Common Object Request Broker Architecture (CORBA) veröffentlicht. CORBA ist ein Stan- dard für sogenannte Middleware, welche eine Abstraktion von der Wahl der Programmiersprache und des Betriebssystems liefert und durch eine standardisierte Schnittstelle Kommunikation von Programmen über ein Netzwerk und einfachere Wiederverwendung von Software erlaubt. [14][60, S. 56] Eine moderne Weiterentwicklung dieses Ansatzes ist die Microservicearchitektur. Dieser Um- schwung wird in zahlreichen Studien belegt und kann unter anderem als eine Folge des immer wichtiger werdenden Cloud-Computings gesehen werden [3, 48]. Das Prinzip der Microservices sieht vor, jeden Dienst als eigenständige Anwendung zu betrachten, welche genau eine bestimmte Aufga- be bearbeitet. Mehrere solcher Microservices können dann kommunizieren und zusammenarbeiten, um ein größeres Ziel zu erreichen. Zwischen den einzelnen, kleinen Anwendungen bestehen üblicher- weise wenig bis keine Abhängigkeiten, wodurch insbesondere Weiterentwicklung und Deployment individuell stattfinden können. Auch steigt durch diesen Entwicklungsansatz die Skalierbarkeit einer Anwendung, da Programmteile, welche eine höhere Last erfahren, zum Beispiel auf mehrere Server geklont werden können, ohne dass Änderungen am Rest der Anwendung nötig werden. Zur Kommunikation zwischen Microservices, speziell bei einem Client-Server-Aufbau, werden häufig Representational State Transfer (REST)-Schnittstellen eingesetzt. Durch die Zustandslo- sigkeit dieses Paradigmas können Ressourcen gespart werden, während die Einheitlichkeit von Schnittstellen deren Nutzung sowie den Austausch einzelner Bausteine vereinfacht. So ergab sich beispielsweise bei einer Studie aus dem Jahr 2019, dass sich 6,41% der technischen Beiträge im Bereich Microservices auf StackOverflow mit dem Thema REST beschäftigten [4, S. 257]. 1.1 Relevanz und Motivation Zur Umsetzung von RESTful Services werden oftmals Frameworks benutzt. Die Auswahl ist hierbei mehr als reichhaltig, mit Optionen, welche beispielsweise auf Java, JavaScript, Python oder Go basieren (siehe zum Beispiel Artikel von Kurmi [36], Brannan [10] oder Purkayastha [51]). Man 1
1 Einleitung findet dabei sowohl bereits weiter verbreitete Angebote wie Spring Boot oder Django als auch neuere, innovative Ansätze wie Go Buffalo oder FastAPI. All diese Möglichkeiten zu vergleichen, um das für den Anwendungsfall am besten passende Angebot zu finden, kann mitunter sehr aufwendig sein, gerade dann, wenn man mit der Zeit gehen und neueren Frameworks eine Chance geben möchte. Inspiration für die vorliegende Arbeit war es, dass eine neu entwickelte Elektronische Mitar- beiterrechteverwaltung (EMU) hinsichtlich verschiedener Parameter untersucht werden sollte. So können aus der Datenbank von EMU unter anderem die Anzahl der aktiven Mitarbeiter, die Anzahl der verwalteten Projekte und die Anzahl vergebener Berechtigungen angefragt werden. Diese und weitere Kennzahlen sollten nun regelmäßig gesammelt und die gesammelten Daten aggregiert werden. Zu diesem Zweck schien es sinnvoll, verschiedene Microservices zu schreiben, welche mithilfe eines Calls über das Hypertext Transfer Protocol (HTTP) angesprochen werden können. EMU wurde bereits mit Spring Boot geschrieben, zudem wird in der Firma auch viel mit Java gearbeitet, wodurch es sich anbieten würde, auch die neuen Dienste in diesem Framework zu schreiben. Mit einem Auge auf die Innovationen der letzten Jahre stellte sich nun jedoch die Frage, ob nicht eine besser geeignete Sprache, beziehungsweise ein besser geeignetes Framework unter den moderneren Lösungen zu finden ist. Diese Frage motivierte die Gegenüberstellung und Evaluation zweier Frameworks im Rahmen dieser Arbeit. 1.2 Zielsetzung Das Ziel dieser Arbeit ist es, Spring Boot – ein etabliertes Framework, geschrieben in der weit verbreiteten Sprache Java, veröffentlicht von einem bekannten Unternehmen – mit Buffalo – einem Framework mit kleinerer Community und kleinem, unabhängigem Team, entwickelt für eine Sprache, die explizit als Alternative zu Sprachen wie Java geschaffen wurde [42, S. 101] – zu vergleichen. Als Codebeispiele für diese Gegenüberstellung sollen drei Dienste dienen, die so auch als Microservices eingesetzt werden könnten. Evaluationskriterien, unterteilt in harte und weiche Kriterien, sollen mit dem analytischen Hierarchieprozess nach Saaty ausgewertet werden. Diese Merkmale sollen messbare und statistisch auswertbare Metriken wie die Programmlaufzeit, aber auch Erlebnisse bei der Arbeit mit der Software beinhalten. Um eine bessere Übersicht zu liefern, soll auf drei unterschiedliche Anwendungsfälle eingegangen werden, welche unter anderem verschiedene Betriebssysteme abdecken. Final soll ein Fazit gezogen werden, das erläutert, in welchen Fällen welches Framework vorzuziehen ist und auf was bei der Auswahl geachtet werden sollte. 1.3 Aufbau der Arbeit Im Folgenden wird zunächst in Kapitel 2 das Projekt vorgestellt, welches sowohl das Thema dieser Arbeit inspiriert als auch Beispieldaten dafür geliefert hat. Es folgt in Kapitel 3 eine Beschreibung der ausgewählten Frameworks, der programmierten Services und der verwendeten Schnittstellen sowie eine Begründung, warum ich mich gerade für diese entschieden habe. Zusätzlich wird in Unterpunkt 3.1.4 eine Evaluation der implementierten Dienste vorgenommen, um aufzuzeigen, wie sehr diese dem klassischen Modell eines Microservices entsprechen. Kapitel 4 bildet mit der Bewertung der Frameworks den Hauptteil der Arbeit. Auf die Erklärung des verwendeten Vergleichsprozesses folgt hier die Definition harter wie weicher Evaluationskriterien, anhand derer in Abschnitt 4.3 die Gegenüberstellung der beiden betrachteten Optionen vorgenommen 2
1.3 Aufbau der Arbeit wird. Abschließend findet sich die Definition dreier Anwendungsfälle, welche zur Gewichtung der Kriterien und zu einer differenzierten Einschätzung der Frameworksleistung beitragen, sowie die finale Bepunktung nach dem eingangs beschriebenen Prozess. Den Schluss der Arbeit bilden ein Fazit, das die gewonnenen Ergebnisse zusammenfasst und deren Bedeutung aufzeigt, und ein Ausblick auf mögliche Weiterführungen des angeschnittenen Themenkomplexes in den Kapiteln 5 und 6. 3
1 Einleitung 4
2 Bestehendes System (EMU) 2.1 Motivation von EMU Bei der Arbeit mit Rechner- und Softwaresystemen von Kundenunternehmen werden für letztere verschiedene Zugänge und Berechtigungen benötigt. Diese Berechtigungen müssen beantragt und meist auch regelmäßig erneuert werden. Da vor dem Ablauf einer Berechtigung häufig keine Erinnerung darüber von Kundenseite erfolgt und der Beantragungsprozess unter Umständen mehrere Wochen dauern kann, ist eine frühzeitige Bearbeitung essentiell. EMU wurde ins Leben gerufen, um diesen Prozess zu vereinheitlichen und zu zentralisieren. Wurde früher noch für jedes Team ein individuell angelegter Datensatz ohne festes Schema gepflegt, so werden jetzt in EMU alle relevanten Daten in einer zentralisierten Datenbank zusammengefasst. EMU steht dabei für Elektronische Mitarbeiterrechteverwaltung beziehungsweise Electronical Management of Userdata. Auch der Beantragungsprozess von Rechten wurde vereinfacht: Benötigt ein Mitarbeiter einen neuen Zugang, kann dieser von der zuständigen Projektleiterin direkt in der Anwendung angefragt werden und ist von der Assistenz der Niederlassungsleitung leicht einzusehen. Auch der Zustand einer Berechtigung, also ob sie bereits bei der Kundenabteilung beantragt wurde, ob sie genehmigt wurde, ob sie funktioniert oder nicht, wird gespeichert und kann mit wenigen Mausklicks eingetragen und geprüft werden. Entwickelt wurde EMU bis Ende 2020 hauptsächlich von einem Auszubildenden und mir, stets in enger Zusammenarbeit mit den Teamleitern, den führenden Personen der Geschäftsfelder und der internen IT. Seitdem wird es von einem inzwischen größeren Team weiterentwickelt und gepflegt. 2.2 Eingesetzte Technologien Bei der Frage nach der zu verwendenden Technologie fiel die Wahl der Sprache schnell auf Java, da diese sowohl von allen initialen Entwicklelnden am Projekt am besten beherrscht wurde als auch im Unternehmen allgemein weit verbreitet ist. Mit dem Ziel der Arbeitsersparnis und um Fehler bei der Auswahl von Bibliotheken zu vermeiden, wurde nach einem Framework Ausschau gehalten. Architektonisch wurde festgelegt, dass eine Webanwendung entwickelt werden soll, um die Auslieferung und Wartung der Software zu vereinfachen und damit einen zügigen Updatezyklus zu gewährleisten. Mit dieser Bedingung im Hinterkopf habe ich mich schließlich, aufgrund der großen Community und weil es im Unternehmen bereits in anderen Projekten eingesetzt wurde, für Spring Boot als Framework für das Projekt entschieden. Für die Datenhaltung schien, wegen der erwarteten relationalen Struktur zwischen Objekten im Projekt, ein objektrelationales Datenbanksystem die beste Lösung. Es war das Anliegen der Entwickler, ein quelloffenes Produkt auszuwählen, und mit diesem Rahmen viel die Wahl auf PostgreSQL. 5
2 Bestehendes System (EMU) 2.3 Nutzerstand Zu Beginn der Entwicklung von EMU wurde es versuchsweise nur für ein Projekt mit etwa 30 Kolleginnen und Kollegen benutzt und getestet. Dort war es so erfolgreich, dass heute, nach circa eineinhalb Jahren Entwicklungszeit, über 15 Projekte mit insgesamt über 450 Nutzerinnen und Nutzern und über 600 Berechtigungen verwaltet werden. Die Anwendung wird in mehreren Niederlassungen eingesetzt und ist inzwischen fester Bestandteil der Ein- und Austrittsprozesse von Angestellten. 6
3 Testaufbau Um eine Grundlage für die Bewertung und Gegenüberstellung der zu testenden Frameworks zu schaffen, habe ich jeweils eine Anwendung in Spring Boot und Buffalo geschrieben. Die beiden Programme implementieren jeweils drei Funktionalitäten, welche über eine REST-Schnittstelle mit HTTP angesprochen werden können. Es wurden also nicht echte, eigenständige Microservices implementiert. Stattdessen wurde eine Architektur gewählt, welche einige Eigenschaften davon, wie das Aufrufen über HTTP oder eine gewisse Trennung von Diensten – hier zum Beispiel durch die Aufteilung in eigene Controller – übernimmt, während der Komfort einer einzelnen Binärdatei pro Sprache gewahrt bleibt. Um die drei unten beschriebenen Dienste, welche im Laufe dieser Arbeit untersucht werden, effizient testen zu können, wurde außerdem ein Service zum Benchmarking entwickelt, der die relevanten Funktionen mehrfach hintereinander ausführen und dabei deren Laufzeit messen kann. Ich möchte anmerken, dass ich zuerst die Spring Boot- Anwendung geschrieben und dann den Buffalo Gegenpart daran orientiert habe, da Java mir vertrauter als Go war. Der strukturelle Aufbau der Anwendungen im Dateisystem ist in Abbildung 3.1 zu sehen. Für die Spring Boot-Anwendung beinhaltet das emu-Paket die Teile der EMU-Anwendung, welche auch für die hier gewünschten Funktionalitäten relevant sind und daher übertragen werden mussten. Im stats-Paket befinden sich Models und Repositorys der eigentlichen Anwendung in den jeweiligen Unterordnern. BenchmarkController, DatapointController, StatController und JsonStatController – welche die Implementationen der verschiedenen Dienste enthalten – sind schließlich im Ordner controller zu finden. Die Struktur des Buffalo-Projekts ist im Gegensatz dazu mit nur zwei Ebenen deutlich flacher. Die Dienste, welche in Java separat in einem controller-Paket untergekommen sind, finden sich hier, zusammen mit zum Beispiel dem Router, in actions. Alle Models, auch die zum Verarbeiten der EMU-Daten, werden in models zusammengefasst. Die Unterordner config, fixtures, grifts, locales und migrations werden von Buffalo generiert, aber hier nicht verwendet. 3.1 Auswahl der Services Bei der Auswahl der zu implementierenden Dienste standen zwei Faktoren im Vordergrund: Zum einen sollten sie sich an dem gegebenen Use-Case, sprich der Auswertung von Nutzungsdaten aus EMU, orientieren. Es sollte betrachtet werden, wie sich bestimmte Kennzahlen aus der Datenbank von EMU im zeitlichen Verlauf verändern. Dies bedeutet, dass die von mir geschriebene Anwendung mit kleinen Änderungen auch im Produktivbetrieb eingesetzt werden könnte. Solche geringen Anpassungen wären zum Beispiel notwendig, um den Benchmark-Controller zu entfernen oder um die korrekte (Live-) Datenbank anzubinden. Ein Ziel dieser Entscheidung war es, für das Testen der Anwendung und vor allem beim Evaluieren deren Laufzeit mit realistischen Beispieldaten arbeiten zu können. Auf diese Weise wurde versucht, ein aussagekräftigeres Ergebnis als mit synthetischen Testdaten zu erreichen. Ein weiteres Ziel war es, bei der späteren Evaluation der Services konkrete Sollwerte zu haben. Bei dieser Orientierung an einem realen Anwendungsfall wurde jedoch darauf geachtet, dass sie nicht auf Kosten der Signifikanz der Messergebnisse beziehungsweise zum Nachteil des folgenden Punktes geschieht. 7
3 Testaufbau thesis_java gradle src main java thesis_go de.thi.dreier.thesis actions controller config emu fixtures repository grifts enums locales model migrations service models stats model (b) Buffalo-Anwendung repository resources test (a) Spring Boot-Anwendung Abbildung 3.1: Paketstrukturen der Anwendungen Zum anderen bestand die Absicht, eine gewisse Diversität unter den zu erstellenden Diensten zu erreichen. Es wurde daher darauf geachtet, dass sich jeder Service auf einen separaten, häufig ge- nutzten Bestandteil von Microservices spezialisiert. Häufige Anwendungsfälle für Microservices sind laut verschiedenen Onlineressourcen neben der Migration von Monolithen Big-Data-Anwendungen und Datenverarbeitung in Echtzeit [20, 35, 45]. Ein besonderes Augenmerk wurde daher auf die Verwendung von Datenbanken und die Abfrage sowie Verarbeitung – in diesem Anwendungsfall – großer Datenmengen gelegt. 3.1.1 Datapoint-Service Der erste von mir im Rahmen dieser Arbeit geschriebene Service, genannt Datapoint-Service, fragt verschiedene Daten aus der EMU-Datenbank – einer objektrelationalen PostgreSQL-Datenhaltung – an, bereitet diese in einem Datapoint-Objekt auf und speichert dieses in einer lokalen, doku- mentenbasierten Mongo-Datenbank. Ein Datapoint spiegelt damit einen Snapshot von EMU zu einem bestimmten Zeitpunkt wieder. Die angefragten Werte lassen sich hierbei in zwei Kategorien 8
3.1 Auswahl der Services unterteilen: Solche, die projektspezifisch zu werten und jene, die projektübergreifend gültig sind. Beispiele für projektspezifische Kennzahlen sind die Anzahl vergebener Berechtigungen pro Projekt, die Anzahl der verfügbaren Berechtigungen im Projekt oder die Anzahl der Mitarbeiter in einem Projekt. Im Datapoint-Objekt werden diese als Map der Form {Projektname als string} → {Wert für dieses Projekt als long / int64} abgelegt. Projektübergreifend werden hingegen beispielsweise die Anzahl der erfolgreich und erfolglos durch die Anwendung verschickter E-Mails und die Anzahl aufgetretener Frontend-Fehler abgefragt, welche als einfache long beziehungsweise int64 Werte gespeichert werden können. Zusätzlich zu diesen Daten wird als Primärschlüssel noch ein Zeitstempel hinterlegt, der den Zeitpunkt der Anfrage angibt. Es kann somit nicht mehrere Datapoints mit dem selben Zeitstempel geben. Da bei diesem Modell nur von einem Knoten der Anwendung ausgegangen wird, über welchen Statistiken erstellt werden sollen, ist diese Einschränkung kein Hindernis und im Gegenteil eine Erleichterung durch die Vermeidung redundanter Datapoints. Die Methode, welche mit POST :/datapoint/generate aufgerufen wird, geht wie folgt vor: Nachdem der Zeitstempel erstellt wurde und bei Bedarf alte Datenpunkte gelöscht wurden, werden zunächst aus der EMU-Datenbank alle angelegten Projekte angefragt und ein neuer, leerer Datapoint angelegt. Anschließend werden die Variablen mit den passenden Werten gefüllt, welche für die projektunabhängigen Daten durch einfache Anfragen an die Datenbank erhalten werden können. Zum Füllen der Maps hingegen wird über die vorher bestimmten verfüg- baren Projekte iteriert, um für jedes die individuellen Werte aus der Datenbank abzufragen. Der vollständig befüllte Datapoint wird zuletzt in der MongoDB gespeichert. Bei meinem konkreten Testaufbau wird der Zeitstempel, welcher zur Identifikation der erzeugten Objekte notwendig ist, nicht aus dem aktuellen Datum gewonnen, sondern beim Methodenaufruf als Requestparameter übergeben. Diese Lösung ist dem Umstand geschuldet, dass meine Anwendung nicht auf die Live-Datenbank von EMU zugreift, sondern jeweils lokale, aus psql-Exporten geladene Kopien verwendet. Auf diese Weise können viele verschiedene Datenpunkte aus verschiedenen historischen Ständen der Datenbank einfach und schnell erzeugt werden. Der übergebene Zeitstempel ist dabei der Name der aktuell eingelesenen SQL-Datei im Format yyyy-MM-dd-HH.sql. Ein netter Nebeneffekt dieser Vorgehensweise ist es, dass Unterschiede im Parsen von Zeitstempeln zwischen Java und Go mitbetrachtet werden können. 3.1.2 Stats-Service Aufbauend auf dem Output des Datapoint-Services arbeiten die Funktionen des Stats-Services. Die Aufgabe dieses Dienstes ist es, aus den Datenpunkten, welche jeweils Informationen zu einem bestimmten Zeitpunkt speichern, Stat-Objekte zu erzeugen. In diesen Stats wird zu einem bestimmten Kennwert, wie zum Beispiel der Anzahl erfolgreich versandter E-Mails, dessen zeitlicher Verlauf in Form einer Map von Zeitpunkt der Messung zu dem Messergebnis abgelegt. Die Gruppierung ändert sich damit von einer zeitlichen zu einer Gruppierung nach Kennwert. Konkret passiert dies folgendermaßen: Nach dem Aufruf von POST :/stats/generate wird zuerst anhand des neuesten Datapoints bestimmt, welche Projektnamen verfügbar sind. Für jeden davon werden anschließend die projektspezifischen Kennzahlen verarbeitet. Dazu wird pro Projekt ein StatsForProjekt-Objekt angelegt, welches den Namen dieses Projekts sowie eine Liste von Stat-Objekten hält. Jedes Stat-Objekt besteht aus einer Bezeichnung, wie beispielsweise employeesPerProject, und einer Map, in der Zuordnungen der Form {Zeitstempel t } → {Wert von zum Beispiel employeesPerProject zu t } 9
3 Testaufbau gespeichert werden. Dieser Stats-Service iteriert pro Projekt über alle verfügbaren Datapoints, um in jedem Interationsschritt jeweils einen neuen Eintrag in die Maps der passenden Stat-Objekte zu schreiben. Nach vollständigem Durchlauf der Schleife wird der StatsForProject-Datensatz in der Datenbank gespeichert. Wurde dieser Vorgang für alle Projekte wiederholt, werden schließlich die projektunabhängigen Kennzahlen mit dem gleichen Vorgehen ermittelt und in einer GeneralStats- Collection gespeichert. Eine Übersicht, wie die verarbeiteten Daten ineinander umgewandelt und in welchem Format sie abgelegt werden, ist in Abbildung 3.2 zu sehen. Die dort dargestellten Datentypen sind die von Java; in Go würden die dazu analogen Bezeichnungen verwendet werden. Dieses Verfahren, immer wieder neu über alle Datapoints zu iterieren, ist so sicher nicht besonders effizient. Auch könnte man diskutieren, ob diese Umsortierung der Datenstrukturen nötig ist. Hohe Effizienz war jedoch nicht das Ziel beim Schreiben dieser als Beispiel dienenden Anwendungsteile; im Gegenteil wurde eher Wert darauf gelegt, mit den vorhandenen Daten und in einem überschaubaren Rahmen pro Microservice ein bestimmtes Muster möglichst intensiv anzuwenden. Während das Muster im ersten Service darin bestand, aus einer PostgreSQL-Datenbank viele verteilte Werte zusammenzufassen, ist es in diesem Fall, viele Dokumente aus einer Mongo-Datenbank anzufragen und zu verarbeiten. Sowohl Datenquelle als auch -senke sind für diesen Microservice MongoDB Collections. Da- durch, dass es sich bei MongoDB um ein dokumentenbasiertes Datenbanksystem handelt, ist die Speicherung von Listen und Maps problemlos darin möglich. Würde stattdessen eine relationale Datenbank verwendet werden, wären deutlich mehr Tabellen und komplexere Abhängigkeiten notwendig, was sowohl die Anwendung als auch die Datenhaltung aufblähen würde. Ein solcher Zuwachs an Komplexität wurde für diesen Anwendungsfall als nicht zielführend erachtet, weshalb die Entscheidung getroffen wurde, für die Speicherung von Datapoints und Stats MongoDB zu verwenden. 3.1.3 Json-Service Als drittes und letztes hier betrachtetes Beispiel dient der Json-Service. Wie der Name bereits vermuten lässt, spielt hierfür die JavaScript Object Notation (JSON) eine Rolle. Im Kern verläuft seine Datenverarbeitung so, wie beim Stats-Service auch, wodurch viele Funktionen wiederverwen- det werden können. Der entscheidende Unterschied zum vorher behandelten Dienst liegt darin, dass der Json-Service sowohl als Datenquelle als auch für den Output einfache Textdateien be- nutzt. Anstelle einer Datenbank liegt deren Inhalt als JSON-formatierte Strings vor. Konkret wird aus einer datapoints.json-Datei gelesen, welche zuvor durch Exportieren der datapoints- Collection mithilfe der IntelliJ IDEA IDE [33] erzeugt wurde. Geschrieben wird in die zwei Dateien generalStats.json und statsForProject.json. Die Idee für diese Implementierung ist aufgekommen, nachdem der Stats-Service bei ersten Testläufen unter gewissen Umständen eine sehr hohe Varianz in der Laufzeit zeigte (mehr dazu in Abschnitt 4.2). Dadurch, dass diese Variante nur mit Textdateien auskommt, sollte überprüft werden, ob diese Auffälligkeiten auf die Verwendung der Datenbank zurückzuführen sind. JSON als Dateiformat wurde hier wegen seiner weiten Verbreitung und seiner guten Lesbarkeit für Maschinen und Menschen verwendet. Das auf den Datentypen von JavaScript basierende Format wird häufig für den Austausch von Daten zwischen Maschinen eingesetzt, kann uns hier aber dank der Möglichkeit, Objekte, Listen, Maps, und vor allem Kombinationen dieser wiederzugeben, auch für die Speicherung der Datensätze behilflich sein. [49, S. 263][9, S. 123] 10
3.1 Auswahl der Services EMU Datenbank Datapoint-Service Datapoint -timestamp: Date -privilegesPerProject: Map -employeesPerProject: Map -visibleJSErrors: long -successfulMails: long -unsuccessfulMails: long ... Stats-Service Json-Service GeneralStats StatsForProject -projectName: String 0..1 0..1 stats stats 0..n 0..n Stat -name: String -values: Map Abbildung 3.2: Verlauf der verwendeten Datenstrukturen am Beispiel der Java-Implementierung 11
3 Testaufbau 3.1.4 Bewertung der Services Als Abschluss dieser Sektion werden die oben beschriebenen Dienste noch bezüglich ihrer Eignung als Beispiele für diese Arbeit bewertet. Zu diesem Zweck wurden sechs typische Eigenschaften eines Microservices aus Texten von Leitner, Richter, Indrasiri u.a. ermittelt [37, S. 166][54, S. 131 f.][29]. 1. Horizontale Skalierbarkeit Skalierung der Leistung bei Vervielfältigung des Dienstes Eine einfache Form der Parallelisierung mit dem Zweck, eine horizontale Skalierung zu erreichen, ist die Verwendung jeweils eigener Instanzen des Datapoint- und des Stat-Services pro Anwendung, über die Statistiken ermittelt werden sollen. Aufgrund der Spezialisierung der Dienste auf die Datenstruktur von EMU ist dies jedoch nicht ohne größere Anpassungen am Code möglich. Eine weitere Möglichkeit, eine Skalierung umzusetzen, ist es, die Teilaufgaben der einzelnen Dienste bei der Arbeit mit EMU auf mehrere Instanzen aufzuteilen. Für den Datapoint-Service auf der einen Seite ist dies gut möglich: Hier könnte die Ermittlung der einzelnen Werte für einen Datenpunkt – wie zum Beispiel die Anzahl der Mitarbeiter pro Projekt employeesPerProject oder die Anzahl der erfolgreich versendeten E-Mails successfulMails – auf verschiedene Knoten verteilt werden. Um hier einen merklichen Zeitgewinn beobachten zu können, könnte es dann noch notwendig sein, den zu bearbeitenden Zustand der EMU-Datenbank zu klonen, um bei den Datenbankzugriffen nicht in ein Bottleneck zu geraten. Für den Stat-Service auf der anderen Seite bietet sich dieses Verfahren weniger an beziehungsweise wäre aufwendiger. Durch die Umordnung der Daten wird für jede generierte Statistik der Input aus allen Datenpunkten benötigt. Dieser Input wird aktuell im Service aus der Datenbank abgefragt. Um redundante Abfragen zu vermeiden, müssten diese also aus dem Dienst ausgelagert und zum Beispiel bei Aufruf bereits mitgeliefert werden. Somit ergäbe sich dann eine Skalierung von nur einem Teil des Services. Ähnlich verhält es sich mit dem Json-Service: Die in einer Datei vorliegenden Daten könnten in mehrere Dateien – eine pro Statistik – aufgeteilt werden, um zu vermeiden, dass bei einer Parallelisierung jede Instanz alle Daten lesen muss. Ansonsten ergibt sich bei einer Teilung nach einzelnen Statistiken bei den beiden zuletzt genannten Diensten noch das Problem, dass die Datenstruktur des Outputs aktuell etwas verschachtelt ist und wahrscheinlich für eine effektive Skalierung umstrukturiert werden müsste. 2. Hardwareunabhängigkeit Unabhängigkeit von verwendeter Hardware Alle für diese Arbeit geschriebene Software wurde auf mehreren handelsüblichen Rechnern entwickelt und getestet, wodurch dieses Kriterium für alle drei Microservices als erfüllt betrachtet werden kann. 3. Technologieunabhängigkeit Unabhängigkeit von umgebender Software, wie Datenbanken Da sowohl der Database- wie auch der Stat-Service direkt aus Datenbanken lesen und in diese schreiben, müssen diese verfügbar und korrekt konfiguriert sein, um die Dienste erfolgreich ausführen zu können. Für den Stat-Service besteht dabei nur eine Abhängigkeit zu einer Mongo-Datenbank, bei dem Database-Service kommt noch eine Abhängigkeit zu einer PostgreSQL-Datenbank hinzu, welche unter Umständen auch auf einem anderen Server liegen könnte. Der Json-Service benötigt im Vergleich dazu keine Verbindung zu einer Datenbank, stattdessen ist jedoch das Vorhandensein einer korrekt formatierten Datei als Input notwendig. Das Kriterium der Technologieunabhängigkeit ist somit nicht erfüllt, wobei der Json-Service durch minimale Änderungen am einfachsten angepasst werden könnte, um dies zu beheben. 4. Austauschbarkeit Vorhandensein einer höchstens losen Kopplung zwischen Komponenten Um mit meiner Anwendung sinnvolle Statistiken generieren zu können, war es für den Datapoint- 12
3.1 Auswahl der Services Service notwendig, eine enge Bindung zu der Datenstruktur der EMU-Anwendung herzustellen. Im Codebeispiel 3.1 ist zu sehen, wie die Mitarbeiter pro Projekt bestimmt und gespeichert werden. Damit dieser Code funktionieren kann, müssen die Tabellen- und die wichtigsten Spaltennamen der EMU-Datenbank wie erwartet vorliegen. Dieses Problem setzt sich im Stat-Service leider fort: Hier werden die durch den Datapoint-Service gespeicherten Daten mit ihrem genauen Bezeichner abge- fragt. Das Codebeispiel 3.2 zeigt die Weiterverarbeitung des vorher bereits behandelten Datensatzes employeesPerProject. Wahrscheinlich existiert eine Lösung, die durch höhere Codekomplexität eine losere Kopplung erreicht, diese Überlegung wurde im Rahmen dieser Arbeit jedoch nicht näher verfolgt. Für den Json-Service ergibt sich, aufgrund der nahezu gleichen Datenverarbeitung, ein ähnliches Bild wie für den Stat-Service, wobei man durch das Lesen aus und Schreiben in Dateien zumindest von einer etwas loseren Bindung sprechen könnte. // Get all available projects. List availableProjects = projectRepo.findAll(); // Populate datapoint. Map employeesPerProject = availableProjects.stream() .collect(Collectors.toMap( Project::getName, employeeRepo::countEmployeesByProject )); datapoint.setEmployeesPerProject(employeesPerProject); Codebeispiel 3.1: Setzen einer Variable eines Datenpunkts im Datapoint-Service // Create the stats with hardcoded names. Stat employeesPerProject = new Stat("employeesPerProject"); // Fill the stats with data. datapoints.forEach(datapoint -> { if (datapoint.getEmployeesPerProject() == null) { return; } employeesPerProject.getValues().put( datapoint.getTimestamp(), datapoint.getEmployeesPerProject().get(projectName) ); // ... Verarbeitung weiterer Statistiken zur Übersichtlichkeit ausgelassen ... }); Codebeispiel 3.2: Weiterverarbeitung eines Datensatzes im Stat-Service 13
3 Testaufbau 5. Standardschnittstellen Verwendung standardisierter Kommunikation via HTTP und JSON Die von mir geschriebene Anwendung wurde so konzipiert, dass alle Methoden beziehungswei- se Funktionen, welche von außen ansprechbar sein sollen, über eine HTTP-REST-Schnittstelle zur Verfügung gestellt werden. Im Beispiel benutzen die meisten Aufrufe den HTTP POST- Befehl, welcher das Anlegen einer neuen Ressource signalisiert. Die POST-Methode wurde ge- wählt, da in der Regel durch jeden Befehl an die Anwendung eine bestimmte Form von Daten generiert und gespeichert wird: Durch POST :/stats/generate werden bei- spielsweise die Statistikdaten aus den vorliegenden Datenpunkten ermittelt und mithilfe von POST :/benchmark/datapoint wird die Methode zum Erzeugen eines Daten- punkts mehrmals ausgeführt und dabei die benötigte Laufzeit gesichert. Gegebenenfalls erforderliche oder optionale Parameter, wie die Anzahl durchzuführender Durchläufe bei einem Benchmark, können über den HTTP-Body gesetzt werden und sind im JSON-Format codiert. 6. Klar definierter Anwendungsbereich Beschränkung auf genau eine konkrete Aufgabe Zwar erfüllt die Anwendung als Ganzes – durch die Bauweise als lose gekoppelter Monolith – mehrere Aufgaben, nämlich die der einzelnen Dienste, letztere sind jedoch für genau eine Aufgabe innerhalb des Verbundes zuständig: Der Datapoint-Service erstellt einen Datenpunkt aus den Daten der EMU-Datenbank, der Stat-Service generiert Statistiken aus den Informationen mehrerer Datenpunkte in der Datenbank und der Json-Service speichert Statistiken zu JSON-codierten Datenpunkten aus einer Datei. 3.2 Auswahl der Frameworks Bei der Entwicklung von Anwendungen, gerade bei Webanwendungen, werden bestimmte Pro- grammbestandteile häufig wiederverwendet. Auch bei Microservices werden Bausteine wie ein Webserver zum Empfangen von Aufrufen, ein Routing-System zur Zuordnung von Call zu be- arbeitender Funktion, ein Security-System zur Berechtigungsverwaltung und häufig auch eine Datenbankanbindung benötigt. Um zu vermeiden, diesen Rahmen einer Anwendung jedes Mal manuell neu anlegen zu müssen, werden hierfür häufig Frameworks benutzt. Dadurch kann ein schlankerer und übersichtlicherer Code erreicht werden, welcher in der Regel weniger Fehler aufweist und optimierter ist, womit man auch dem „Micro“ in Microservice besser gerecht wird. Doch nicht alle Frameworks sind gleichzustellen, stattdessen lassen sie sich gut in verschiedene Gruppen, passend für den jeweiligen Anwendungsfall, untergliedern: Wenn bereits ein Backend existiert und darauf aufbauend lediglich eine REST-Schnittstelle über ein Application Programming Interface (API) benötigt wird, bietet es sich an, ein leichtgewichtiges Microframework wie Flask [19] zu benutzen. Ist auf der anderen Seite ein Client erforderlich, welcher selbst API-Requests absetzt, kann ein Blick auf Client-Libraries wie Requests [52] geworfen werden. Für die Entwicklung einer vollen Webanwendung von Grund auf empfiehlt sich schließlich die Verwendung eines sogenannten Full-Stack-Frameworks (wie zum Beispiel Django [41]), welches die volle Bandbreite an Schichten und Funktionen anbietet. Um bei meiner Gegenüberstellung Unterschiede zwischen den verwendeten Frameworks sinnvoll vergleichen zu können, habe ich mich bei der Auswahl ebenjener auf einen Typ beschränkt: Da Spring Boot für mich als erstes Vergleichsobjekt schon zu Beginn schnell feststand, sollte das Vergleichs-Framework ebenso ein Full-Stack-Framework sein. Wie bereits in Punkt 3.1 zu sehen, decken meine Beispielanwendungen nicht den vollen Um- fang eines Full-Stack-Frameworks ab. Speziell wurde kein Frontend entwickelt; die Funktionen werden mit HTTP-Calls – angelehnt an das REST-Muster – angesprochen und liefern lediglich kurze Statusmeldungen zurück, nicht jedoch ganze Webseiten. Der Frontendbereich wurde somit 14
3.2 Auswahl der Frameworks ausgeklammert, da dort andere Vergleichsparameter in Frage kämen als bei der Backendentwick- lung und im Microservicebereich. Aufgrund der Spezialisierung einzelner Services auf bestimmte Aufgaben, werden Front- und Backend auch generell häufig getrennt behandelt. Da das Backend einer Anwendung ein essentieller Teil ist, sollte meine Fokussierung darauf bei der Evaluation eines Full-Stack-Frameworks die Relevanz der Ergebnisse wenig einschränken; trotzdem ist diese Entscheidung im Hinterkopf zu behalten. 3.2.1 Spring Boot Spring Boot ist ein Projekt des Unternehmens VMware, Inc., welches die Erstellung und vor allem die Konfiguration von Anwendungen in dem quelloffenen Spring-Framework vereinfacht. Laut offizieller Website können damit Standalone-Anwendungen erstellt werden, die „einfach laufen“ [65]. Während bei einer klassischen Spring Anwendung noch viel Konfiguration in der Extensible Markup Language (XML) notwendig ist, übernimmt Spring Boot diese Arbeit und bringt auch direkt einen Webserver (Tomcat, Jetty oder Undertow) sowie Dependency-Pakete für verschiedene Anwendungsfälle wie „Spring Web“, „Spring Boot Dev Tools“ oder „Spring Data JPA“ mit [66]. Durch diese Vereinfachungen bei der Initialisierung einer neuer Applikation wird Spring Boot gerne für die Entwicklung von Microservices eingesetzt. Spring-Anwendungen, und damit auch solche mit Spring Boot, werden in der Programmiersprache Java geschrieben, welche sich seit über zehn Jahren einen Platz als eine der wichtigsten und meistgenutzten Programmiersprachen sichern kann [8, 16, 46]. Mit ein Grund für diesen Erfolg wird sicher die Plattformunabhängigkeit der Sprache sein, welche durch die Verwendung der Java Virtual Machine (JVM) erzielt wird [27]. Auch EMU, die Datenbasis für diese Arbeit, wurde in Java und mit Spring Boot geschrieben, weshalb es sich anbot, hier als erstes Framework zur Gegenüberstellung ebenso Spring beziehungsweise das darauf aufbauende Spring Boot-Projekt, zu verwenden. 3.2.2 Buffalo There’s a joke that Go was conceived while waiting for a C++ program to compile, which is kind of half true [42]. Andrew Gerrand Software Ingenieur bei Google Die Programmiersprache Go, ursprünglich ein Nebenprojekt dreier Google-Entwickler, wurde im November 2009 mit Spezifikation und Open-Source-Referenzimplementierung veröffentlicht. Auch heute noch von Google unterstützt, hat sich seitdem die Zahl der Mitwirkenden auf das mehr als hundertfache erhöht. Auch außerhalb des Unternehmens hat sich eine aktive Community gebildet. Anstoß für die Entwicklung von Go war eine steigende Unzufriedenheit einiger Google-Mitarbeiter mit den Tools und Sprachen, mit denen sie arbeiten mussten, insbesondere, da diese für den dort erforderlichen Maßstab nie designt wurden. Namentlich werden Java und C++ genannt, beide über ein Jahrzehnt älter als Go, beide weit verbreitete Sprachen, aber beide für die Go Gründer unzufriedenstellend. Im Gegensatz zu Java ist Go keine reine objektorientierte Sprache, erlaubt jedoch objektorientierte Programmierung. Go legt besonderen Wert auf einfache Skalierbarkeit und einfache (auch kooperative) Entwicklung. Ein Beispiel hierfür ist Gofmt (kurz für Go Format), ein Werkzeug, welches Go Quellcode in kanonischer Formatierung drucken kann, wodurch dieser nahtlos in anderen, bestehenden Code eingefügt werden kann. Auf diese Weise sollen Differenzen zwischen Programmierern bezüglich unterschiedlicher Präferenzen im Formatierungsstil umgangen 15
3 Testaufbau werden [42, S. 101]. Für diese Arbeit habe ich mir ein Framework in Go zum Vergleich mit einem Java Framework ausgesucht, um zu überprüfen, inwieweit die Bemühungen gelungen sind, eine für die Entwicklung von Webanwendungen bessere Alternative zu Java zu schaffen. Auch wenn ich in diesem Rahmen die Skalierbarkeit meiner Software nicht vollumfänglich testen kann, so bieten die Microservices zumindest einen Einblick für dieses Qualitätsmerkmal. Auch die Erstellung von Webanwendungen wird bei Go out of the box unterstützt und erleichtert. Sowohl Anbindungen an verschiedene Datenbanken als auch ein performanter Webserver werden in der Standardbibliothek bereitgestellt. Es wäre somit auch gut möglich, Microservices ohne das Hinzuziehen eines Frameworks in Go zu schreiben, was nicht heißt, dass eine Unterstützung nicht trotzdem sinnvoll sein kann. [23] Dieser Unterschied zu anderen Sprachen, sprich, dass bereits viele der benötigten Tools direkt in der Standardbibliothek mitgebracht werden, verändert aber, welche Aufgaben auf das Framework zurückfallen. Bei der Evaluation in Sektion 4.3 soll näher auf diese Beobachtung eingegangen werden. Für diese Arbeit habe ich mich dazu entschieden, das Go-Framework Buffalo einzusetzen. Buffalo ist von Full-Stack-Web-Developer Mark Bates [5] speziell für Webentwicklung mit Priorisierung der Entwicklungsgeschwindigkeit und -produktivität entworfen worden. Gerade der Einstieg für neue Go-Entwickler soll durch umfangreiche Codegenerierung und der Bereitstellung von Tools wie automatischem Deployment von Docker-Containern oder Hot Code Reload (automatischem Rebuilden der Anwendung bei Änderungen) erleichtert werden. Es können mit Buffalo einfach volle Webanwendungen erstellt werden, bei der Initialisierung einer Anwendung kann die Generierung des Frontend-Codes mithilfe des --api-Flags jedoch auch übersprungen werden, wodurch es auch für die hier gewünschte Entwicklung von Microservices, welche lediglich aus Backend bestehen, gut geeignet ist. 3.3 Schnittstellen Datenbankverbindungen sind häufig ein wichtiger Bestandteil einer Anwendung, damit ein organi- siertes Ablegen und Abrufen von Daten sichergestellt werden kann. Aufgrund der Vielfältigkeit von Microservices ist es durchaus realistisch, dass für eine Anwendung, bestehend aus vielfältigen Microservices, auch Verbindungen zu mehreren, unterschiedlichen Datenbanken notwendig sind. Auch im Laufe der Entwicklung von EMU kam diese Anforderung auf. Dort existiert zum Beispiel eine Verbindung zur projekteigenen Datenbank – derjenigen, welche auch die Daten für die in dieser Arbeit besprochenen Microservices liefert – und eine zur firmenübergreifenden LDAP-Datenbank. War die Entwicklung mit nur einer Datenbank zu Beginn kein Problem in Spring Boot, stellte sich das Hinzufügen neuer Datenquellen im weiteren Verlauf als nicht trivial heraus. Aufgrund dieser Erfahrung beschloss ich, meine Testkandidaten auch auf die Schwierigkeit der Verwaltung von multiplen Datenbanken in einer Anwendung zu testen. Die erste sollte dabei PostgreSQL sein, da dies das von EMU verwendete Datenbanksystem ist. Für EMU wurde PostgreSQL damals aus dem Grund ausgewählt, dass es sich dabei um ein weit verbreitetes relationales System handelt, welches zudem quelloffen ist. Für die zweite Datenbank wollte ich einen Kontrast zu der bereits vorhandenen, relationalen, herstellen. Dokumentenbasierte Datenbanken werden gerade im Microservice- und Cloud-Bereich immer wichtiger. In einer cloudbasierten Webanwendung, welche das Event-Sourcing-Pattern einsetzt, ist es beispielsweise vorstellbar, eine relationale Datenbank zum Event-Sourcing zu ver- wenden, während zum Verwalten des View-Models eine dokumentenbasierte NoSQL-Datenhaltung eingesetzt wird [55][15]. Nachdem im Laufe des Benchmarkings meiner Anwendung Phänomene auftraten, welche ein 16
3.3 Schnittstellen mögliches Bottleneck bei der Kommunikation mit den Datenbanken vermuten ließen, wurde der Anwendung eine dritte Schnittstelle zur Datenhaltung hinzugefügt: dem Schreiben und Lesen von Werten aus und in Textdateien, die mit JSON formatiert sind. Zur einfachen Erzeugung geeigneter Inputdateien wurden Teile der bereits bestehenden Datenbanken mithilfe der IntelliJ IDEA IDE [33] exportiert. 17
3 Testaufbau 18
4 Evaluation Die Evaluation der behandelten Frameworks auf Basis des von mir geschriebenen Codes soll mithilfe des Analytischen Hierarchieprozesses von Thomas L. Saaty [57] durchgeführt werden. Hierfür werden zuerst Evaluationskriterien in harte und weiche Kriterien unterteilt. Harte Kriterien (KHx, mit x einer fortlaufenden Nummer) werden bei der abschließenden Bewertung berücksichtigt, während weiche Kriterien (KWy, mit y einer fortlaufenden Nummer) zwar bei der Gegenüber- stellung betrachtet werden, jedoch in die Bewertung nicht mit einfließen. Nach der Aufstellung und Gliederung der Kriterien werden die Frameworks im darauffolgenden Teil anhand von ihnen miteinander verglichen. Im letzten Unterkapitel werden drei verschiedene Anwendungsfälle einge- führt, welche Grundlage für die Gewichtung der Kriterien und Bewertung der Frameworks sind. Die angesprochene Gewichtung und die abschließende Evaluation mit einer Auswertung der zuvor gesammelten Daten, aufgeteilt auf die Anwendungsfälle, erfolgt ebenso in Punkt 4.4. 4.1 Analytischer Hierarchieprozess nach Saaty Der Analytische Hierarchieprozess (AHP) ist ein Instrument zum Vergleich verschiedener Optionen in einem Sachverhalt, um die Entscheidungsfindung zwischen ihnen zu vereinfachen. Der von Thomas L. Saaty 1971 bis 1975 entwickelte und in seinem Buch The Analytic Hierarchy Process [57] beschriebene Vorgang stützt sich dabei auf paarweise Gegenüberstellungen, bei denen jeweils ein Gewinner und eine Gewichtung des Vergleichs bestimmt werden muss. Letztere erfolgt dabei durch das Vergeben einer Punktzahl von eins bis neun – eine eins steht für gleiche Wichtigkeit der verglichenen Partner, fünf etwa für eine deutlich höhere Bedeutung des Siegers, und neun für eine extrem größere Wichtigkeit des Gewinners in dem behandelten Vergleich; die dazwischenliegenden Bewertungen ergeben sich entsprechend. Eine weitere Eigenschaft des AHP ist, dass Inkonsistenzen im Bewertungsprozess rechnerisch aufgedeckt und somit korrigiert werden können. [56, 61] Verglichen werden zum einen die Kriterien bezüglich ihrer Relevanz in der gegebenen Situation. Um ein möglichst aussagekräftiges Ergebnis liefern zu können, werden in Abschnitt 4.4 drei verschie- dene Anwendungsfälle getrennt voneinander behandelt. Die Gewichtung der Evaluationskriterien der Frameworks nach dem AHP erfolgt deshalb in diesem Teil für jeden Fall individuell. Das Ergebnis ist eine Zerlegung der Eins durch die Gewichtungen der Kriterien, wobei eine höhere Prozentzahl für eine höhere Priorität für den entsprechenden Fall steht. Es gilt also X gk = 1 und ∀k ∈ K : 0 < gk < 1 k∈K mit K der Menge der gewichteten Evaluationskriterien, gk der Gewichtung von Kriterium k. Zum anderen werden Spring Boot und Buffalo auf Basis der Kriterien gegenübergestellt. Hier spielt nicht mehr die Priorität der Kriterien, sondern lediglich die Performance der Frameworks eine Rolle. Der Vergleich erfolgt wie oben mit einer Skala von eins bis neun. Für KH3 und KH4 kann dieser Vergleich fallübergreifend vorgenommen werden, für KH1 und KH2 muss – aufgrund der unterschiedlichen Messergebnisse bei Verwendung unterschiedlicher Betriebssysteme – pro Anwendungsfall eine separate Beurteilung abgegeben werden. Am Ende können pro Anwendungsfall 19
4 Evaluation die Prozentwerte der Prioritäten der Kriterien und die der Gewichtungen der Alternativen mitein- ander verrechnet werden, wodurch sich ein Gesamtwert in Prozent jeweils für Spring Boot und Buffalo für diesen Anwendungsfall ergibt. Für diese Arbeit wurden die für den Prozess notwendigen Berechnungen mithilfe eines online verfügbaren Tools der Business Performance Management Singapore (BPMSG) [24] angefertigt. 4.2 Definition der Evaluationskriterien Wie oben bereits beschrieben, werden hier die harten und weichen Kriterien definiert, anhand derer später die Evaluation erfolgt. Der Vollständigkeit halber soll an dieser Stelle zusätzlich noch das Kriterium der Skalierbarkeit betrachtet werden, welches im Weiteren nicht mehr behandelt wird. Gerade im Bereich des Cloudcomputing ist Skalierbarkeit von Mikroservices eine wichtige Voraussetzung für den Erfolg einer Anwendung, da sowohl ein Service häufig redundant auf verschiedenen Servern läuft, auf einem Server aber auch viele verschiedene Services betrieben werden können. Nachdem die Programmiersprache Go auch mit besonderem Augenmerk auf Webanwendungen geschrieben wurde, ist damit zu rechnen, dass, speziell aufgrund des Threading- Ansatzes, Go-Anwendungen in diesem Bereich gut abschneiden. Java wurde ursprünglich nicht mit Fokus auf Cloud-Deployment entwickelt, Spring Boot bringt jedoch einige passende Dienste zur automatischen Skalierung mit. Beispiele hierfür sind ein Endpoint, welcher Metriken über die Anwendung zur Verfügung stellt, ein Endpoint, über den ein geordnetes Herunterfahren möglich ist, dynamische Portzuweisung, sowie eine einfache Anbindung an Netflix Eureka (einem REST-AWS- Service für Lastverteilung und Ausfallsicherung [17]) [43]. Konkrete Tests in diese Richtung, oder auch Untersuchungen speziell zum Einfluss von Buffalo auf diesem Gebiet, konnten im Rahmen dieser Arbeit jedoch nicht untersucht werden, weshalb dieses Kriterium nicht in die Evaluation einfließt. 4.2.1 Harte Kriterien KH1 & KH2: Durchschnittliche Laufzeit und Standardabweichung der Laufzeit Als erste harte Kriterien, soll die Laufzeit der erstellten Anwendungen untersucht werden. Unter- schieden wird dabei zwischen der durchschnittlichen Laufzeit in KH1 und der Standardabweichung davon in KH2. Auf diese Weise kann sowohl reine Geschwindigkeit als auch Konsistenz betrachtet werden. Ein Vorteil dieser Benchmarks ist auf der einen Seite, dass sie objektive Ergebnisse in Form von eindeutigen, statistisch auswertbaren Zahlwerten liefern. Die konkrete Bewertung dieser Werte mag zwar einen gewissen subjektiven Charakter haben, das generelle Resultat der Messung (beispielsweise „Service A ist in Framework X durchschnittlich um t Millisekunden schneller als in Framework Y“) bleibt jedoch bestehen. Auf der anderen Seite ist ein Nachteil dieses Verfahrens, dass es von der vorliegenden Implementierung abhängt. So können verschiedene Funktionalitäten in einer bestimmten Sprache unterschiedlich performant sein. Zudem kann die gleiche Funktionalität durch Verwendung verschiedener Entwicklungsansätze unterschiedlich effizient gestaltet werden. Um durch diesen Faktor möglichst wenig Ungenauigkeiten in meinen Messwerten zu erzeugen, habe ich zwei Maßnahmen ergriffen: Erstens habe ich mich, wie bereits in Kapitel 3 erläutert, bei der Entwicklung pro Service auf jeweils unterschiedliche Funktionen und Schnittstellen fokussiert, damit sich mögliche Bottlenecks in einem Bereich der Sprache oder des Frameworks nicht auf die gesamte Anwendung niederschlagen. Zweitens wurde darauf geachtet, die jeweils zusammen- gehörigen Implementierungen in den beiden Frameworks möglichst ähnlich zu halten. Aufgrund der unterschiedlichen Programmiersprachen kann der Code natürlich nicht gleich sein, auch die 20
Sie können auch lesen