GraalVM – Ein Blick auf die Performance

Die GraalVM ist nun seit reichlich zwei Jahren am Markt. Sie verspricht im Wesentlichen zwei Dinge: Bessere Laufzeiteigenschaften und die Integration mehrerer Programmiersprachen.

Dieser Blogbeitrag konzentriert sich auf die Performance. Dabei soll es aber nicht primär darum gehen, ob und wieviel ein spezielles Programm auf dem Graal-JDK schneller ausgeführt wird als auf einem herkömmlichen JDK. Die konkreten Messwerte und die relativen Vergleiche sind ohnehin nicht nur vom untersuchten Programm abhängig und wenig verallgemeinerbar, sondern auch nur Momentaufnahmen: Sowohl die GraalVM als auch beispielsweise das OpenJDK werden beständig weiterentwickelt, so dass sich auch die Messwerte stets ändern werden. Stattdessen beschäftigt sich der Blogbeitrag vor allem mit den Fragen: Warum sollte die GraalVM wesentlich performanter sein? Was macht sie anders als die herkömmlichen JDKs? Damit wird eine Abschätzung möglich, ob alle Programme performanter ausgeführt werden oder keine nennenswerte Steigerung zu erwarten ist oder ob die Performance-Steigerung nur in bestimmten Anwendungsfällen zu erwarten ist. Und letztendlich, ob das „herkömmliche“ Java demzufolge zu langsam ist…

Laptop verbunden mit LED Display, auf dem "Hello World" abgebildet wird.

Entwicklung der Compiler

Da die Performance eines Java-Programms wesentlich durch den Compiler bestimmt wird und auch hier die Kernfrage ist, was die GraalVM anders macht verschaffen wir uns zunächst einmal einen Überblick über Compiler.

In den Anfängen der Programmierung existierte zunächst noch gar kein Compiler – es wurde direkt der Maschinencode programmiert. Da dies unübersichtlich und wenig verständlich war, hat sich zeitnah der Assembler-Code entwickelt. Dabei handelt es sich jedoch im Wesentlichen um eine direkte Abbildung des Maschinencodes – nur dass statt Binär- oder Hexadezimal-Opcodes nun Buchstabenkürzel verwendet werden. Auch hier sprechen wir (zumindest im Scope dieses Blogbeitrags) noch nicht von einer Programmiersprache und einem Compiler.

Mit der Zeit wurde die Entwicklung immer komplizierterer Programme notwendig. Damit wurde der Assembler-Code zunehmend unpraktikabel. Daher wurden in den 1950er Jahren die ersten höheren Programmiersprachen entwickelt. Diese benötigten einen Compiler, der den Quelltext in Maschinencode übersetzt.

Dies war zunächst der klassische AOT-Compiler (AOT: Ahead Of Time). Der Quelltext wird analysiert (Syntaxanalyse), in eine interne Baumstruktur überführt (Syntaxbaum) und aus diesem wird Maschinencode generiert (Codegenerierung). Die entstehende Binärdatei kann nun direkt ausgeführt werden.

Alternativ zur AOT-Kompilierung kann ein Programm auch durch einen Interpreter ausgeführt werden. Hierbei wird der Quelltext eingelesen und zeilenweise durch den Interpreter umgesetzt. Die eigentlichen Operationen (z. B. Addition, Vergleich, Programmausgabe) führt dann der Interpreter aus.

Der AOT-Compiler hat gegenüber dem Interpreter den Vorteil, dass die Programme wesentlich schneller ausgeführt werden. Allerdings sind die erzeugten Binärdateien maschinenabhängig. Darüber hinaus hat der Interpreter bessere Fehleranalysemöglichkeiten, da er beispielsweise Zugriff auf Laufzeitinformationen hat.

Java, Interpreter und JIT-Compiler

Beim Entwurf der Programmiersprache Java war ein Ziel, dass sie architekturneutral und portabel ist. Aus diesem Grund wurde der Java-Quellcode von Anfang an in maschinenunabhängigen Bytecode übersetzt. Dieser konnte dann von einer Laufzeitumgebung, der JRE (Java Runtime Environment), interpretiert werden. Damit war der übersetzte Bytecode maschinenunabhängig. So konnten beispielsweise Applets ohne Anpassungen auf einem Windows-PC, einem Mac oder einer Unix-Workstation ausgeführt werden. Die JRE muss dazu – unabhängig vom Applet – vorab auf den Workstations installiert sein.

Diese Mischform – AOT bis zum Bytecode, dann Interpretation zur Laufzeit – ist übrigens keine Idee aus der Java-Welt: Bereits in den 1970er Jahren hat beispielsweise Pascal den p-Code genutzt. [1]

Als die Java-Technologie im Jahre 1995 veröffentlicht wurde [2], war diese Maschinenunabhängigkeit zunächst mit großen Performance-Einbußen verbunden. Viele der damals relevanten Programmiersprachen wie bspw. „C“ kompilieren ihren Quellcode direkt in Maschinencode (AOT). Dieser kann auf dem entsprechenden System nativ ausgeführt werden und ist somit wesentlich performanter als die Interpretation des Bytecodes. Zu jener Zeit hat sich in den Köpfen vieler IT-Fachkräfte die Grundeinstellung „Java ist langsam“ festgesetzt – damals zu Recht.

Nun ist aber ein weiteres Entwurfsziel der Programmiersprache Java die hohe Leistungsfähigkeit. Aus diesem Grund wurde im Jahr 1998 der JIT-Compiler (JIT: Just In Time) eingeführt [2]. Damit wurden die Performance-Einbußen durch die reine Interpretation stark reduziert.

Bei der JIT-Kompilierung wird der Bytecode beim Programmstart zunächst ebenfalls interpretiert. Allerdings wird dabei genau verfolgt, welche Programmteile wie oft ausgeführt werden. Die häufig ausgeführten Programmteile werden nun – zur Laufzeit – in Maschinencode übersetzt. Zukünftig werden diese Programmteile nicht mehr interpretiert, sondern es wird der native Maschinencode ausgeführt. Hier wird somit zunächst Ausführungszeit für die Kompilierung „investiert“, um bei jedem zukünftigen Aufruf dann Ausführungszeit einsparen zu können.

Die JIT-Kompilierung ist daher ein Mittelweg zwischen AOT-Kompilierung und Interpretation. Die Plattformunabhängigkeit bleibt erhalten, da der Maschinencode erst zur Laufzeit erzeugt wird. Und da die häufig genutzten Programmteile nach einer gewissen Warmlauf-Zeit als nativer Maschinencode ausgeführt werden, ist auch die Performance (dann) annähernd so gut wie bei der AOT-Kompilierung. Ganz allgemein gilt dabei: Je häufiger einzelne Programmteile ausgeführt werden, desto mehr können die anderen, interpretierten Programmteile bei der Performance-Betrachtung vernachlässigt werden. Und dies gilt vor allem für oft durchlaufene Schleifen oder langlaufende Server-Anwendungen, deren Methoden ständig aufgerufen werden.

Laufzeitoptimierungen

Mit den bisher betrachteten Mechanismen ist der JIT-Compiler zwar plattformunabhängig – kann sich aber an die Ausführungszeit des AOT-Compilers nur herantasten, sie jedoch nicht erreichen oder gar übertreffen. Aus diesem Grund war zu dem Zeitpunkt, als der JIT-Compiler in das JDK integriert wurde, noch keineswegs sicher, dass es für einen Siegeszug ausreichen wird.

Allerdings hat der JIT-Compiler einen großen Vorteil gegenüber dem AOT-Compiler: Er ist nicht nur auf die statische Quellcode-Analyse angewiesen, sondern er kann das Programm direkt zur Laufzeit beobachten. Da sich die allermeisten Programme in Abhängigkeit von Eingaben und/oder Umgebungszuständen unterschiedlich verhalten, kann der JIT-Compiler zur Laufzeit wesentlich zielgenauer optimieren.

Ein großer Pluspunkt sind dabei die spekulativen Optimierungen. Dabei werden Annahmen getroffen, welche in den meisten Fällen zutreffen. Damit das Programm trotzdem korrekt funktioniert, wird die Annahme mit einem sogenannten „Guard“ abgesichert. Beispielsweise geht die JVM davon aus, dass in einem produktiv ausgeführten Programm die Polymorphie so gut wie nicht genutzt wird. Natürlich ist die Polymorphie grundsätzlich sinnvoll, aber die praktischen Einsatzszenarien beschränken sich meist auf den Testbereich oder auf die Entkopplung einer Codebasis – üblicherweise zur Nutzung durch verschiedene Programme oder für zukünftige Erweiterbarkeit. Während der Laufzeit eines konkreten produktiven Programmes – und dies ist der Scope der JVM – wird die Polymorphie jedoch selten genutzt. Das Problem ist dabei, dass es beim Aufruf einer Interface-Methode relativ zeitaufwendig ist, für das vorhandene Objekt die passende Methoden-Implementierung herauszusuchen. Aus diesem Grund werden die Methodenaufrufe getrackt. Wird beispielsweise mehrmals die Methode „java.util.List.add(…)“ auf einem Objekt vom Typ „java.util.ArrayList“ aufgerufen, merkt sich die JVM dies. Bei den folgenden Methodenaufrufen „List::add“ wird darauf spekuliert, dass es sich wieder um eine ArrayList handelt. Zunächst wird mit einem Guard die Annahme abgesichert: Es wird geprüft, dass das Objekt tatsächlich vom Typ ArrayList ist. Üblicherweise trifft dies zu und die bereits mehrfach ermittelte Methode wird mittels der „gemerkten” Referenz einfach direkt aufgerufen.

Seit der Integration des JIT-Compilers in das JDK sind nun mittlerweile mehr als zwei Jahrzehnte vergangen. In dieser Zeit wurden sehr viele Laufzeitoptimierungen integriert. Die vorgestellte Polymorphie-Spekulation ist nur ein kleines Beispiel, welches verdeutlichen soll: Neben der Kompilierung von Maschinencode wurden sehr viele Optimierungen erdacht, welche in einer komplexen Sprache wie Java nur zur Laufzeit funktionieren. Wenn eine Instanz beispielsweise mittels Reflection erzeugt wird, ist es für einen AOT-Compiler sehr schwer bis unmöglich, den konkreten Typ zu bestimmen und die spekulative Optimierung umzusetzen. Der Geschwindigkeitsvorteil der aktuellen JIT-Compiler beruht demnach im Wesentlichen darauf, dass sie dem Programm bei der Ausführung zuschauen, Gewohnheiten erkennen und schließlich Abkürzungen einbauen können.

GraalVM

Die GraalVM ist ein JDK von Oracle, welches auf dem OpenJDK basiert. Sie bringt eine virtuelle Maschine sowie viele Entwickler-Tools mit – was soweit auch für die anderen JDKs gilt. Warum also erregt die GraalVM wesentlich mehr Aufmerksamkeit als die anderen JDKs?

Zunächst bringt die GraalVM einen GraalVM-Compiler mit, welcher in Java entwickelt wurde. Darüber hinaus soll auch die gesamte JVM in Java umgeschrieben werden. Im letzten Kapitel wurde dargelegt, dass die aktuellen JVMs vor allem deswegen sehr performant sind, weil jahrzehntelang verschiedenste Optimierungen ergänzt wurden, welche sich nun summieren. Diese Optimierungen sind vor allem Java-spezifisch. Sie werden zumeist von Leuten entwickelt, welche einen Java-Background besitzen. Wenn nun die Ausführungsumgebung nicht in C++ sondern in Java umgesetzt ist, sind folglich keine C++-Kenntnisse mehr notwendig, um Optimierungen beizusteuern. Die Entwickler-Community wird so mittel- bis langfristig auf eine breitere Basis gestellt.

Ein weiterer spannender Aspekt der GraalVM ist, dass sie nicht nur Java-basierte Sprachen unterstützt. Das “Truffle Language Implementation Framework” ist ein Ansatzpunkt für die Entwicklung eigener Sprachen (DSL). Der GraalVM-Compiler unterstützt mit dem Truffle-Framework entwickelte Sprachen, so dass diese auch in der GraalVM ausgeführt werden können und von allen Vorteilen entsprechend profitieren. Einige Sprachen wie JavaScript, Python oder Ruby werden dabei bereits von Haus aus seitens der GraalVM unterstützt. Da alle Truffle-Sprachen gleichzeitig und gemeinsam in der GraalVM ausgeführt werden können, wird hier auch von einer polyglotten VM gesprochen.

Darüber hinaus werden auch LLVM-basierte Sprachen unterstützt. Bei LLVM handelt es sich um ein Rahmenprojekt für optimierende Compiler [4][5]. Dabei werden nicht nur Compiler-Bestandteile und -Technologien für externe Compiler-Entwicklungen bereitgestellt, sondern es werden auch im LLVM-Projekt bereits Compiler für viele Programmiersprachen wie bspw. C/C++ oder Fortran angeboten. Die LLVM Runtime ist ein weiterer Bestandteil der GraalVM, mit welchem LLVM-basierte Sprachen aufbauend auf dem Truffle-Framework in der GraalVM ausgeführt werden können. Auf den polyglotten Aspekt soll hier aber nicht weiter eingegangen werden, denn er hätte seinen eigenen Blogbeitrag verdient.

GraalVM Native Image

Die für diesen Beitrag relevanteste Neuerung ist die sogenannte „Native-Image-Technologie“. Das native-image ist ein Entwickler-Tool der GraalVM. Es erzeugt aus Bytecode eine ausführbare Datei. Die Ziele sind eine bessere Performance und weniger Hauptspeichernutzung zur Laufzeit. Nun wurde aber bisher beschrieben, dass Java immer schneller geworden ist: Der JIT-Compiler übersetzt alle häufig ausgeführten (d. h. relevanten) Programmteile in nativen Maschinencode. Das Programm wird während der Ausführung beobachtet und es werden fortlaufend Laufzeitoptimierungen vorgenommen. Damit stellt sich also die Frage: Was kann denn hier noch um Größenordnungen verbessert werden?

