InspectorJ – Teil 2: MicroStream II

MicroStream: Konfiguration der Storage Engine

Sven Ruppert

© Shutterstock / Sira Anamwong (modifiziert)

Das Java-Universum bietet viele Bibliotheken, Tools und Frameworks. In seiner Serie „InspectorJ“ untersucht Java-Experte Sven Ruppert aktuelle Trends. Im zweiten Teil legt er sein Augenmerk nochmals eingehender auf das Projekt MicroStream. Das Framework verspricht, hochperformante Lösungen für die Serialisierung und Persistierung von Java-Anwendungen zu bieten.

Seit einiger Zeit gibt es einen neuen Mitbewerber im Bereich der Persistenz und Serialisierung. Die Rede ist vom Projekt MicroStream. Um was handelt es sich da genau? MicroStream nimmt in Anspruch, eine hochperformante und vor allem für den Entwickler sehr einfach zu handhabende Lösung für die Herausforderungen der Serialisierung und der Persistierung zu sein. In diesem Teil geht es um die Storage Engine und die Nutzung des Frameworks mit etwas vielfältigeren Elementen.

Was bisher geschah

Im vorherigen Teil haben wir uns angesehen, wie wir ein Projekt für die MicroStream Engine vorbereiten, damit wir mit dem Experimentieren beginnen können. Für die ersten praktischen Versuche wurde eine Klasse mit genau einem Attribut erzeugt und dann mehrfach instanziiert. Nachfolgend wurde sie auf Platte geschrieben (persistiert) und schließlich wieder zum Leben erweckt (geladen und instanziiert). Ebenfalls ausprobiert wurde die Kotlin-Variante dieser HelloWorld-Klasse.

Was heute ansteht

Bei unseren letzten Versuchen haben wir uns nicht darum gekümmert, wie man mit einer etwas umfangreicheren Anzahl an Elementen umgehen kann. Hier haben wir in Java die unterschiedlichsten Datenstrukturen, die uns schon vom JDK selbst mitgeliefert werden. Das werden wir uns heute ein wenig genauer ansehen. Aber bevor wir uns mit einer Vilezahl von Elementen beschäftigen, werden wir erst einmal eine Kleinigkeit, die wir das letzte Mal außen vor gelassen haben, lösen.

Die Rede ist hier davon, dass im vorherigen Teil der Serie die Storage Engine lediglich mit den Default-Werten gestartet worden ist. Dies führte dazu, dass alle Tests denselben Platz zum Speichern ihrer Elemente verwendet haben. Das ist natürlich nicht praktisch. Wenn man nun eine MicroStream Storage Engine startet, kann man diese natürlich auch konfigurieren. Für die jetzigen Beispiele wird für jeden Test ein eigenes Verzeichnis auf der Festplatte des Rechners im Verzeichnis target/storage angelegt. Da wir das Projekt mit Maven verwalten, wird demnach mit jedem Aufruf von mvn clean das Verzeichnis gelöscht. So stellen wir sicher, dass in mehr oder weniger geregelten Abständen der für die Tests in Anspruch genommene Platz auf dem Speichermedium wieder freigegeben wird.

Für die ersten Tests werden wir uns eine praktische Lösung ansehen, die nicht dafür gedacht is,t im großen Umfang verwendet zu werden. Da wir JUnit 5 in verbindung mit der standardmäßig beigefügten Test-Engine Jupiter einsetzen, können wir jeder Testmethode einen Parameter vom Typ TestInfo injizieren lassen. In diesem Holder befinden sich Informationen über den Test selbst. Unter anderem werden einem der Name der Testklasse und der Testmethode bereitgestellt. Damit haben wir alles zusammen, um für jeden Test ein eigense Verzeichnis zu erstellen, das wir allerdings später auch wieder jedem Test eindeutig zuordnen können. Fehlerbehandlungen ignoriere ich an dieser Stelle bewusst, um die Beispiele möglichst klein zu halten.

public static String TARGET_PATH = "./target/storage/";

public Function<TestInfo, File> infoToCleanFolder = (info) -> {
  final Class<?> aClass     = info.getTestClass().get();
  final Method   method     = info.getTestMethod().get();
  final File     tempFolder = new File(TARGET_PATH, 
                                       aClass.getSimpleName() + "_" + method.getName());
  if (tempFolder.exists()) {
    try {
      walk(tempFolder.toPath()).sorted(reverseOrder())
                               .map(Path::toFile)
                               .forEach(File::delete);
    } catch (IOException e) {
      logger().warning(e.getMessage());
    }
  }
  return tempFolder;
};

Es wird im Verzeichnis target/storage ein individuelles Verzeichnis für den jeweiligen Test erzeugt. Sollte von einem vorhergehenden Testdurchlauf schon ein Verzeichnis vorhanden sein, so wird dieses onne Rückfrage vollständig geleert. Damit kann nun die MicroStream Storage Engine mit den folgenden Zeilen Quelltext gestartet werden:

final File                   tempFolder      = infoToCleanFolder.apply(info);
final EmbeddedStorageManager storageManagerA = EmbeddedStorage.start(tempFolder);

Nachdem alle Tests aus dem vorherigen Teil dementsprechend umgebaut worden sind, sollten alle Tests in einer beliebigen Reihenfolge erfolgreich durchlaufen werden.

Kommen wir nun zu unserem ersten Versuch, eine Liste von Elementen zu speichern. Die Implementierung zu diesem Beispiel befindet sich in der Klasse ListOfElementsTest. Als Root Node kommt die Klasse mit dem Namen CollectionRootNode zum Einsatz. Diese hat wieder lediglich ein Attribut, diesmal vom Typ Collectiona<T>.

public class CollectionRootNode<T> {
  private final Collection<T> elements;

  CollectionRootNode(Collection<T> elements) {this.elements = elements;}

  public Collection<T> getElements() {
    return elements;
  }
}

Der erste Test besteht wiederum lediglich aus dem Schreiben und Lesen einer Liste von Elementen. Unter der Verwendung der Klasse IntStream werden eine Menge von 100_000 Elementen erzeugt, die nachfolgend der Instanz RootNode übergeben und gespeichert werden. Eine erste Messung ergab auf meinem nun doch schon einige Jahre altem MacBook eine Zeit von 215ms. Ich bitte an dieser Stelle jedoch ausdrücklich darum, diesem Wert keine besondere Aufmerksamkeit zu schenken, da es sich hierbei um kein Ergebnis handelt, das mit besonderer Sorgfalt ermittelt worden ist.

Kommen wir nun zu einer weiteren Datenstruktur, der HashMap. Hier wird der erzeugte Integer-Wert als Schlüssel zum Einsatz kommen und eine Instanz von HelloWorld den Wert selbst darstellen. Dabei gilt zu beachten, dass eine Implementierung von hascode und equals vorhanden ist.

public class HashMapRootNode {
  private final Map<Integer, HelloWorldImmutable> elements;
  public HashMapRootNode(Map<Integer, HelloWorldImmutable> elements) {
    this.elements = elements;
  }
  public Map<Integer, HelloWorldImmutable> getElements() {
    return elements;
  }
}
@Test
void test001(TestInfo info, TestReporter reporter) {
  final File                   tempFolder      = infoToCleanFolder().apply(info);
  final EmbeddedStorageManager storageManagerA = EmbeddedStorage.start(tempFolder);

  final Map<Integer, HelloWorldImmutable> elements 
    = IntStream.range(0, 100_000)
               .mapToObj(i -> Pair.next(i, new HelloWorldImmutable(i + "")))
               .collect(toMap(Pair::getT1, Pair::getT2));

  final Instant start = now();
  storageManagerA.setRoot(new HashMapRootNode(elements));
  storageManagerA.storeRoot();
  final Instant stop = now();
  reporter.publishEntry("duration store [ms] " + between(start, stop).toMillis());

  storageManagerA.shutdown();

  final EmbeddedStorageManager storageManagerB = EmbeddedStorage.start(tempFolder);
  final HashMapRootNode        rootAgain       = (HashMapRootNode) storageManagerB.root();

  Assertions.assertEquals(elements, rootAgain.getElements());

  storageManagerB.shutdown();
}

Auch in dem Fall sind die Daten in unter 250ms auf der Platte gespeichert.

Von Listen zu Bäumen

Bisher haben wir ausschließlich Listen oder einfache Schlüssel-/Wert-Paare gespeichert. Wie aber sieht es mit eigenen und von der Vermaschung her komplexeren Strukturen aus? Hierzu erzeugen wir uns als erstes ein Beispiel, das auch noch theoretischer Natur ist. Für diesen Versuch erstelle ich eine Klasse mit dem Namen BinaryTree und der einzige Wert, der pro Knoten in diesem Binärbaum gespeichert werden soll, ist vom Typ Integer. Die Knoten selbst sind durch eine Klasse mit dem Namen Node realisiert worden.