Die Antwort ist verblüffend einfach: Die Startzeit. Auch mit JIT-Compilern wird der Bytecode zunächst interpretiert. Erstens ist der Programmstart meistens kein häufig ausgeführter Programmteil. Zweitens müssen diese Programmteile üblicherweise erst ein paarmal durchlaufen werden, damit der JIT-Compiler diese als lohnendes Übersetzungsziel erkennt. Mit den Laufzeitoptimierungen verhält es sich analog: Das Programm muss erst einmal zur Laufzeit beobachtet werden, damit die passenden Optimierungen erkannt und eingebaut werden können. An dieser Stelle kommt verschärfend hinzu, dass beim Start alle benötigten Objekte sowie deren Klassen inklusive der kompletten Vererbungshierarchie initialisiert werden müssen.

Da wir nun eine Vorstellung vom „Was“ haben, interessiert uns das „Wie“: Wie kann unser Programm dazu bewegt werden, schneller zu starten?

Bei der Erstellung des Native Image wird der Bytecode zunächst sehr umfangreich statisch analysiert. Unter anderem wird dabei geprüft, welche Code-Teile zur Laufzeit überhaupt ausgeführt werden können. Dies bezieht sich nicht nur auf die vom Nutzer bereitgestellten Klassen, sondern auf den gesamten Klassenpfad – also inklusive der von der JVM bereitgestellten Java-Klassenbibliotheken. Nur die ermittelten Quellcode-Fragmente werden in das Native Image aufgenommen. Somit wird an dieser Stelle zwar der Umfang stark reduziert – aber es wird auch eine „Closed world assumption“ aufgestellt: Sobald irgendetwas dynamisch zur Laufzeit geladen wird, steht das Native Image Tool vor einem Problem. Es erkennt nicht, dass auch diese Quellcode-Teile ausgeführt werden können und damit benötigt werden. Aus diesem Grund wird auf diese Weise nicht viel mehr als ein einfaches HelloWorld-Programm funktionieren. Deshalb kann und muss man bei der Erstellung des Native Image dem Tool noch Informationen mitgeben, was alles dynamisch aufgerufen werden kann.

Nach der statischen Analyse wird nun der erste Punkt umgesetzt, welcher die Startgeschwindigkeit erhöht: Da der JIT-Compiler mit der Interpretation starten würde, wird ein AOT-Compiler genutzt, um Maschinencode zu erstellen. Das erzeugte Native Image ist, wie der Name schon impliziert, nativ ausführbarer Maschinencode. Damit geht allerdings die Plattformunabhängigkeit verloren.

Zusätzlich zum nativ kompilierten Programm wird die sogenannte SubstrateVM in das Native Image aufgenommen. Hierbei handelt es sich um eine abgespeckte VM, welche nur die zur Ausführung des Native Image notwendigen Komponenten wie beispielsweise Thread Scheduling oder Garbage Collection enthält. Die SubstrateVM hat dabei auch Limitierungen, beispielsweise ist die Unterstützung eines Security Managers gar nicht vorgesehen.

Eine zusätzliche Steigerung der Startgeschwindigkeit wird erreicht, indem das Native Image bei der Erstellung bereits vorab initialisiert wird. Das Programm wird dazu nach dem Kompilieren soweit gestartet, bis die wesentlichen Initialisierungen erfolgt sind, aber noch kein Input von außen verarbeitet werden muss. Von diesem gestarteten Zustand wird ein Speicherabbild erstellt und in das Native Image gelegt.

An das „Was“ und das „Wie“ schließt sich nun noch ein eher kritisches „Warum“ an: Der AOT-Compiler ist schon ein halbes Jahrhundert bekannt und Java existiert mittlerweile auch seit einem Vierteljahrhundert. Vor allem in der Anfangszeit von Java wurden verschiedene AOT-Ansätze ausprobiert – welche sich jedoch nicht durchsetzen konnten. Warum sollte es jetzt anders sein? Warum wird ausgerechnet jetzt eine geringere Startzeit interessant, wenn sie doch mit Nachteilen verbunden ist? Warum sind die performanten Antwortzeiten im darauffolgenden tage- oder wochenlangen Betrieb plötzlich weniger wichtig?

Die Antwort ist im Cloud Computing zu suchen: Hier werden die Services in einer anderen Form zur Verfügung gestellt. Bisher wurden die Services vorwiegend in einem Anwendungscontainer betrieben, welcher Tag und Nacht ausgeführt wird und in welchem das Programm schon seit Tagen komplett durchoptimiert? ist. Schließlich wurde der Anwendungscontainer üblicherweise auch bei spärlicher Nutzung (z. B. Tageszeiten-abhängig) nicht heruntergefahren. Im Gegensatz dazu kann in der Cloud die Service-Infrastruktur bei Nicht-Nutzung problemlos heruntergefahren werden – somit können Kapazitäten gespart werden. Beim nächsten Aufruf wird die Infrastruktur wieder hochgefahren und der Aufruf wird ausgeführt. Das bedeutet, dass die Programme in der Cloud nicht im Dauerbetrieb laufen, sondern dass es sich unter Umständen bei jedem Aufruf um einen Kaltstart handelt. Demzufolge ist die Startzeit hier „auf einmal“ sehr entscheidend. Und da zu erwarten ist, dass zukünftig noch mehr Java-Programme in der Cloud statt in einem Anwendungscontainer ausgeführt werden, wird sich die Fokussierung auf die Startzeit wohl noch verstärken.

Hands On: HelloWorld

Nach der ganzen Theorie möchten wir dem JDK nun bei der Arbeit zuschauen. Dazu kommt zunächst die in Listing 1 dargestellte Klasse „HelloWorld“ zum Einsatz.

package de.zeiss.zdi.graal;

public class HelloWorld {

    public static void main(String[] args) {
        System.out.println("Moin!");
    }

}

Listing 1

Zunächst die klassische Variante: Wir befinden uns auf einer Linux-VM und es ist ein OpenJDK installiert:

> java --version

openjdk 11.0.11 2021-04-20
OpenJDK Runtime Environment (build 11.0.11+9-Ubuntu-0ubuntu2.20.04)
OpenJDK 64-Bit Server VM (build 11.0.11+9-Ubuntu-0ubuntu2.20.04, mixed mode, sharing)

java 11.0.12 2021-07-20 LTS
Java(TM) SE Runtime Environment GraalVM EE 21.2.0.1 (build 11.0.12+8-LTS-jvmci-21.2-b08)
Java HotSpot(TM) 64-Bit Server VM GraalVM EE 21.2.0.1 (build 11.0.12+8-LTS-jvmci-21.2-b08, mixed mode, sharing)

Mit diesem Setup kompilieren wir die HelloWorld-Klasse (javac) und führen den entstandenen Bytecode auf einer JVM aus:

time java -cp target/classes de.zeiss.zdi.graal.HelloWorld

Damit erhalten wir folgende Ausgabe:

Moin!

real    0m0.055s
user    0m0.059s
sys     0m0.010s

Für die Auswertung ist hier die Summe der beiden Zeilen „user“ und „sys“ relevant. Das ist die Rechenzeit, die für die Programmausführung notwendig war – in dem Fall also ca. 69ms.

Eine Anmerkung zu den 55ms: Das HelloWorld-Programm hat vom Start bis zu seiner Beendigung 55ms „Real-Zeit“ (die Zeit, welche der Nutzer wahrnimmt) benötigt. Das ist weniger als die 69ms benötigte Rechenzeit. Dies liegt daran, dass das eingesetzte Linux-System mehrere Prozessoren besitzt. Für unsere Messungen soll hier aber die vom System aufgewandte Rechenzeit betrachtet werden. Erstens ist die Rechenzeit unabhängiger davon, wie viele Prozessoren das Programm ausgeführt haben. Und zweitens ist dies in der Cloud beispielsweise auch die Zeit, welche der Anwendungsbetreiber bezahlen muss.

Nun sind wir auf die GraalVM gespannt. Auf deren Homepage [3] wird sie zum Download angeboten. Für unsere Evaluation ist die Enterprise-Variante („Free for evaluation and development“) passend, da ein Großteil der Performance-Optimierungen nur hier enthalten sind.

Die Installation ist für Linux sehr gut dokumentiert und funktioniert nahezu problemlos. Damit ist die GraalVM als JDK nutzbar.

> java --version

java version "11.0.12" 2021-07-20 LTS
Java(TM) SE Runtime Environment GraalVM EE 21.2.0.1 (build 11.0.12+8-LTS-jvmci-21.2-b08)
Java HotSpot(TM) 64-Bit Server VM GraalVM EE 21.2.0.1 (build 11.0.12+8-LTS-jvmci-21.2-b08, mixed mode, sharing)

Unser HelloWorld-Programm können wir nun genauso mit dem GraalJDK kompilieren (javac) und ausführen. Damit erhalten wir die folgende Ausgabe:

Moin!

real    0m0.084s
user    0m0.099s
sys     0m0.017s

Interessanterweise benötigt die JVM des GraalJDK nahezu 70 % mehr Rechenzeit, um unser HelloWorld-Beispiel als Bytecode auszuführen. Nun verspricht die GraalVM den signifikanten Performance-Vorteil allerdings auch nicht primär bei der Ausführung von Bytecode, sondern bei Nutzung der Native-Image-Technologie.

Das native-image (das Entwickler-Tool) ist in der heruntergeladenen GraalVM noch nicht enthalten, dafür existiert aber das Kommandozeilen-Tool „gu“ (GraalVM Updater). Mit diesem kann man zusätzliche Komponenten nachladen, verwalten und aktualisieren. Auch hier unterstützt die Dokumentation der GraalVM sehr gut. Mit dem nachgeladenen Entwickler-Tool können wir nun aus dem Bytecode das Native Image erzeugen. Im Fall eines so trivialen Programms wie unserem HelloWorld-Beispiel genügt dazu ein einfacher Kommandozeilenbefehl mit dem vollqualifizierten Klassennamen als Argument:

cd ~/dev/prj/graal-eval/target/classes
native-image de.zeiss.zdi.graal.HelloWorld

Die Erstellung des HelloWorld Native Image benötigt reichlich 3 Minuten Rechenzeit – und das ausführbare Programm ist ca. 12 MB groß. Auf den ersten Blick vergleicht man die Größe vermutlich mit dem Bytecode: Die HelloWorld.class ist lediglich 565 Byte groß. Allerdings enthält das Native Image nicht nur die kompilierte Klasse, sondern alle relevanten Teile der Java-Klassenbibliothek sowie die SubstrateVM. Verglichen mit der Größe einer JRE liegt das Native Image nur noch bei grob geschätzt 10 %.

Doch zurück zu unserem Native Image: Wir haben es erfolgreich erstellt, können es ausführen und erhalten dabei die folgende Ausgabe.

time ./de.zeiss.zdi.graal.helloworld
Moin!

real    0m0.004s
user    0m0.003s
sys     0m0.001s

Dieses Resultat können wir erst einmal als relevanten Geschwindigkeitsgewinn stehen lassen.

Hands On: HelloScript

Bei der GraalVM wird immer wieder herausgestellt, dass es sich dabei nicht nur um eine Java-VM sondern um eine polyglotte VM handelt. Aus diesem Grund erweitern wir unser HelloWorld-Programm noch um einen kleinen Exkurs in die JavaScript-Welt. Der Quellcode dazu ist in Listing 2 dargestellt. Der wesentliche Unterschied ist hier der notwendige Übergang von der Java- in die JavaScript-Welt.

package de.zeiss.zdi.graal;

import javax.script.ScriptEngine;
import javax.script.ScriptEngineManager;
import javax.script.ScriptException;

public class HelloScriptEngine {

    public static void main(String[] args) throws ScriptException {

        ScriptEngine jsEngine = new ScriptEngineManager().getEngineByName("javascript");

        System.out.print("Hello ");
        jsEngine.eval("print('JavaScript!')");
    }

}

Listing 2

Neben dieser universellen JavaScript-Anbindung mittels der javax.script.ScriptEngine wollen wir auch die Graal-spezifische JavaScript-Anbindung ausprobieren. Dazu nutzen wir org.graalvm.polyglot.Context. Der Quelltext ist in Listing 3 dargestellt.

package de.zeiss.zdi.graal;

import org.graalvm.polyglot.Context;

public class HelloScriptPolyglot {

    public static void main(String[] args) {

        System.out.print("Hello ");
        try (Context context = Context.create()) {
            context.eval("js", "print('JavaScript!')");
        }
    }

}

Listing 3

Die beiden HelloScript-Programme werden analog dem HelloWorld-Programm in Bytecode übersetzt. Bei der Erstellung der Native Images muss das Entwickler-Tool noch informiert werden, dass die JavaScript-Welt genutzt werden wird. Dies geschieht mit dem folgenden Aufruf:

cd ~/dev/prj/graal-eval/target/classes
native-image --language:js de.zeiss.zdi.graal.HelloScriptEngine
native-image --language:js de.zeiss.zdi.graal.HelloScriptPolyglot

Anschließend kann der Bytecode auf den VMs oder die Native Images nativ ausgeführt werden. Da das HelloScriptPolyglot Graal-spezifisch ist, können wir es allerdings nicht ohne Weiteres auf dem OpenJDK ausführen.

Ein Blick auf die Messwerte

Die drei Szenarien wurden jeweils als Bytecode auf dem OpenJDK, als Bytecode auf dem GraalJDK und als Native Image ausgeführt. Die durchschnittlichen Programmausführungszeiten sind in Tabelle 1 aufgeführt.

Hello WorldHelloScriptEngineHelloScriptPolyglot
Bytecode OpenJDK69 ms1321 msX
Bytecode GraalJDK116 ms2889 ms2775 ms
Native Image4 ms13 ms11 ms

Tabelle 1: Beispiel für durchschnittliche Programmausführungszeiten

Auf den ersten Blick fällt ins Auge, das die Ausführung als Native Image in allen drei Szenarien jeweils um ein Vielfaches schneller ist als die übliche Bytecode-Ausführung.

Auf den zweiten Blick fällt aber auch auf, dass die Bytecode-Ausführung mit dem GraalJDK wesentlich mehr Rechenzeit benötigt als mit dem OpenJDK: Im HelloWorld-Beispiel knapp 70 % mehr, bei dem HelloScriptEngine-Beispiel über 100 % mehr. Das wurde von Oracle so nicht kommuniziert, ist aber grundsätzlich auch kein allzu großes Problem, da die Motivation für den Einsatz der GraalVM vermutlich nicht in der schnelleren Bytecode-Ausführung liegt. Man sollte diesen Fakt aber im Hinterkopf behalten, wenn man den relevanten Speed-Up durch das Native Image ermitteln möchte: Zur Erstellung des Native Image muss schließlich die GraalVM installiert sein. Wenn man nun zum Vergleich die Bytecode-Ausführung misst und „java -jar …“ ausführt, wird der Bytecode mittels der GraalVM ausgeführt. Da im produktiven Betrieb aber bisher vermutlich eher das OpenJDK eingesetzt wurde, sollte man eher mit diesem vergleichen – und damit wäre der Speed-Up „nur“ noch reichlich halb so hoch.

Was zu bedenken ist

Um die versprochenen Performance-Gewinne zu erzielen, reicht es nicht, die GraalVM anstatt eines herkömmlichen JDKs zu installieren. Bei der Bytecode-Ausführung konnte – zumindest mit unseren Beispielen und unserem Setup – noch kein Performance-Gewinn erreicht werden. Dies ist erst mit einem Native Image möglich, welches gegenüber der Bytecode-Ausführung jedoch auch mehrere Nachteile hat, derer man sich bewusst sein sollte.

  • Im Native Image wird die SubstrateVM als JVM verwendet. Diese besitzt einige Einschränkungen. Abgesehen davon, dass derzeit noch nicht alle Features umgesetzt sind, stehen einige Dinge wie z. B. ein Security Manager gar nicht auf der Agenda.
  • Weiterhin sollte die Dauer des Build-Prozesses beachtet werden: Beim Native Image verschwindet die Startzeit nicht einfach. Die Rechenzeit wird mit verschiedenen Ansätzen „lediglich“ verlagert: Von der Ausführungszeit zur Build-Zeit. Die Erstellung unseres HelloWorld-Beispiels hat in unserer Umgebung reichlich drei Minuten benötigt – die Erstellung des HelloScript-Programmes dauerte bereits mehr als 20 Minuten (HelloScriptEngine: 1291s, HelloScriptPolyglot: 1251s).
  • Die größte Herausforderung ist aber die „Closed world assumption“. Bei der Erstellung des Native Image wird eine statische Codeanalyse durchgeführt – und nur die durchlaufenen Code-Teile werden in das Native Image kompiliert. Das funktioniert zwar für unser HelloWorld-Programm, aber bereits bei den JavaScript-Beispielen mussten Kommandozeilen-Parameter angegeben werden. Mittels „Reflection“ geladene Klassen werden nur erkannt, wenn der vollqualifizierte Klassenname festverdrahtet im Quellcode steht. Demzufolge gibt es Probleme mit allen Technologien, welche Dynamic Class Loading in irgendeiner Form nutzen: JNDI, JMX, …

Die dynamisch geladenen Programmteile können (und müssen) bei der Erstellung des Native Image explizit angegeben werden. Das schließt alle Programmteile ein: Neben dem eigenen Projekt-Code sind das auch alle eingesetzten Bibliotheken – bis hin zu denen der JRE. Da diese Konfiguration für „echte“ Programme eine wirkliche Herausforderung ist, werden hierzu Hilfswerkzeuge bereitgestellt, ohne die es in der Praxis vermutlich nicht funktioniert. Der Tracing Agent beispielsweise beobachtet ein als Bytecode ausgeführtes Programm. Er erkennt alle reflektiven Zugriffe und erzeugt daraus eine JSON-Konfiguration. Diese kann nun für die Erstellung des Native Image verwendet werden.

In der Praxis würde die Build-Pipeline also zunächst die Bytecode-Variante erstellen. Nun können alle automatisierten Tests mit dieser Bytecode-Variante ausgeführt werden, wobei der Tracing Agent die reflektiven Zugriffe erkennt. Unter der Annahme, dass dabei wirklich jeder Programmpfad ausgeführt wurde, kann nun das Native Image in einem weiteren Build-Schritt erzeugt werden. Dies führt direkt zum nächsten Punkt: Bei der Arbeit mit der Native-Image-Technologie wird der Build-Prozess insgesamt länger und komplexer.

Zusammenfassend bedeutet dies, dass beim Einsatz der Native-Image-Technologie einige Dinge kaum oder nicht möglich sind (bspw. Security Manager). Viele andere Dinge funktionieren zwar grundsätzlich, müssen aber umständlich konfiguriert werden. Hierfür ist eine Tool-Unterstützung gegeben und wird auch recht dynamisch weiterentwickelt. Die Hoffnung ist hier, dass die Mehraufwände (abgesehen von der Build-Dauer) durch die Werkzeuge kompensiert werden können. Allerdings wird dadurch der Build-Prozess auch komplexer und somit fehleranfälliger.

Fallstricke unter Windows

Abschließend noch ein Blick zur Windows-Plattform: Diese wird mittlerweile auch unterstützt. Vorbereitend für diesen Blogbeitrag wurden die Versionen „GraalVM Enterprise 20.3.0″ sowie „GraalVM Enterprise 21.0.0.2“ auf einem Windows-System betrachtet. Leider war hier die Dokumentation noch etwas lückenhaft und das Tooling greift noch nicht so gut ineinander wie in der Linux-Umgebung. Dadurch gab es auch Hindernisse, die unter Linux nicht aufgefallen sind. So trat beispielsweise ein Problem bei der Erstellung eines Native Images auf, wenn der zugrundeliegende Bytecode von einem anderen JDK (in dem Fall durch das OpenJDK) erzeugt wurde. Dabei ist die auftretende Fehlermeldung leider auch nicht so aussagekräftig, dass sie auf die eigentliche Ursache hinweist:

native-image de.zeiss.zdi.graal.HelloWorld

[de.zeiss.zdi.graal.helloworld:20764]    classlist:     947.02 ms,  0.96 GB
[de.zeiss.zdi.graal.helloworld:20764]        (cap):   3,629.54 ms,  0.96 GB
[de.zeiss.zdi.graal.helloworld:20764]        setup:   5,005.98 ms,  0.96 GB

Error: Error compiling query code (in C:\Users\xyz\AppData\Local\Temp\SVM-13344835136940746442\JNIHeaderDirectives.c). Compiler command ''C:\Program Files (x86)\Microsoft Visual Studio\2019\BuildTools\VC\Tools\MSVC\14.28.29333\bin\HostX64\x64\cl.exe' /WX /W4 /wd4244 /wd4245 /wd4800 /wd4804 /wd4214 '-IC:\Program Files\Java\graalvm-ee-java11-21.0.0.2\include\win32' '/FeC:\Users\xyz\AppData\Local\Temp\SVM-13344835136940746442\JNIHeaderDirectives.exe' 'C:\Users\xyz\AppData\Local\Temp\SVM-13344835136940746442\JNIHeaderDirectives.c' ' output included error: [JNIHeaderDirectives.c, Microsoft (R) Incremental Linker Version 14.28.29337.0, Copyright (C) Microsoft Corporation.  All rights reserved., , /out:C:\Users\xyz\AppData\Local\Temp\SVM-13344835136940746442\JNIHeaderDirectives.exe , JNIHeaderDirectives.obj , LINK : fatal error LNK1104: Datei "C:\Users\xyz\AppData\Local\Temp\SVM-13344835136940746442\JNIHeaderDirectives.exe" kann nicht ge?ffnet werden.]

Error: Use -H:+ReportExceptionStackTraces to print stacktrace of underlying exception
Error: Image build request failed with exit status 1

Ein weiterer Fallstrick bestand in der laufwerksübergreifenden Arbeit: Es ist unter Windows leider nicht möglich, die GraalVM auf einem Laufwerk zu installieren (in dem Fall unter C:\Programme) und auf einem anderen Laufwerk auszuführen (in dem Fall unter D:\dev\prj\…):

native-image de.zeiss.zdi.graal.HelloWorld

[de.zeiss.zdi.graal.helloworld:10660]    classlist:   3,074.80 ms,  0.96 GB
[de.zeiss.zdi.graal.helloworld:10660]        setup:     314.93 ms,  0.96 GB

Fatal error:java.lang.IllegalArgumentException: java.lang.IllegalArgumentException: 'other' has different root
        at java.base/jdk.internal.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method)
[…]

Darüber hinaus konnten mit dem Native Image in der Windows-Umgebung leider keine Performance-Vorteile festgestellt werden. Derzeit ist die Windows-Unterstützung (sowohl die GraalVM-Toolchain selbst als auch die generierten Native Images) somit noch eher experimentell.

Fazit

Im Blogbeitrag wurde vor allem darauf eingegangen, dass die Startzeit der Java-Programme mit GraalVMs Native-Image-Technologie enorm verbessert werden kann. Es wurde dargestellt, welchen Ansätzen und welcher Techniken sich die GraalVM dabei bedient. Das Resultat wurde mit Messungen an Beispiel-Programmen untermauert. Auf der anderen Seite wurden aber auch einige Herausforderungen genannt, welche beim Einsatz der Native-Image-Technologie auftreten.

Mit länger laufenden Programmen sind vermutlich kaum Performance-Steigerungen zu erwarten, weil dann auch die Optimierungen der herkömmlichen JVMs greifen. Das ist erst einmal nur eine Behauptung. Eine Untersuchung dieses Aspekts würde den aktuellen Rahmen sprengen und hat genug Potenzial für einen eigenen Blogbeitrag.

Nun wollen wir uns noch kurz den Fragen aus der Einleitung zuwenden: Grundsätzlich ist das „herkömmliche“ Java schon lange nicht mehr langsam, sondern rasend schnell. Schon allein der Einsatz im (rechenintensiven) Big-Data-Umfeld ist ein Indiz hierfür. Die hauptsächliche Vorbedingung für diese hohe Performance ist eine gewisse Warmlaufzeit. Im Umkehrschluss heißt das: Der Start eines herkömmlichen Java-Programms lässt zu wünschen übrig – und genau dies ist der Hauptansatzpunkt der Native-Image-Technologie. Auf der anderen Seite bringt diese Technologie auch eine Menge Nachteile mit – vor allem für große, technik-reiche Anwendungen.

Zusammenfassend lässt sich sagen, dass die GraalVM das Potenzial hat, sich in verschiedenen Einsatzgebieten zu etablieren. Die polyglotten Eigenschaften können sich mehrsprachige Anwendungen zunutze machen und der Einsatz der betrachteten Native-Image-Technologie ist vor allem für Cloud Services durchaus denkbar. Allerdings wird sich der Einsatz der GraalVM vor allem für rechenintensive (d. h. meist auch länger laufende) nicht-triviale Anwendungen vermutlich nicht lohnen.

Abschließend soll noch als Vorteil für die GraalVM festgehalten werden, dass Compiler und Optimizer in Java implementiert sind. Das ist zwar zunächst grundsätzlich nicht besser oder schlechter als die bisherigen Implementierungen – wird aber mittel- bis langfristig die Chancen erhöhen, das Potential der Java-Community besser zu nutzen.

Alles in allem bleibt es wohl spannend: Eine grundsätzliche Ablösung des OpenJDK ist derzeit nicht absehbar. Und man darf schließlich auch nicht vergessen, dass die Entwicklung dort genauso wenig stillsteht. Allerdings hat die GraalVM durchaus das Potenzial, um sich (zunächst erst einmal?) in bestimmten Anwendungsgebieten zu etablieren.

Security und Compliance in Softwareprojekten – Dependencies unter Kontrolle bringen

Symbolbild: Weiße Tastatur mit Schloss und Schlüssel

Dieser Blogbeitrag befasst sich mit den hohen Ansprüchen an Security und Compliance, die wir an jedes Softwareprojekt stellen. Dafür verantwortlich ist in jedem Projekt ein ausgebildeter Security Engineer. Dabei stellen ihn insbesondere die unzähligen Dependencies in Softwareprojekten, welche in ihrer Vielzahl von Versionen unter Kontrolle gebracht werden müssen, vor große Herausforderungen.

Abbildung 1: Ein Ausschnitt aus dem Abhängigkeits-Graph eines npm Paketes, aus npmgraph.js.org/?q=mocha

Herausforderungen in Softwareprojekten

Große Softwareprojekte bestehen schon seit langer Zeit aus kleineren Teilen, die für ihr jeweiliges Gebiet wiederverwendet werden können. Komponenten, bei denen es nicht um geheime Funktionalität geht, werden zunehmend als „FOSS (Free and Open Source Software)“ veröffentlicht. Das bedeutet „quelloffen“ (Open Source) und mit einer freien Lizenz zur Weiterverwendung.