public class Node {
  public Node(int id) { this.id = id; }
  private final int  id;
  private       Node left;
  private       Node right;
  private       Node parent;
  //Getter - Setter - skipped
}
public class BinaryTree
    implements HasLogger {

  private Node rootNode;

  private Node add(Node current, int value) {
    if (current == null) { return new Node(value); }
    if (value < current.getId()) current.setLeft(add(current.getLeft(), value));
    else if (value > current.getId()) current.setRight(add(current.getRight(), value));
    else return current; // value already exists
    return current;
  }

  public void add(int value) { rootNode = add(rootNode, value); }
  
  public List<Integer> traverseLevelOrder() {
    if (rootNode == null) return Collections.emptyList();
    List<Integer>     nodeIDs = new ArrayList<>();
    final Queue<Node> nodes   = new LinkedList<>();
    nodes.add(rootNode);
    while (!nodes.isEmpty()) {
      Node node = nodes.remove();
      nodeIDs.add(node.getId());
      if (node.getLeft() != null) { nodes.add(node.getLeft()); }
      if (node.getRight() != null) { nodes.add(node.getRight()); }
    }
    return nodeIDs;
  }

Für diesen Test habe ich einige Funktionen nicht implementiert. Hierunter fallen zum Beispiel die Möglichkeiten, Knoten wieder aus dem Baum zu entfernen oder den Baum auf das Vorhandensein eines speziellen Knoten hin zu überprüfen. An dieser Stelle möchte ich allerdings nur einen Baum mittels eigener Klassen aufbauen, nachfolgend mit Daten befüllen und dann speichern. Das anschließende Lesen dieser Datenstruktur soll exakt dasselbe Abbild wieder im Speicher erzeugen.

Der hierzu verwendete Test ist mittels JUnit 5 implementiert. Es werden mittels Zufallszahlengenerator eine beliebige Menge an Werten erzeugt und dann dem Baum übergeben. Wie in den vorhergehenden Beispielen, wird dann die Datenstruktur mittels MicroStream Engine gespeichert sowie nachfolgend wieder gelesen und mit der initialen Instanz vom Typ BinaryTree verglichen. Um den Vergleichsprozess zu vereinfachen, gibt es die Methode traverseLevelOrder(), die einem eine Liste von Integer-Werten zurückliefert. So kann man dann mit den JUnit-5-Boardmitteln die beiden Listen vergleichen.

@Test
void test001(TestInfo info, TestReporter reporter) {
  final File                   tempFolder      = infoToCleanFolder().apply(info);
  final EmbeddedStorageManager storageManagerA = EmbeddedStorage.start(tempFolder);

  final BinaryTree tree = new BinaryTree();
  new Random().ints()
              .limit(1_000)
              .forEach(tree::add);

  final Instant start = now();
  storageManagerA.setRoot(tree);
  storageManagerA.storeRoot();
  final Instant stop = now();
  reporter.publishEntry("duration store [ms] " + between(start, stop).toMillis());
  storageManagerA.shutdown();

  final EmbeddedStorageManager storageManagerB = EmbeddedStorage.start(tempFolder);
  final BinaryTree             rootAgain       = (BinaryTree) storageManagerB.root();
  Assertions.assertEquals(tree.traverseLevelOrder(), rootAgain.traverseLevelOrder());
  storageManagerB.shutdown();
}

Fazit

Wir haben in diesem Teil gesehen, dass das Speichern und Laden von Binär-Bäumen sehr einfach in der Handhabung ist. Wenn man sich einmal mit der Herausforderung beschäftigen durfte, Bäume in XML oder JSON abzubilden, wird man nachempfinden können, wie angenehm diese Lösung ist. Ebenfalls aufwendig ist es, wenn man eine Datenstruktur wie diesen Baum in einem RDBMS ablegen und verwalten muss.

In den nächsten Teilen werden wir uns ansehen, ob und wie wir mit Graphen arbeiten können. Ich muss an dieser Stelle gestehen, das ich das Ergebnis selbst noch nicht vorhersehen kann. Ich bin gespannt, was sich ergeben wird. Ein weiteres Thema wird der Umgang mit großen Datenmengen sein, die mit der MicroStream Engine verwaltet werden.

Wer Fragen und Anmerkungen hat, meldet sich am besten per Twitter an @SvenRuppert oder direkt per Mail an sven.ruppert@gmail.com

Happy Coding!

Geschrieben von
Sven Ruppert
Sven Ruppert
Sven Ruppert arbeitet seit 1996 mit Java und ist Developer Advocate bei Vaadin. In seiner Freizeit spricht er auf internationalen und nationalen Konferenzen, schreibt für IT-Magazine und für Tech-Portale. Twitter: @SvenRuppert
Kommentare

Hinterlasse einen Kommentar

1 Kommentar auf "MicroStream: Konfiguration der Storage Engine"

avatar
4000
  Subscribe  
Benachrichtige mich zu:
Just your ordinary Java developer
Gast
Sieht ja ganz interessant aus, aber ich vermisse einige Infos – sowohl in diesem Artikel, als auch auf der Microstream-Website (die man ruhig hätte verlinken können): – Unter welcher Lizenz steht das Projekt? Gibt es irgendwo den Quellcode? Mir scheint das ganze ein Closed Source-Produkt zu sein, nur ein Binary steht auf dem Microstream-eigenen Maven-Repo zum Ausprobieren bereit. – Was ist das Geschäftsmodell von Microstream? Support, Consulting, Verkauf von Binary-Lizenzen, Verkauf des Quellcodes, ganz was anderes? Alles sehr nebulös. – Die Website liest sich fast schon großkotzig, angeberisch, als hätten sie gerade das beste Produkt seit geschnitten Brot zusammenprogrammiert. Ganz… Read more »