Dabei ist es für die Einschätzung und Prävention von Sicherheitslücken äußerst wichtig, eine vollständige Übersicht über alle eingebundenen Drittbibliotheken zu haben. Denn jedes unserer importierten Module kann ebenfalls mit mehreren Abhängigkeiten verbunden sein. Schnell steigt dann die Anzahl an zu beobachtenden Abhängigkeiten in die Tausende und es ist nicht einfach, zwischen allen Versionen den Überblick über Lizenzen und Sicherheitslücken zu behalten.

Die Auswirkung der Problematik wird z. B. klar, wenn man Fälle von „Supply chain attacks“ und „Dependency Hijacking“ der letzten Jahre liest. Eine interessante Meta-Analyse ist „What Constitutes a Software Supply Chain Attack? “ von Ax Sharma (https://blog.sonatype.com/what-constitutes-a-software-supply-chain-attack). Den Umgang mit diesen Komponenten in großen wie kleinen Softwareprojekten aus Sicht eines Security Engineers möchten wir weiter erläutern.

Lösungsmöglichkeiten mittels FOSS Scanner

Über die Zeit haben sich einige Projekte dem Problem der Kenntlichmachung von FOSS-Komponenten gewidmet. Es gibt Programme zum Erstellen von Bill of Material (BOM) und Übersichten zu Sicherheitsrisiken, welche wir verprobt haben.

Weiter gibt es große Kataloge wie den „Node Paketmanager“ (npm), die selbst ausführliche Informationen zu den jeweils angebotenen Komponenten geben.

Auch wenn es diese freien und quelloffenen Komponenten gratis gibt, so sind sie nicht ohne Aufwand, besonders in langlebigen und wichtigen Softwareprojekten.

Wir haben für die Evaluierung den OWASP-Dependency Check (DC) und das OSS Review Toolkit als kombinierte Lösung für das Auffinden von Sicherheitsproblemen mit DC und Überprüfung der Einhaltung der Lizenzbestimmungen eingesetzt. Im Vergleich zu kommerziellen Lösungen wie BlackDuck bieten diese frei und kostenlos die Möglichkeit einer Übersicht über die FOSS-Komponenten in Projekten und die Bewertung von Risiken.

Das war aber unserer Erfahrung nach mit Mehraufwand sowohl in der Konfiguration als auch bei der kontinuierlichen Überprüfung, d. h. neuen Scans auf neue Sicherheitsprobleme, verbunden.

Verantwortung als Software Engineer

Unsere Richtlinien für sichere Entwicklung und den Einsatz von Open Source geben die notwendigen Prozesse und Ziele vor, an dem sich unsere Security Engineers in Vertretung der Projekte orientieren. Der vielleicht wichtigste Ausschnitt daraus wird im folgenden Abschnitt aufgeführt:

It is our responsibility that the following so called Essential FOSS Requirements are fulfilled:

  • All included FOSS components have been identified and the fitness for purpose has been confirmed.
  • All licenses of the included FOSS have been identified, reviewed and compatibility to the final product/service offering has been verified. Any FOSS without a (valid) license has been removed.
  • All license obligations have been fulfilled.
  • All FOSS are continuously – before and after release – monitored for security vulnerabilities. Any relevant vulnerability is mitigated during the whole lifecycle.
  • The FOSS Disclosure Statement is available to the user.
  • The Bill of Material is available internally.

For that it must be ensured that

  • the relevant FOSS roles are determined and nominated.
  • the executing development and procurement staff is properly trained and staffed.

Anhand dieser Richtlinien werden verpflichtende Trainings, Wissensträger und Qualitätskontrollen gebildet.

Vorstellung der Abläufe

  • Untersuchen vor Einbindung (Lizenzen, Operational Risk wie Update-Häufigkeit)
  • Überwachen von Updates (Operational Risks)

Irgendwann soll eine neue Funktion zu einem Softwareprojekt hinzugefügt werden. Oft kennen Entwickler bereits mögliche FOSS Software, die bei der Funktionalität hilft.

Ein wichtiger Aspekt ist, dass möglichst jeder Entwickler den Umgang mit Paketmanagern und mögliche Implikationen kennt, um Ergebnisse aus den Tools oder Analysen richtig einordnen zu können. Es ist z. B. sehr wichtig, sich zu veranschaulichen, aus wie vielen Teilen eine Top-Level-Abhängigkeit besteht – oder verschiedene Abhängigkeiten gleicher Funktionalität im Hinblick auf zukünftige sichere Entwicklung (Operationelle Risiken) zu bewerten. Immer öfter sehen wir das Ziel, die Zahl an Abhängigkeiten klein zu halten. Das sollte bei der Auswahl von Komponenten berücksichtigt werden, um möglichst nur das wirklich notwendige an Funktionalität von zusätzlichen Abhängigkeiten zu erhalten.

Bereits vor dem Einbinden sind durch den Security Engineer potenzielle Imports auf ihre kompatible Lizenz und bestehende Sicherheitslücken zu überprüfen. Ebenso wichtig ist aber auch der Blick auf das, was unter operationale Risiken fällt wie z. B.:

  • Aktualität
  • Lebendige Community oder aktive Instandhaltung
  • Update-Zyklus ausreichend agil, um auftretende Sicherheitslücken zu beseitigen
  • Wird Wert auf den sicheren Umgang mit Abhängigkeiten gelegt?
  • Ist die Anzahl an weiteren Abhängigkeiten sinnvoll und wird wenn möglich reduziert?

Im laufenden Entwicklungsprozess und später im Betrieb muss das Projektteam auch informiert werden, wenn neue Sicherheitslücken entdeckt oder geschlossen werden. Dafür können periodische Scans oder eine Datenbank mit Alerts für Sicherheitslücken eingesetzt werden. Für periodische Scans spricht die größere Unabhängigkeit von der einen Datenbank – dafür müssen Hardware und Alerts selbst bereitgestellt werden. Diese wiederum sind einer der Mehrwerte einer Software-Composition-Analysis-Lösung wie BlackDuck.

Da der Anteil an gut gekennzeichneter FOSS steigt, wird bei neuen Versionen der Zeitaufwand für manuelle Kuration vergleichsweise geringer. Dazu zählen das Deklarieren einer Lizenz – und leicht auffindbare und formatierte Copyright-Angaben in den Komponenten, was in älteren Komponenten oft sehr individuell formatiert oder ganz weggelassen wurde. Ist keine Lizenz angegeben, so darf dies nicht fälschlicherweise als „Freibrief“ verstanden werden. Ohne eine Lizenz darf eine Komponente nicht ohne Einverständnis der Autoren benutzt werden!

Beispiel einer Sicherheitslücke

Ein Beispiel für eine komplizierte Sicherheitslücke ist unter dem CVE-2021-32796 veröffentlicht worden. Eingebunden wird das problematische Modul xmldom indirekt über zwei weitere Abhängigkeiten in unserem Beispielprojekt.

BlackDuck zeigt uns zu dem Modul folgende Sicherheitswarnung:

Abbildung 2: BlackDuck: Beispiel Zusammenfassung einer Schwachstelle  

Damit kann der Security Engineer bereits eine grobe Einschätzung zur Tragweite der Sicherheitslücke vornehmen. Auch ist ein Hinweis auf dem Patch in Version 0.7.0 angegeben.

Wichtigkeit von Vorlauf für Updates/Austausch von Kompetenzen

Wir haben in der Zeit bis zu der „frischen Veröffentlichung“ unter @xmldom/xmldom bereits überprüfen können, welchen Aufwand es bedeuten würde, ohne diese Abhängigkeit auszukommen.

Um diese Zeit zu haben, ist es sehr nützlich, bereits im Entwicklungsprozess – und mit genügend Vorlauf zu einer Produktveröffentlichung – eine Übersicht über mögliche Probleme zu bekommen.

Das erleichtert den Entwicklern das Evaluieren von Ausweichlösungen für problematische Software-Bibliotheken, sei es wegen Sicherheitslücken, inkompatiblen Lizenzen oder anderer operativer Risiken.

Fazit

Dieser Beitrag hat einen Überblick über unsere Arbeit mit der großen Vielfalt an Open Source in unseren Projekten und die Aufgaben als Security Engineer im Umgang mit Open Source gegeben. Damit bringen wir mittels moderner Werkzeuge die Vielfalt an Abhängigkeiten unter Kontrolle und schaffen die notwendige Transparenz und Sicherheit. Bereits vor Einbinden von Abhängigkeiten sollte eine Evaluierung dieser von einem geschulten Team durchgeführt werden, und danach während des ganzen Software-Lebenszyklus überwacht und auf Probleme reagiert werden.

Mit Matrizen bewerten – Die perfekte Entscheidungsmatrix

Ein nicht unerheblicher Teil der Arbeit eines Softwarearchitekten besteht darin, verschiedene Lösungsalternativen miteinander zu vergleichen. Hierbei wird oft auf Entscheidungstabellen bzw. Bewertungsmatrizen zurückgegriffen, wobei beide Begriffe in aller Regel synonym verwendet werden. Dieser Artikel soll einen Einblick in zwei grundlegende Vorgehen bieten und jene nach ihrer Eignung bewerten.

Symbolbild: Blick auf die Füße einer Person in weißen Turnschuhen, die auf einer asphaltierten Straße auf einem aufgemalten weißen Pfeil steht, der in zwei verschiedene Richtungen zeigt
Abbildung 1: Softwarearchitekten müssen oft verschiedene Lösungsalternativen miteinander vergleichen. Dafür nutzen sie oft bestimmte Bewertungsmatrizen.

Arten von Bewertungsmatrizen

Bewertungsverfahren, um mehrere zur Auswahl stehende Varianten miteinander zu vergleichen, reichen von einfachen, über Abstimmung entstandene Rankings bis hin zu komplizierten Bewertungsverfahren über Matrizenrechnungen. Die Herausforderung besteht darin, für den Vergleich zwei zur Auswahl stehender Optionen die am besten geeignete Methodik zur objektivierten Bewertung zu finden. Kriterien hierfür sind:

  • Schneller Vergleich
  • Einfaches, unkompliziertes Verfahren
  • Geringe bis keine Einarbeitungszeit
  • Beliebige Personenanzahl

Nach kurzem Vergleichen der Bewertungsverfahren anhand der genannten Kriterien stellt sich hier die sehr verbreitete und bekannte Nutzwertanalyse als das Mittel der Wahl heraus. Warum? Weil sich mit ihr unkompliziert und einfach Optionen anhand verschiedener Kriterien vergleichen lassen, ohne dass es eines großen mathematischen Aufwandes bedarf. Die Varianten werden bei der Nutzwertanalyse mithilfe gewichteter Kriterien mit einem Score bewertet, Gewichtung und Score miteinander multipliziert und alle Bewertungen für eine Option addiert (siehe Beispiel).

Tabelle entsprechenden Kriterien
Abbildung 2: Bewertung der einzelnen Varianten in einer Nutzwertanalyse

Dieses Verfahren sollte fast jedem geläufig sein und findet auch in der Praxis in vielen Bereichen Anwendung. Neben der Bewertung durch die bewertende Person ergibt sich eine potenzielle subjektive Fehlerquelle: die Gewichtung der Kriterien. Da die Score-Vergebung nicht objektiver gestaltet werden kann, muss eine Möglichkeit der objektiven Gewichtungsberechnung gefunden werden. Diese ist absolut unumgänglich, wenn eine aussagekräftige Nutzwertanalyse gefordert ist. Die objektivierte Gewichtung stellt sicher, dass keine durch z. B. Zeitdruck entstandenen, „kopflosen“ Entscheidungen getroffen werden und die Gewichtung möglichst unabhängig vom Betrachtenden ist.

Verfahren zur Gewichtungsberechnung

Wie schon bei der Nutzwertanalyse besteht hierbei der Anspruch, dass das Verfahren unkompliziert, schnell und ohne Einarbeitungszeit erfolgt. Unter diesen Prämissen kristallisieren sich insbesondere zwei Verfahren heraus, die im Folgenden kurz erläutert werden.

Abgestufte Gewichtung

Bei der abgestuften Gewichtungsberechnung werden alle Kriterien hinsichtlich ihrer Bedeutung untereinander verglichen. Dabei umfasst die Skala fünf Abstufungen von -2: „wesentlich kleinere Bedeutung“ bis hin zu 2: „wesentlich größere Bedeutung“. Für jede Paarung von Kriterien muss daher diese granulare Einschätzung erfolgen. Über eine an Matrizenrechnung angenähertes Verfahren wird dann die Gewichtung berechnet.

Skala mit Legende
Abbildung 3: Beispiel für eine Skala bei einer abgestuften Gewichtung

Prioritätengewichtung

Hierbei wird auf die granulare Bewertung verzichtet und lediglich unterschieden in „wichtiger“ oder „unwichtiger“. Somit wird jedes Kriterium mit jedem anderen verglichen und das wichtigere in der Tabelle notiert. Über den relativen Anteil der Anzahl eines Kriteriums wird dann die Gewichtung ermittelt. Diese Vorgehensweise lässt sich gut im Team integrieren, da man nach dem Ranking durch die Einzelpersonen alle Ergebnisse zusammenführen kann und somit eine repräsentative Gewichtung erhält.

Tabelle mit verschiedenen Kriterien
Abbildung 4: Beispiel für eine Prioritätengewichtung einzelner Kriterien

Wann ist welches Verfahren geeignet?

Prinzipiell kann jedes Verfahren zur Gewichtungsberechnung in jeder Situation angewendet werden. Die folgende Tabelle gibt aber Anhaltspunkte, bei welchen Gegebenheiten welche Berechnungsart vorteilhaft sein kann.

Abgestufte GewichtungPrioritätengewichtung
Geringe Anzahl an KriterienHohe Anzahl an Kriterien
Geringe Anzahl an bewertenden PersonenHohe Anzahl an bewertenden Personen
Ausreichend Zeit verfügbarWenig Zeit verfügbar
Besonders wichtige Entscheidung 

Zusammenfassend kann gesagt werden, dass das Verfahren der abgestuften Gewichtung häufig zu aufwändig ist, wenn man in der Praxis davon ausgeht, dass Entscheidungen teilweise nicht einmal von einem repräsentativen Bewertungsverfahren begleitet werden. Die Prioritätengewichtung hingegen ist eine unkomplizierte, schnell verständliche und auch im Team implementierbare Möglichkeit und wird daher besonders empfohlen.

Neben der Nutzwertanalyse können auch noch weitere Verfahren und Kennzahlen zum Variantenvergleich hinzugezogen werden, beispielweise Standardabweichung, Anzahl Gewinne/Verluste usw., welche die schlussendliche Entscheidung leichter machen sollen, aber nicht Bestandteil dieses Beitrags sind.

Tester-Tea-Time (Teil 1): Der Mythos des „historischen Software-Wachstums“ auf dem Prüfstand

Die „Tester-Tea-Time“ ist ein Beitragsformat auf diesem Blog, in dem Themen aufgegriffen werden, die Testerinnen und Tester tagtäglich beschäftigen. Gewisse Problemstellungen oder Themen kehren immer wieder, daher soll hier eine Basis geschaffen werden, solche Phänomene zu erläutern und Lösungen dafür zu finden. Zudem sollen Diskussionen und neue Denkweisen angeregt werden. Im Testing können wir viel voneinander lernen, indem wir unseren Alltag beobachten!

Moderator: Willkommen zur ersten Tester-Tea-Time! Im Interview mit Testerinnen und Testern der ZEISS Digital Innovation (ZDI) werden wir spannende Themen diskutieren.

Widmen wir uns nun dem heutigen Thema. Dazu sprechen wir mit Sandra Wolf (SW), Testerin bei ZDI. Worum geht es bei dem Mythos des „historischen Wachstums“?

SW: Versierte Testerinnen und Tester werden bei der Überschrift des Artikels wahrscheinlich ungewollt innerlich mit den Augen rollen. Denn was hier so geschichtsträchtig anklingt, ist für viele eine der häufigsten Antworten, die im Arbeitsalltag ihren Weg kreuzen. Sobald eine Abweichung in der Software gefunden wird, die nicht sofort behoben werden kann oder soll, wird dieser Ausspruch sehr gern verwendet. Beispiele für solche Abweichungen sind mangelhafte Benutzerfreundlichkeit und eine stetige Inkonsistenz in der zu testenden Software. Wird von Seiten der Testerinnen und Tester hier auf eine Verbesserung aufmerksam gemacht, erhalten diese von Teammitgliedern aus den Bereichen Entwicklung, Projektleitung o. a. gern die Antwort, dass diese bestimmte Auffälligkeit historisch so gewachsen wäre und daher kein Handlungsbedarf bestehe. Oft bleiben die um Qualität bemühten Testerinnen und Tester ratlos und frustriert zurück – wird ihnen doch keine Handlungsperspektive im Sinne ihrer Rolle aufgezeigt. Was hat es daher mit dem Ausspruch des „historischen Wachstums“ auf sich? Und wie kann damit professionell und effektiv umgegangen werden? Darum soll es heute gehen.

Zwei Kollegen zum Interview über Videokonferenz miteinander verbunden
Abbildung: Hendrik Lösch und Sandra Wolf in der virtuellen Tester-Tea-Time im Gespräch.

Moderator: Okay, das klingt interessant. Welche Auswirkungen hat denn dieses sogenannte „historische Wachstum“ auf das Projekt und deren Software?

SW: Von vielen Projektbeteiligten wird die Brisanz der Problemstellung verkannt. Das „historische Wachstum“ ist ein Ausweichen, denn tatsächlich verbirgt sich dahinter die Angst vor einer Erneuerung der jeweiligen Software, die mit weiterem Ignorieren jedoch unvermeidlich näher rückt. Gerade die im Laufe der Jahre erweiterten Software-Bestandteile entwickeln mehr und mehr Wechselwirkungen, wodurch der Wartungsaufwand und die Wartungskosten immer weiter steigen. Die verschiedenen Erweiterungen passen unter Umständen gar nicht mehr zusammen, haben weder eine einheitliche Bedienung noch ein einheitliches Design. Die Qualität der Software sinkt somit kongruent zur Kundenzufriedenheit. Eine innovative Weiterentwicklung wird zu diesem Zeitpunkt immer schwerer oder existiert gar nicht mehr. Werden all diese Fakten betrachtet, wird schnell klar, dass das „historische Wachstum“ über eine bloße Floskel hinausgeht und schwerwiegende Folgen für das jeweilige Unternehmen, Projekt und die Software selbst haben kann. Hier muss somit etwas getan werden und ein Umdenken stattfinden.

Moderator: Wie kann das Projekt dem „historischen Wachstum“ den Kampf ansagen?

SW: Die Befürchtung vieler Projektmitglieder bezüglich des „historischen Wachstums“ ist, dass dieses Problem einzig und allein dadurch gelöst werden kann, eine Software einzustampfen und komplett neu aufzuziehen. Dass das aber selten Sinn ergibt, kann sich jeder denken. Auch eine Rundumerneuerung einer Software wäre sehr kostenintensiv. Nun stellt sich die Frage, wie eine Lösung dieses Problems aussehen kann. Hier möchte ich darauf hinweisen, dass die Aufgabe des Testers nicht darin liegt, das „historische Wachstum“ selbst zu beseitigen oder unter Kontrolle zu bringen. Wir als Tester können lediglich den Anstoß für Veränderungen geben und sollten dies auch unbedingt tun, da wir eine wichtige Schnittstellenfunktion zwischen Entwicklung, Fachabteilung und Projektleitung einnehmen. Unsere Perspektive über den gesamten Prozess ist hier Gold wert. Wir haben einen umfassenden Überblick über alle Schritte des Prozesses, da wir an dessen Ende agieren.

Moderator: Wie könnte eine Lösung für das „historische Wachstum“ aussehen?

SW: Bei dieser umfangreichen Problemstellung bietet es sich an, die Lösung zunächst in kleineren Schritten voranzutreiben. Zuerst sollte der Kontakt zu einer passenden Ansprechpartnerin oder einem passenden Ansprechpartner, z. B. zur Projektleitung, gesucht werden, da eine Testerin oder ein Tester niemals über solche übergreifenden Projektthemen entscheiden kann. Im Gespräch mit dieser Person kann dann auf die Folgen und Risiken des „historischen Wachstums“ aufmerksam gemacht werden. Gemeinsam im gesamten Team sollte dann das weitere Vorgehen definiert werden. Vielleicht muss nicht die komplette Software erneuert werden, sondern es wird sich auf bestimmte Teilbereiche der Anwendung beschränkt. Auch hier können Testerinnen und Tester wichtiges Wissen aus ihrem Alltag beisteuern. Die Entwicklung des gesamten Projekts muss beachtet werden und auch, welche Teilbereiche zuerst entstanden sind. Ziel des Gesprächs ist es, auf das Problem aufmerksam zu machen und somit den Anstoß für Veränderungen zu geben, um die Qualität der Software wiederherzustellen. In welcher Form das geschieht – beispielweise auch durch externe Unternehmen – muss ebenfalls entschieden werden. Für uns als Tester ist dies aber eine Gelegenheit, übergreifend zur Qualität der zu testenden Software beizutragen. Denn das Wichtigste ist am Ende, dass das sehr weitreichende Thema des „historischen Wachstums“ nicht mehr als Ausrede gilt, sondern endlich im Projekt platziert wird.

Moderator: Eine Lösung kann somit nur gemeinsam gefunden werden. In diesem Zusammenhang interessiert uns auch die Perspektive eines Entwicklers. Dazu sprechen wir nun mit Hendrik Lösch (HL), Software-Architekt bei ZDI. Welche Lösungsansätze siehst du für das „historische Wachstum“?

HL: Ich selbst würde es nicht als „historisches Wachstum” bezeichnen, da dies impliziert, dass das Wachstum in der Vergangenheit liegt. Ich spreche allgemeinhin lieber von “Softwareevolution”. Denn Wachstum ist immer vorhanden, problematisch wird es erst dann, wenn die Strukturen der Software ungewollt mutieren. Wir bezeichnen diese Mutationen auch als technische Schulden da sie zukünftige Investitionen in die Restrukturierung notwendig machen und dadurch Mittel binden, die nicht in wertsteigernde Maßnahmen fließen können. Je weiter man diese Restrukturierungen aufschiebt, desto schwerer sind sie durchzuführen. Aus dieser Richtung kommt dann wahrscheinlich auch das „historische Wachstum”. Es ist eben über die Zeit gewachsen und niemand weiß mehr genau warum. Um dieses Problem zu lösen, bedarf es einer Tiefenanalyse der Strukturen, bei der erfahrene Softwarearchitekten die Ist-Architektur ermitteln. Anschließend wird eine Soll-Architektur erstellt und ein Weg skizziert, um vom einen zum anderen zu gelangen. Mit dem Health Check bieten wir als ZDI hier ein geeignetes Verfahren an.

Moderator: Danke, Sandra und Hendrik für eure aufschlussreichen Ausführungen. Wir können somit zusammenfassen, dass die Problematik des „historischen Wachstums” durchaus eher ein Mythos ist. Wachstum ist nämlich stets vorhanden, allerdings kann es durch fehlende Struktur problematisch werden. Dafür gibt es aber nicht nur Handlungsbedarf, sondern auch konkrete Strategien zur Bewältigung. ZDI selbst bietet diese heute schon im Portfolio an.

In den folgenden Beiträgen werden wir weitere Problemstellungen aus dem Alltag von Testerinnen und Testern aufgreifen und besprechen, welche möglichen Lösungsansätze es dafür gibt.

Verteiltes Arbeiten – Ein Erfahrungsbericht zur praktischen Anwendung von Remote Mob Testing

Innerhalb von ZEISS Digital Innovation (ZDI) gibt es in regelmäßigen Abständen eine interne Weiterbildungsveranstaltung – den sogenannten ZDI Campus. Dabei präsentieren wir als Mitarbeiterinnen und Mitarbeiter Softwareentwicklungsthemen mittels Vorträgen, Workshops oder Diskussionsrunden. 

Katharina hatte beim vergangenen ZDI Campus schon einmal über kollaborative Testmethoden berichtet und auch Blogartikel bezüglich Remote Pair Testing sowie Remote Mob Testing veröffentlicht. Daher wollten wir das Thema gern auf dem nächsten ZDI Campus weiterführen und einen Workshop aus der Themenwelt kollaborativer Testmethoden anbieten. Aufgrund der Covid-19-Pandemie musste der nächste Campus jedoch online stattfinden. Dennoch wollten wir mehreren Teilnehmerinnen und Teilnehmern die Möglichkeit bieten, eine Testmethode am praktischen Beispiel anzuwenden und haben uns daher für einen remote Mob Testing Workshop entschieden.  

Doch es gab eine Herausforderung: Wir hatten noch nie mit so vielen Personen im Mob remote gearbeitet! 

Aufbau des Workshops 

Wie in dem Blogbeitrag über Remote Mob Testing beschrieben ist es sinnvoller, kleine Gruppen aus vier bis sechs Personen zu betreuen. Durch das verteilte Arbeiten kann es sonst häufiger zu Verzögerungen durch technische Probleme (z. B. schlechte Verbindung, schlechte Tonqualität) kommen, was die Einsatzzeit des jeweils aktuellen Navigators verkürzen könnte. Auch kann man als Facilitator bei kleineren Gruppen besser den Überblick behalten und die Teilnehmenden können die Rolle des Navigators oder Drivers häufiger einnehmen.
(Zur Erläuterung der verschiedenen Rollen siehe ebenfalls Blogbeitrag Remote Mob Testing

Unser Setting sah wie folgt aus:

  • Microsoft Teams als Kommunikationsplattform  
  • Leicht verständliches Testobjekt (Website)
  • 3 Facilitators  
  • 39 Teilnehmerinnen und Teilnehmer aus den Bereichen QA, Entwicklung und Business Analyse 
  • Zeitlicher Rahmen: 1,5 h  

Weil wir allen die Möglichkeit geben wollten, das Mob Testing selbst an einem praktischen Beispiel kennenzulernen, haben wir im Vorfeld des Workshops keine Teilnehmerbegrenzung festgelegt. Schlussendlich hatten alle drei Facilitators über 12 Teilnehmerinnen und Teilnehmer. 

Person am Schreibtisch, in einer Videokonferenz mit vielen weiteren Personen
Beim Remote Mob Testing nehmen alle Teilnehmenden einmal die aktive Rolle des Navigators ein.

Als Testobjekt haben wir eine einfache Website gewählt, damit sich alle auf das Vermitteln bzw. Kennenlernen der Testmethode konzentrieren konnten und sich nicht noch zusätzlich Wissen über die Anwendung aneignen mussten. 

Feedback 

Schon während der Durchführung des Workshops fiel uns auf, dass die Wartezeiten, bis eine aktive Rolle (Driver oder Navigator) eingenommen wird, als unangenehm empfunden werden könnte.   

Dies wurde auch in der Feedbackrunde angesprochen. Daher empfehlen wir, dass der Mob bei Testideen mit unterstützt. Es bedeutet nämlich nicht, dass man sich als Mob-Mitglied zurücklehnen und mit den Gedanken abschweifen kann. Das vermeidet auch doppeltes Testen oder sich die Blöße geben zu müssen, nicht aufgepasst zu haben.  

Teilweise fiel es einigen Teilnehmenden am Anfang schwer, sich bei der Rolle des Drivers darauf zu konzentrieren, nur die Anweisungen des Navigators auszuführen und eigene Testideen zurückzuhalten. Durch entsprechende Hinweise des Facilitators gewöhnten sich die Teilnehmerinnen und Teilnehmer jedoch nach einiger Zeit daran. Dieser Aspekt des Mob Testings wurde als sehr positiv empfunden, weil alle in der Rolle des Navigators zu Wort kommen und eigene Testideen einbringen können.  

Dennoch kam die Frage auf, warum die Rollen Navigator und Driver nicht zusammengefasst werden können. Dazu lässt sich Folgendes sagen: Es fördert den Lernprozess, wenn ein Mitglied Schritt für Schritt artikuliert, was es vorhat. So bindet man mehr Teilnehmerinnen und Teilnehmer in diese aktive Rolle ein. Durch das Mitteilen des Ziels wird es den Teilnehmenden erleichtert, den Ideen des Navigators zu folgen. Andernfalls kann es passieren, dass einige Schritte zu schnell und mit zu wenig Erklärung durchgeführt werden. Dadurch ginge die Nachvollziehbarkeit verloren und dem Mob würde es erschwert, sich aktiv einzubringen.  

Weiteres positives Feedback gab es zur Rolleneinteilung und dem gesamten Vorgehen. Den Aussagen der Befragten nach erhalte man die Testansätze zum Teil durch den Weg, den der vorherige Navigator eingeschlagen hat und betrachtet daher das Testobjekt aus den unterschiedlichsten Blickwinkeln. Aus diesem Grund ist es immer sinnvoll – je nach Zweck der Mob Testing Session – Teilnehmerinnen und Teilnehmer mit unterschiedlichen Kenntnissen einzuladen. Das erhöht den Lernprozess und die Anzahl der Testfälle. Das einfache Beispiel-Testobjekt habe zudem geholfen, sich auf die Methode zu konzentrieren und sie zu verinnerlichen. 

Sehr positiv hervorgehoben wurde das kollaborative Arbeiten beim Mob Testing. Dadurch wird der agile Gedanke gelebt.  

Lösungsansatz für eine große Teilnehmerzahl 

Das Problem einer zu hohen Teilnehmeranzahl könnte man lösen, indem man im Vorfeld die Gruppengröße begrenzt oder aber eine neue Rolle einführt: die Rolle des Zuschauers. Dabei würde dann eine Trennung zwischen aktiven Teilnehmern und Teilnehmerinnen einerseits sowie Zuschauerinnen und Zuschauern andererseits vollzogen. Die Teilnehmenden würden das oben beschriebene Vorgehen durchführen und sich an die Rollenverteilung (Navigator, Driver, Mob) sowie den Rollenwechsel halten. Die Zuschauerinnen und Zuschauer würden nur beobachten und nicht teilnehmen. Auch Kommentare von ihnen wären nicht erlaubt, weil das bei einer hohen Zuschauerzahl die aktiven Teilnehmenden stören könnte. 

Fazit 

Alles in Allem ist der Workshop auf der Campus-Veranstaltung sehr gut angekommen und hat gezeigt, dass es sehr gut möglich ist, Mob Testing auch remote und somit für das verteilte Arbeiten anzuwenden. Durch diese Möglichkeit kann das Zusammenarbeiten beibehalten werden, auch wenn es nicht immer möglich ist, sich vor Ort zu treffen.  

Dieser Beitrag wurde verfasst von:

Maria Petzold

Maria Petzold arbeitet seit 2010 bei der ZEISS Digital Innovation. Als Testmanagerin liegt ihr Fokus auf der Qualitätssicherung von Software. Vor allem in medizinischen Softwareprojekten konnte sie ihre Test-Erfahrungen sammeln.

Alle Beiträge des Autors anzeigen

MAUI – Mehr als eine Insel

Im Mai 2020 hat Microsoft eine neue, vereinheitlichte UI-Plattform für alle Systeme unter dem klangvollen Namen MAUI bzw. in voller Länge „.NET Multi-Platform App UI“ vorgestellt, die voraussichtlich im November 2021 auf den Markt kommen wird. Doch was genau verbirgt sich hinter diesem Namen? Diese Frage soll in diesem Artikel beantwortet werden. Dabei sollen die Plattform vorgestellt, die technischen Hintergründe erläutert und vor allem auch das Potential der Technologie herausgestellt werden.  

Um MAUI als Plattform zu verstehen, muss man zunächst Xamarin kennen. Xamarin (und im speziellen Xamarin.Forms) ist eine Plattform zur Entwicklung von nativen Apps für iOS, Android und UWP mit C#, XAML und .NET. Man kann dabei aus einer Code-Basis für alle unterstützten Betriebssysteme eine App erzeugen. Somit ist der Aufwand für die Entwicklung für verschiedene Betriebssysteme im Vergleich zur nativen Codierung der Anwendungen deutlich geringer. Aktuell sind tatsächlich die diversen SPA-Frameworks für den Browser die einzige Technologie, welche vergleichbare Portabilität bei ähnlichem Gesamtaufwand bieten. 

Schematische Darstellung von MAUI
Abbildung 1: Mit Xamarin.Forms kann man aus einer Code-Basis native Apps für alle unterstützten Betriebssysteme erzeugen. MAUI wird der direkte Nachfolger von Xamarin.Forms.

Aber was ist nun dieses geheimnisvolle MAUI und was hat es mit Xamarin zu tun? Diese Frage lässt sich recht simpel beantworten: MAUI ist das neue Xamarin oder, präziser ausgedrückt, dessen direkter Nachfolger, der erstmals mit .NET 6 ausgeliefert wird. 

Genau wie mit Xamarin.Forms lassen sich mit MAUI aus einem Projekt und mit derselben Code-Basis Apps für alle unterstützten Betriebssysteme erstellen. Aus dem Code werden Installationspakete generiert, die dann auf den verschiedenen Plattformen installiert werden können. Offiziell werden von Microsoft Android ab Version 10, iOS, macOS und natürlich Windows, sowohl nativ als auch als UWP-App, unterstützt. Zudem soll es eine Community-Implementierung für Linux-Betriebssysteme sowie eine von Samsung zur Verfügung gestellte Implementierung für deren Tizen-Plattform geben.  Das Projekt- und Build-System wird für alle Plattformen vereinheitlicht und das Erzeugen der Apps wird sowohl über Visual Studio als auch das .NET CLI möglich sein. 

Ein weiteres Feature wird die geteilte Nutzung von Ressourcen wie Bildern oder Übersetzungen sein. Diese sollen von MAUI automatisch in die entsprechenden nativen Formate umgewandelt und in die erstellten Pakete integriert werden können. Außerdem wird man jederzeit auf die APIs des jeweiligen Betriebssystems zugreifen können. Hierzu soll es im Projekt einen speziellen Ordner geben, unter dem man die nativen Code-Hooks ablegt und die dann beim Kompilieren automatisch ins Paket integriert werden.  

Alle im .NET-Standard verfügbaren Funktionalitäten wie zum Beispiel Dependency Injection sollen dabei auch für eine MAUI-App genutzt werden können. Durch die Verwendung von C# und XAML wird auch die Nutzung entsprechender Entwurfsmuster wie dem viel genutzten MVVM-Pattern möglich sein. Neu ist zudem der Support für das Model-View-Update-Pattern, ein von Elm entliehenes Muster, mit dem man einen unidirektionalen Datenfluss analog zu Redux abbilden können soll. Auch Microsofts webbasierte Client-Technologie Blazor soll unterstützt werden. 

Leider wird MAUI erst mit der Einführung von .NET 6 im November 2021 offiziell zur Verfügung stehen. Zudem wurden Teile des Frameworks nach .NET 7 und damit ins Jahr 2022 verschoben. Hier sind vor allem der offizielle Support für Blazor und das MVU-Pattern zu nennen. Da MAUI der offizielle Nachfolger von Xamarin ist, wird dieses auch mit dem Release von .NET 6 noch für ein Jahr unterstützt und dann eingestellt.  

Damit scheint Microsofts Strategie für die Zukunft der UI-Entwicklung mit dem .Net-Framework klar zu sein: MAUI ist der neue „First Class Citizen“, wenn es um die Erstellung von nativen User Interfaces geht.  

Das klingt zunächst alles danach, dass Microsoft eine native Cross Platform-Unterstützung ins .NET-Framework integrieren möchte. Dieser Plan wird jedoch nur aufgehen, wenn MAUI von den Entwicklerinnen und Entwicklern akzeptiert wird. Durch das sehr späte Release und einer Menge hervorragender Open Source-Alternativen wie Uno könnte sich MAUI am Ende eventuell nicht etablieren. Aktuell kann man deshalb nur abwarten und sehen, was die Zukunft bringt. Die Migration von bestehenden WPF-Anwendungen gestaltet sich beispielsweise schwierig, da der ersehnte XAML-Standard, der eine Vereinheitlichung der Elemente und Tags für alle XAML-Dialekte definieren sollte, scheinbar wieder verworfen wurde und sich MAUI und WPF wegen unterschiedlicher Elemente nicht übergangslos austauschen lassen werden.

Wenn die Technologie aber tatsächlich hält, was sie verspricht, die Entwicklerinnen und Entwickler sie breitflächig einsetzen und MAUI so hochklassig wird, wie Microsoft es in Aussicht stellt, könnte hier eine Cross Platform-Revolution unter .NET in den Startlöchern stehen.

Exploratives Testen (Teil 1) – Der Test für Faule oder die Krone der Testschöpfung?

Wir sind Testerinnen und lieben Testmethoden, bei denen die menschlichen Stärken, die Kreativität und die Kollaboration im Vordergrund stehen. Dabei wollen wir aber keinesfalls ohne Regeln bzw. Rahmenbedingungen agieren oder für eine Dokumentationsabstinenz plädieren.

Uns sind bereits einige Vorurteile über das explorative Testen begegnet, daher haben wir uns die Frage gestellt, wie diese zustande kommen. In diesem Artikel gehen wir speziell auf diese Vorurteile ein mit dem Ziel, diese aus dem Weg zu räumen. Doch zuerst betrachten wir das explorative Testen näher.

Was ist exploratives Testen?

Exploratives Testen kann viel mehr als Testfälle stumpf abzuarbeiten: Es fördert das kreative Denken während einer Testsession, sprengt Grenzen auf und befriedigt die Neugier. Gleichzeitig verlangt es Disziplin in Bezug auf das Erreichen des gesetzten Ziels während einer Testdurchführung und im Hinblick auf die Dokumentation.

Das ISTQB (unabhängiges Expertengremium für standardisierte Ausbildung von Softwaretestern) bezeichnet das explorative Testen in seinem Glossar als einen „Testansatz, bei dem die Tests dynamisch entworfen und ausgeführt werden, basierend auf dem Wissen, der Erkundung des Testelements und den Ergebnissen früherer Tests“ (Archivversion 3.4 des ISTQB® GTB Glossary). Anders als beim „klassischen“ Testen mit vorbereiteten Testfällen handelt es sich somit um einen intuitiven Testansatz, bei dem jede vorherige Idee zur nächsten führt. Dabei gibt es verschiedene Hilfsmittel, um einen neuen Ansatzpunkt zu erhalten, Ideen zu entwickeln und mehr Struktur zu schaffen. Dazu zählen bspw. die Test-Charter, die Heuristiken, das Test-Orakel oder auch die Test-Touren.

Hand, die eine Glühbirne hält, aus der untereinander vernetzte Punkte aufsteigen
Abbildung: Exploratives Testen – ein intuitiver Testansatz

Zusammenfassend lässt sich sagen, dass die Testerin oder der Tester die Tests durch Erfahrung, Intuition, Neugier und strukturiertes Vorgehen erstellt. Trotzdem ist diese Art des Testens nicht nur für erfahrene Testerinnen und Tester sondern auch für Projektneulinge gut geeignet, die das Testobjekt und/oder das Vorgehen erst kennenlernen. Um die Person dabei an die Hand zu nehmen, sollte ein Ziel definiert oder ein Überblick des Objekts geschaffen werden. Dies kann entweder im direkten Austausch per Pair Testing erfolgen oder in Gruppen über den Ansatz des Mob Testings.

Vorurteile gegenüber dem explorativen Testen

Aber welche Vorurteile gibt es nun, gegen die sich das explorative Testen behaupten muss?

Exploratives Testen ist oft als strukturlos, nicht nachvollziehbar, nicht quantifizierbar – schlicht und einfach als „Ich-klick-da-mal-so-rum-und-schau-was-passiert“ – verschrien. Die Aussage obsoleter Dokumentation erschwert es zusätzlich, das explorative Testen in Kundenprojekten in Rechnung zu stellen. Das ist ein nachvollziehbarer Grund, wenn davon ausgegangen wird, dass exploratives Testen dokumentationsfrei und somit die Testleistung nicht nachvollziehbar ist. Schnell kommen somit auch die Aussage sowie das Totschlagargument zustande, dass für diesen Test keine Zeit zur Verfügung steht.

Im Folgenden werden wir die eben dargelegten Behauptungen sowie weitere negative Aussagen genauer betrachten und dazu Stellung nehmen.

Vorurteil Nummer 1: “Exploratives Testen ist nicht nachvollziehbar.” / “Explorativ getestete Inhalte kann ich nicht erneut testen.” / “Wie soll ich da einen Bug-Retest machen?” Diese Aussagen haben uns neugierig gemacht. Auf die Frage, was unter explorativem Testen verstanden wird, erhielten wir oft die Antwort, dass es für das spontane Herumklicken im Testobjekt genutzt wird. Es soll das Kennenlernen des Testobjekts fördern oder weitere Varianten testen, die bei der vorherigen Testfallerstellung nicht beachtet wurden. Und somit wird es nebenbei ungeplant sowie unstrukturiert durchgeführt. Eine Dokumentation wurde nicht erwähnt. Somit erklärt sich, wie das Vorurteil zustande kommt, diese Testmethode sei nicht nachvollziehbar und Tests sowie Bugfixes könnten schwer nachgetestet werden. Es ist absolut richtig, dass explorative Tests die spontane Testdurchführung unterstützen. Jedoch muss darauf geachtet werden, die Testschritte, erwartete Ergebnisse oder auch Abweichungen stets zu dokumentieren und die Tests somit nachvollziehbar zu machen. Wie die Dokumentationstiefe dabei aussieht, muss durch die Rahmenbedingungen eines jeden Projekts einzeln definiert werden. Wir halten fest: Exploratives Testen bedeutet nicht, nicht zu dokumentieren.

Um dem ersten Vorurteil entgegenzuwirken und Struktur in das explorative Testen zu bringen, kann man das Session-basierte Testmanagement nutzen. Während einer Session wird das Testobjekt mit größtmöglicher Flexibilität und Freiheit geprüft. Dabei werden die Rahmenbedingungen mithilfe eines definierten Standpunktes und einer Charta (Ziel und Ablauf für eine Session) gesetzt. Dies beinhaltet neben einigen anderen Komponenten auch eine Dokumentation durch Session-Reports, in denen die durchgeführten Testschritte, Abweichungen, das Testobjekt und weitere Informationen festgehalten werden können. Diese können je nach testender Person oder Projekt angepasst werden. Im Internet lassen sich verschiedene Beispiele für Session-Reports finden.

An dieser Stelle sei ein spezieller Anwendungsfall erwähnt: Exploratives Testen im regulierten Umfeld. Hier nimmt die Dokumentation eine zentrale Rolle ein. Das Vorurteil hier: Exploratives Testen ist im regulierten Umfeld unmöglich oder nur sehr schwer umsetzbar, aufgrund der regulatorischen Vorgaben. Wie man es trotz Vorgaben umsetzen kann, werden wir in einem weiterführenden Artikel aufzeigen.

Vorurteil Nummer 2: “Für exploratives Testen haben wir keine Zeit” / “Erst einmal müssen wir unsere vorbereiteten Testfälle abarbeiten, bevor wir explorativ testen können.“

Um die explorative Testmethode aktiv im Projekt umzusetzen, muss man bereit sein, das bisherige Testvorgehen anzupassen und den Vorteil der Zeitersparnis zu erkennen.  Denn sieht man den explorativen Test nur als Ergänzung, ohne eine Angleichung der bisher genutzten Teststruktur, wird dieses Vorgehen nur als Belastung oder als ein lästiger Zusatz gesehen, wodurch das Keine-Zeit-Vorurteil zustande kommt. Erfolgt allerdings, wie beim explorativen Testen vorgesehen, die Testfallerstellung gleichzeitig zur Testfalldurchführung, entfällt die nun überflüssige Erstellung von Testfällen, die zum Schluss nicht durchgeführt werden und somit Zeitverschwender darstellen. Die gewonnene Zeit ermöglicht es der Testerin oder dem Tester, neue Testideen zu entwickeln und somit das Testobjekt intensiver zu testen. So können z. B. Debriefings durchgeführt werden, um die gewonnenen Erkenntnisse mit den anderen Teammitgliedern zu teilen. Auch kann diese Zeitersparnis genutzt werden, um die Testautomatisierungsquote zu erhöhen.

Vorurteil Nummer 3: “Dokumentation schön und gut, aber exploratives Testen ist nicht ausreichend.”:
Diese Aussage ist richtig, wenn damit gemeint ist, dass exploratives Testen für sich stehend als vollumfänglicher Testansatz gelten soll. Neben den explorativen Tests sollten mindestens automatisierte Tests definiert und ausgeführt werden (z. B. Unit-, Integrations- und GUI Tests). Wenn das Projekt einen hohen Automatisierungsgrad besitzt, ist es sogar durchaus möglich, das manuelle Testen rein explorativ durchzuführen. Alternativ kann die explorative Methode als Ergänzung zum klassischen Testansatz genutzt werden, um das manuelle Testen leichter, flexibler und freier gestalten zu können. Wie viel exploratives Testen in einem Projekt angewendet wird, muss stets sorgfältig geprüft werden. Nicht immer ist es möglich, alle manuellen Tests explorativ zu gestalten. Das kann an unterschiedlichen Faktoren liegen z. B. an einem zu niedrigen Automatisierungsgrad oder auch an fehlenden Skills der Testerinnen und Tester. Sie sollten aus unserer Sicht Skills wie Kreativität, Gewissenhaftigkeit, Eigeninitiative, Eigenverantwortung, Verständnis von Qualität sowie soziale Aspekte wie Kommunikations- und Teamfähigkeit mitbringen.

Fazit

In diesem Beitrag haben wir die Vorteile des explorativen Testens aufgezeigt und Argumente zusammengetragen, welche die vorhandenen Vorurteile demgegenüber entkräften sollen. Aus unserer Sicht lohnt es sich, das bisherige Testvorgehen zu überdenken und sich auf das explorative Testen einzulassen, um dessen positive Effekte zu nutzen. Natürlich sollte jedes Projekt für sich betrachtet und die Möglichkeiten zum explorativen Testen ermittelt werden – ohne dieses jedoch von Vornherein komplett auszuschließen. Negative Aussagen oder Erfahrungen sollten ggf. hinterfragt werden. Bei der Wahl der Methode und ihrer Ausführung sind immer auch die Skills der Testerinnen und Tester zu betrachten. An dieser Stelle möchten wir noch einmal besonders darauf hinweisen, dass die Dokumentation auch beim explorativen Testen essenziell ist.

Dieser Beitrag wurde verfasst von:

Katharina Warak

Katharina Warak arbeitet als Senior Softwaretest Engineer bei der ZEISS Digital Innovation. Sie ist für die Automatisierung auf API- und GUI-Ebene zuständig. Außerdem hilft sie den Kunden in den Projekten mehr über die kollaborativen Testmethoden zu erfahren und diese Methoden zur Verbesserung ihrer Arbeitsweise einzusetzen. Dieses Wissen teilt sie auch gerne auf internationalen und nationalen Konferenzen.

Alle Beiträge des Autors anzeigen

Weihnachtsfeier in Zeiten von Corona: Der 2. ZEISS Digital Innovation Online-Campus

Bereits im Frühjahr beginnen wir als ZEISS Digital Innovation (ZDI) mit der Planung unserer Weihnachtsfeier, die in der Regel am Freitag vor dem ersten Adventswochenende stattfindet. 2020 stand für uns schnell fest, dass eine Weihnachtsfeier, zu der sonst alle Beschäftigten in Dresden zusammenkommen und gemeinsam feiern, dieses Mal leider nicht möglich sein würde. Die Weihnachtsfeier ausfallen zu lassen, kam für uns jedoch ebenfalls nicht in Frage, sodass wir uns auf die Suche nach einem neuen Format begaben.

Hierbei knüpften wir an unseren 1. ZDI Online-Campus an, zu welchem wir bereits viele positive Rückmeldungen erhalten hatten. Besonders wichtig war uns dieses Mal neben der internen Wissensvermittlung, dass die weihnachtliche Stimmung und das „Wir-Gefühl“ nicht zu kurz kommen sollten. Aber wie bringen wir diese zu unseren Kolleginnen und Kollegen nach Hause?

Eine Karte mit allen Standorten, von denen aus unsere Kolleginnen und Kollegen dem Online-Campus zugeschaltet waren.
Abbildung 1: Unsere Kolleginnen und Kollegen nahmen von den unterschiedlichsten Standorten am Online-Campus teil.

Eine kleine Weihnachtsüberraschung für alle Mitarbeitenden

Um verteilt weihnachtliche Stimmung aufkommen zu lassen, haben alle Beschäftigten jeweils ein kleines Weihnachtspäckchen per Post erhalten, mit der Bitte, dieses erst am Tag unseres gemeinsamen Online-Campus zu öffnen.

Die 345 Päckchen wurden von dem Organisationsteam bei stimmungsvoller Weihnachtsmusik eigenhändig gemeinschaftlich verpackt. Wir waren glücklich, dass die Weihnachtsüberraschung bei all unseren Kolleginnen und Kollegen aus Deutschland und Ungarn rechtzeitig zu Hause ankam.

Somit waren alle pünktlich zu unserem 2. ZDI Online-Campus mit weihnachtlichen Leckereien, Glühweingewürz für einen leckeren Punsch, Adventskalender, Weihnachtsmütze und anderen kleinen Aufmerksamkeiten ausgestattet und es konnte losgehen.

2. Online-Campus mit Online-Teamspiel und weihnachtlicher Feierstunde

Mit den positiven Erfahrungen aus unserem 1. ZDI Online-Campus wurden auch dieses Mal verschiedene Vortragsslots von und für unsere Kolleginnen und Kollegen über den gesamten Tag hinweg angeboten. Die technische Umsetzung erfolgte erneut mit Microsoft Teams. Es wurden insgesamt 29 Vorträge zu unterschiedlichen Themen, wie bspw. Java, .NET, Cloud, Usability, Agile und Web, gehalten. Als Lessons Learned passten wir die Dauer der Vortragsslots (45 Minuten statt 30 Minuten) und Wechselpausen (15 Minuten statt 10 Minuten) an. So hatten alle Teilnehmenden zwischen den Slots Zeit durchzuatmen und sich gedanklich auf den nächsten Slot vorzubereiten.

Zeitplan aller Vorträge des 2. ZEISS Digital Innovation Online Campus.
Abbildung 2: Insgesamt 29 Vorträge in 6 parallel laufenden Slots wurden zu fachverwandten Themen gehalten.

Nach einer längeren Mittagspause fand unser Online-Teamspiel mit insgesamt 28 Teams bestehend aus jeweils neun bis zehn Teammitgliedern statt. Bei der Teamzusammenstellung wurde auch dieses Mal auf eine möglichst heterogene Gruppenaufteilung hinsichtlich der Reifegrade, Standorte und Geschäftsbereiche geachtet. Zum gegenseitigen Kennenlernen und der anschließenden Lösung der Aufgaben hatte jedes Team einen eigenen virtuellen Teamraum zur Verfügung. Die Aufgaben beinhalteten unter anderem Wissensfragen, Scharade- und Rätselaufgaben sowie die Abstimmung eines weihnachtlichen Teambeitrags, welcher zum Abschluss unseres Online-Campus im Rahmen der gemeinsamen weihnachtlichen Feierstunde von den Teams live aufgeführt werden sollte.

Zum Auftakt der weihnachtlichen Feierstunde wurde zunächst mit leckerem Glühwein und Punsch gemeinsam angestoßen. Im Anschluss waren die jeweiligen Teams mit ihren weihnachtlichen Live-Beiträgen an der Reihe. Von musikalischen und poetischen Darbietungen bis hin zu Weihnachtsgrüßen in verschiedenen Sprachen und weihnachtlichen Filmtipps waren alle Teams kreativ dabei und auch der verfügbare Teams-Chat wurde rege genutzt. Somit kam trotz der Verteilung und damit einhergehenden physischen Distanz eine besinnliche und weihnachtliche Stimmung unter uns auf.

Fazit

Uns allen war bewusst, dass ein Online-Format mit einer virtuellen weihnachtlichen Feierstunde nicht das Gefühl einer real stattfindenden Weihnachtsfeier vor Ort mit Schlittschuhfahren, leckerem Essen, Tanzen und dem lockeren Austausch untereinander ersetzen kann. Dennoch waren wir überrascht, wie viel weihnachtliche Stimmung und Gemeinschaftsgefühl verteilt online entstehen kann. Es hat uns erneut gezeigt, dass wir als starkes Team an einem Strang ziehen und trotz Corona dazu bereit sind, das Beste aus der aktuellen Situation zu machen und gemeinsam neue Wege zu gehen.

Ob wir in diesem Jahr vielleicht wieder mit allen Kolleginnen und Kollegen gemeinsam in Dresden vor Ort ein Sommerfest oder auch eine Weihnachtsfeier veranstalten können, ist ungewiss. Was wir jedoch bereits jetzt prognostizieren können: Unser neues Format des Online-Campus mit dem Online-Teamspiel wird Corona sicherlich überdauern.

Datenbankänderungen erkennen und streamen mit Debezium und Apache Kafka (Teil 1) – Die Theorie

In nahezu jedem Unternehmensumfeld spielen Datenbanken, insbesondere relationale Datenbanken, eine große Rolle. In ihnen werden sowohl die Stammdaten von Mitarbeitern, Kunden etc. als auch die sich ständig ändernden Bewegungsdaten des Unternehmens verwaltet. Aus den verschiedensten Gründen können sich nun für betreffende Firmen neue Anforderungen ergeben, so dass die Änderungen der Bewegungsdaten in Echtzeit in anderen Prozessen und Applikationen weiterverarbeitet werden müssen – wie bspw. bei Buchungen in einem System. Dafür reicht es nicht mehr aus, dass eine Anwendung durch Polling regelmäßig eine Quelltabelle abfragt. Vielmehr wird es notwendig, dass die Anwendung über Change-Events in einer Tabelle benachrichtigt wird und die Informationen sofort aufbereitet zur Verfügung gestellt bekommt.

Und genau das ist mit dem Einsatz von Debezium und der Streaming-Plattform Apache Kafka möglich.

Datenänderungen erkennen

Zu Beginn sollte einmal kurz geklärt werden, welche grundlegenden Möglichkeiten es zum Erfassen von Datenänderungen in relationalen Datenbanken gibt. In der Fachliteratur finden sich dazu unter dem Begriff Change Data Capture vier Methoden, die im folgenden Abschnitt vorgestellt werden.

Die einfachste Möglichkeit zur Erkennung von Änderungen ist der zeilen- und spaltenweise Vergleich einer Datenbanktabelle mit einer älteren Version von ihr. Es ist offensichtlich, dass dieser Algorithmus gerade für größere Tabellen nicht besonders effizient, dafür aber leicht zu implementieren ist. Eine weitere Idee ist es, nur die geänderten Datensätze anhand einer geschickten SQL-Abfrage auszuwählen. Dafür kommen in der Praxis meist Zeitstempel zur Anwendung, die für jeden Datensatz angeben, wann dieser zuletzt geändert wurde. Sucht man nun nach Datenänderungen in der Tabelle, selektiert man alle Datensätze, deren Zeitstempel jünger sind als der Zeitpunkt des letzten Vergleichs. Ein minimales SQL-Statement könnte so aussehen:

SELECT * FROM [source_table] 
WHERE last_updated < [datetime_of_last_comparison]

Problematisch an diesem Ansatz ist, dass eine solche Zeitstempel-Spalte in der Datenbanktabelle schon vorhanden sein muss, was wohl nicht immer der Fall sein wird. Es werden also Anforderungen an das Datenschema gestellt. Weiterhin lassen sich mit dieser Methode gelöschte Datensätze nur sehr schwer erkennen, da ihre Zeitstempel ebenfalls gelöscht werden.

Eine dritte Möglichkeit, die dieses Problem lösen kann, ist die Implementierung eines Datenbanktriggers, der nach jedem INSERT, UPDATE und DELETE auf der Tabelle ausgelöst wird. Weiterhin wäre es auch denkbar, Änderungen am Datenbankschema mithilfe eines Triggers zu überwachen. Ein solcher Trigger könnte dann die erfassten Datenänderungen in eine extra dafür vorgesehene Tabelle schreiben.

Zu guter Letzt gibt es noch eine vierte Methode, das sogenannte Log-Scanning, welches in diesem Blogpost eine besonders wichtige Rolle spielt. Die meisten Datenbankmanagementsysteme (DBMS) führen ein Transaktionslog, das alle Änderungen, die an der Datenbank vollzogen wurden, aufzeichnet. Dies dient vor allem dazu, die Datenbank nach einem Ausfall, sei es bedingt durch einen Stromausfall oder einen Schaden am physischen Datenträger, wieder zurück in einen konsistenten Zustand zu bringen. Für das Change Data Capture wird nun das Transaktionslog etwas zweckentfremdet: Durch das Auslesen der Logdatei ist es möglich, Änderungen an den Datensätzen einer bestimmten Quelltabelle zu erkennen und diese dann weiter zu verarbeiten.

Der große Vorteil des Log-Scannings ist es, dass durch das Auslesen des Transaktionslogs kein Overhead auf der Datenbank erzeugt wird, wie es beim Stellen von Abfragen oder beim Ausführen eines Triggers nach jeder Änderung der Fall ist. Damit wird die Performance der Datenbank nicht weiter beeinträchtigt. Weiterhin können alle Arten von Datenänderungen erfasst werden: das Einfügen, Aktualisieren und Löschen von Datensätzen und auch das Ändern des Datenschemas. Problematisch ist aber, dass der Aufbau solcher Transaktionslogs nicht genormt ist. Die Logdateien der einzelnen Datenbankhersteller sehen vollkommen unterschiedlich aus und unterscheiden sich teilweise auch zwischen den einzelnen Versionen ein und desselben Datenbankmanagementsystems. Möchte man also Datenänderungen aus mehreren Datenbanken unterschiedlicher Hersteller erfassen, müssen mehrere Algorithmen implementiert werden.

Und genau an dieser Stelle kommt Debezium ins Spiel.

Was ist Debezium?

Debezium ist an sich keine eigenständige Software, sondern eine Plattform für Konnektoren, die das Change Data Capture für verschiedene Datenbanken implementieren. Dabei nutzen die Konnektoren das Log-Scanning, um die Datenänderungen zu erfassen und sie an die Streaming-Plattform Apache Kafka weiterzureichen.

Jeder Konnektor repräsentiert eine Software für ein bestimmtes DBMS, welche in der Lage ist, eine Verbindung zur Datenbank aufzubauen und dort die Änderungsdaten auszulesen. Die genaue Funktionsweise eines Konnektors unterscheidet sich zwischen den DBMS, im Allgemeinen wird aber zunächst ein Snapshot vom bestehenden Datenbestand erzeugt und danach auf Änderungen im Transaktionslog gewartet. Bisher hat Debezium Konnektoren für die Datenbanken MongoDB, MySQL, PostgreSQL und SQL Server entwickelt. Weitere für Oracle, Db2 und Cassandra befinden sich aktuell noch in der Entwicklungsphase, sind aber schon verfügbar.

Die gängigste Variante, Debezium einzusetzen, ist in Kombination mit Apache Kafka und dem dazugehörigen Framework Kafka Connect. Apache Kafka ist eine quelloffene, verteilte Streaming-Plattform, die auf dem Publisher-Subscriber-Modell basiert. Es bietet Applikationen die Möglichkeit, Datenströme in Echtzeit zu verarbeiten und Nachrichten zwischen mehreren Prozessen auszutauschen. Mit Kafka Connect lassen sich passend dazu Konnektoren implementieren, die sowohl Daten nach Kafka schreiben (Quell-Konnektoren) als auch aus Kafka lesen können (Sink-Konnektoren). Debezium stellt, wie man schon vermutet, solche Quell-Konnektoren zur Verfügung, die die ermittelten Change-Events an Kafka streamen. Applikationen, die nun an den Änderungsdaten interessiert sind, können diese aus den Logs von Apache Kafka lesen und verarbeiten.

Architektur-Skizze für den Einsatz von Debezium
Abbildung: Architektur-Skizze für den Einsatz von Debezium 1

Seit kurzem gibt es auch die Möglichkeit, Debezium nicht mehr in Verbindung mit Kafka Connect, sondern auch als Standalone-Applikation zu verwenden. Damit sind Anwender nicht mehr an Kafka gebunden und können Change-Events auch an weitere Streaming-Plattformen, wie z. B. Kinesis, Google Cloud Pub/Sub oder Azure Event Hubs weiterreichen. Die Änderungsdaten, die Debezium aus unterschiedlichen Datenbanken ermittelt, müssen für Applikationen, die diese weiterverarbeiten wollen (Consumer), in einem einheitlichen Format zur Verfügung gestellt werden. Das realisiert Debezium über das JSON-Format. Ein Change-Event hat dabei immer zwei Teile: einen Payload-Teil und ein vorangehendes Schema, das den Aufbau des Payloads beschreibt. Im Payload-Teil befinden sich u. a. die Operation, die auf dem Datensatz ausgeführt wurde bzw. auch die Inhalte der Zeile vor und nach der Änderung. Besonders gekennzeichnet wird auch der Primärschlüssel des Datensatzes, was wichtig für die Zuordnung des Events zu einer Partition in Kafka ist. Doch dazu mehr im zweiten Teil.

Weiterhin legt Debezium beim Change Data Capture einen großen Wert auf Fehlertoleranz: Sollte mal ein Konnektor abstürzen, wird nach seinem Neustart an der letzten verarbeiteten Position im Transaktionslog weitergelesen. Auch bei einer fehlenden Verbindung zu Apache Kafka werden die Änderungsdaten so lange zwischengespeichert, bis wieder eine Verbindung hergestellt werden konnte. Damit wären zunächst einmal die Grundlagen für die Nutzung von Debezium geklärt. Im nächsten Teil des Blogposts wird ein konkretes Beispiel vorgestellt, bei dem Debezium Change-Events aus einer Beispieltabelle im SQL Server streamt.


1 Die Skizze ist in ihrer Art an die Architektur-Skizze in der Dokumentation von Debezium angelehnt.

WCF-Alternativen (Teil 1) – Eine Einführung

Die Windows Communication Foundation (WCF) ist eine von Microsoft für das .NET Framework entwickelte Kommunikationsplattform zur Erstellung von verteilten Anwendungen. Sie wurde im Jahr 2006 mit dem .NET Framework 3.0 vorgestellt und löste damals .NET Remoting ab. Durch WCF werden die verschiedenen Aspekte einer verteilten Kommunikation logisch getrennt und dabei unterschiedliche Kommunikationstechnologien zu einer einheitlichen Programmierschnittstelle zusammengefasst. Dadurch ist es möglich, sich auf die Business-Logik zu fokussieren und sich nicht um die Anbindung der verschiedenen Kommunikationstechnologien kümmern zu müssen.

Der Aufbau von WCF

Mit der Veröffentlichung von WCF wurden die folgenden Kommunikationstechnologien vereinheitlicht.

Abbildung 1: Vereinheitlichung von Kommunikationstechnologien

Der Aufbau einer WCF Anwendung folgt dabei den 3-W‘s (Wo, Wie, Was).

Das erste W – „Wo“ beschreibt, unter welcher Adresse die Anwendung erreicht werden kann, um mit dieser zu kommunizieren, z. B.:

  • http://localhost:81/DataInputService
  • net.tcp://localhost:82/TcpDataInputService
  • net.pipe://localhost/PipeDataInputService
  • net.msmq://localhost/MsMqDataInputService

Das zweite W – „Wie“ beschreibt, mit welchen Protokollen und welchem Encoding, den sogenannten Bindings, die Kommunikation erfolgen soll. Diese werden in der *.config der Anwendung definiert und können somit jederzeit, ohne die Anwendung neu erstellen zu müssen, geändert werden. Von WCF werden neun verschiedene Bindings unterstützt:

  • Basic Binding (BasicHttpBinding)
  • TCP Binding (NetTcpBinding)
  • Peer Network Binding (NetPeerTcpBinding)
  • IPC Binding (NetNamedPipeBinding)
  • Web Service Binding (WSHttpBinding)
  • Federated WS Binding (WSFederationHttpBinding)
  • Duplex WS Binding (WSDualHttpBinding)
  • MSMQ Binding (NetMsmqBinding)
  • MSMQ Integration Binding (MsmqIntegrationBinding)  

Mit dem letzten W – „Was“ wird durch verschiedene Contracts definiert, welche Endpunkte und Datentypen bereitgestellt werden. Dabei werden durch ServiceContracts die Endpunkte und in DataContracts die Datentypen definiert.

Beispiel eines WCF ServiceContract und DataContract:

[ServiceContract]
public interface IDataInputService
{
    [OperationContract]
    int CreateUser(User user);
 
    [OperationContract]
    int Login(User user);
 
    [OperationContract]
    List<Time> GetTimes(int userId);
 
    [OperationContract]
    void AddTime(Time time, int userId);
 
    [OperationContract]
    List<string> Projects();
}
 
[DataContract]
public class User
{
    [DataMember]
    public string Name { get; set; }
 
    [DataMember]
    public string Passwort { get; set; }
}
 
[DataContract]
public class Time
{
    [DataMember]
    public DateTime Start { get; set; }
 
    [DataMember]
    public DateTime End { get; set; }
 
    [DataMember]
    public string Project { get; set; }
 
    [DataMember]
    public int uId { get; set; }
 
    [DataMember]
    public int Id { get; set; }
}

Die durch die Aufteilung geschaffene Flexibilität und auch Vielseitigkeit machen WCF sehr beliebt, wodurch die Plattform in vielen Projekten gern eingesetzt wird.

Warum wird eine Migration notwendig?

Schon bei der ersten Ankündigung von .NET Core im Jahr 2016 war WCF nicht mehr enthalten. Auch in den folgenden .NET Core Releases sowie dem zuletzt vorgestellten .NET 5.0 ist WCF kein Bestandteil mehr.

Für Microsoft wäre sicher das „W“ aus WCF, welches für Windows steht, die größte Herausforderung bei einer Portierung. Hier müsste, um .NET Core gerecht zu werden, eine plattformübergreifende Lösung gefunden werden. Ein Problem dabei sind die aktuell genutzten Windows-spezifischen Betriebssystembibliotheken für z. B. Socket Layer oder Kryptographie.

Auch wenn sich die Entwickler-Community eine Integration von WCF in .NET Core wünscht, so wird diese von Microsoft wohl in absehbarer Zeit nicht erfolgen.

Die Zukunft mit gRPC und Web API

Um ein Bestandsprojekt zukunftssicher zu machen oder generell die Vorteile von .NET Core zu nutzen, sollte eine Portierung auf .NET Core angestrebt werden. Besonders sinnvoll ist dies bei Projekten, welche sich in einer aktiven und kontinuierlichen Weiterentwicklung befinden. Kommt bei einem Projekt WCF zum Einsatz, stellt dies die Portierung vor eine zusätzliche Herausforderung. Hier muss zunächst eine Alternative gefunden und eine vorbereitende Umstellung von WCF erfolgen. Microsoft empfiehlt zur Ablösung von WCF hauptsächlich zwei Alternativen, gRPC und Web API.

In einer Blogpostreihe zu diesem Thema wollen wir die beiden Alternativen vorstellen und dabei auf die Besonderheiten und Herausforderungen der Migration eingehen. Weitere Artikel folgen in Kürze.