Apache Ignite Programmierung

Der Zugriff auf das Apache Ignite-Grid kann über verschiedene Protokolle erfolgen. Dazu werden Treiber-Bibliotheken für verschiedene Programmiersprachen angeboten. Daneben ist auch ein REST-API vorgesehen.

Nachdem Ignite selbst in Java realisiert ist bietet es sich an, auch für  Client-Anwendungen die Java-Plattform zu benutzen.

Direkter Zugriff auf einen Cache

Das Ansprechen eines Caches ist aus Sicht eines Programmierers nichts anderes als ein Map-Zugriff. Folglich ist das folgende Programm, das den Beispielprogrammen der Ignite-Community auf GitHub entnommen wurde, sehr trivial:

public class ClientPutGetExample {
    public static void main(String[] args) {
        ClientConfiguration cfg = new ClientConfiguration().setAddresses("127.0.0.1:10800");
        try (IgniteClient igniteClient = Ignition.startClient(cfg)) {
            final String CACHE_NAME = "put-get-example";
            ClientCache cache = igniteClient.getOrCreateCache(CACHE_NAME);
            Integer key = 1;
            Address val = new Address("1545 Jackson Street", 94612);
            cache.put(key, val);
            Address cachedVal = cache.get(key);
            System.out.println("Found address: " + cachedVal);
        }
        catch (ClientException e) {
            System.err.println(e.getMessage());
        }
        catch (Exception e) {
            System.err.format("Unexpected failure: %s\n", e);
        }
    }
}


Das SQL-Grid

Obwohl Ignite ein Key-Value-Store ist werden auch SQL-Abfragen unterstützt. Für die Java-Programmierung stellt sich Ignite damit als relationales Datenbank-System dar, das über einen JDBC-Treiber angesprochen wird:

public class SqlJdbcExample {
    public static void main(String[] args) throws Exception {
        try (Connection conn = DriverManager.getConnection("jdbc:ignite:thin://127.0.0.1/")) {
            try (Statement stmt = conn.createStatement()) {
                stmt.executeUpdate("CREATE TABLE city (id LONG PRIMARY KEY, name VARCHAR) " +
                    "WITH \"template=replicated\"");

                stmt.executeUpdate("CREATE TABLE person (id LONG, name VARCHAR, city_id LONG, " +
                    "PRIMARY KEY (id, city_id)) WITH \"backups=1, affinity_key=city_id\"");
                stmt.executeUpdate("CREATE INDEX on Person (city_id)");
            }
            try (PreparedStatement stmt = conn.prepareStatement("INSERT INTO city (id, name) VALUES (?, ?)")) {
                stmt.setLong(1, 1L);
                stmt.setString(2, "Forest Hill");
                stmt.executeUpdate();
                stmt.setLong(1, 2L);
                stmt.setString(2, "Denver");
                stmt.executeUpdate();
                stmt.setLong(1, 3L);
                stmt.setString(2, "St. Petersburg");
                stmt.executeUpdate();
            }
            // Populate Person table with PreparedStatement.
            try (PreparedStatement stmt =
                conn.prepareStatement("INSERT INTO person (id, name, city_id) values (?, ?, ?)")) {
                stmt.setLong(1, 1L);
                stmt.setString(2, "John Doe");
                stmt.setLong(3, 3L);
                stmt.executeUpdate();
                stmt.setLong(1, 2L);
                stmt.setString(2, "Jane Roe");
                stmt.setLong(3, 2L);
                stmt.executeUpdate();
                stmt.setLong(1, 3L);
                stmt.setString(2, "Mary Major");
                stmt.setLong(3, 1L);
                stmt.executeUpdate();
                stmt.setLong(1, 4L);
                stmt.setString(2, "Richard Miles");
                stmt.setLong(3, 2L);
                stmt.executeUpdate();
            }
            try (Statement stmt = conn.createStatement()) {
                try (ResultSet rs =
                    stmt.executeQuery("SELECT p.name, c.name FROM Person p INNER JOIN City c on c.id = p.city_id")) {
                    while (rs.next())
                        System.out.println(rs.getString(1) + ", " + rs.getString(2));
                }
            }
            try (Statement stmt = conn.createStatement()) {
                stmt.executeUpdate("DROP TABLE Person");
                stmt.executeUpdate("DROP TABLE City");
            }
        }
    }
}


Zu beachten ist, dass die Ablage der Daten immer noch in einem Key-Value-Store erfolgt: Die „Tabellen“ sind damit Caches!

Weitere Grids

Ignite bietet noch weitere Grids an:

  • Das Compute-Grid ermöglicht die Ausführung einer beliebigen Programm-Sequenz auf den Knoten des Ignite-Clusters. Dazu überträgt der Java-Client beispielsweise eine Lambda-Funktion oder ein Runnable-Objekt. Typischerweise sind diese Programme als Map-Reduce-Funktionen ausgebildet, die auf den in den Caches gespeicherten Daten operieren.
  • Mit dem Service-Grid werden auf den Cluster Services installiert.  Damit wird Ignite faktisch zu einem Applikationsserver.
  • Eine Sonderform des Compute-Grids ist Machine Learning: Die dazu benötigten Algorithmen werden als Bestandteil der Ignite-Distribution fertig implementiert zur Verfügung gestellt.

Apache Ignite: Eine Übersicht

Apache Ignite ist eine Java-basierte NoSQL-Datenbank. Ähnlich wie Apache Cassandra ist auch Ignite ein Key-Value-Store. Allerdings fokussiert Ignite nicht darauf, eine reine Datenbanklösung zu realisieren; Ignite ist deutlich flexibler ausgelegt.

Das Memory Grid und Caches

Apache Ignite ist Memory-zentriert und benötigt damit beträchtlich Speicher. Damit scheint Ignite gerade ein Produkt darzustellen, für das Java nicht gemacht ist. Aber Ignite kann ja auch das Off-Heap-Memory benutzen, um nicht durch die Garbage Collection behindert zu werden. Der vom Ignite-Prozess benutzte Speicherplatz kann somit durchaus gewaltig sein, mehrere hundert Gigabyte sind nicht außergewöhnlich.

Dies ist aber noch nicht alles: Der effektiv verfügbare Gesamtspeicher wird nämlich durch das Memory Grid definiert. Das ist die Summe der Speicher aller Ignite-Knoten, die zu einem Grid vereinigt sind. Damit sind sehr einfach Memory-Größen erreichbar, die vor wenigen Jahren nicht einmal von einzelnen Festplatten realisiert werden konnten.

Das Grid wird wieder in einzelne Bereiche unterteilt, die dann die eigentliche Datenhaltung übernehmen. Dies sind die Caches. Jeder Cache kann hierbei konfiguriert werden, um den fachlichen Anforderungen zu entsprechen:

  • Bei einem partitionierten Cache werden die Daten an Hand des Keys auf die verschiedenen Knoten verteilt. Optionale Backup-Knoten verhindern Datenverluste. Damit kann das Memory Grid besonders gut große Daten-Volumina halten.
  • Bei einem replizierten Cache werden die Daten auf allen Knoten gleichzeitig vorgehalten. Damit können Abfragen auf und Analysen der Daten sehr gut parallelisiert werden.
  • Daneben unterstützt Ignite verschiedene Modi beim Schreiben von Daten: ATOMIC und TRANSACTIONAL. Ein transaktioneller Cache garantiert hierbei die Datenkonsistenz. Aus Sicht des CAP-Theorems ist ein solcher Cache ein CP-System, ATOMIC hingegen AP.

Cache-Persistenz

Um eine Ausfallsicherheit zu erreichen können die Caches persistiert werden. Dies erfolgt entweder

  • Klassisch durch Ablage der Daten in einem Datenbank-System. Dazu wird der Cache entweder mit READ/WRITE-THROUGH oder mit WRITE-BEHIND konfiguriert. Details hierzu liefert die Wikipedia.
  • Die Ignite Persistence schreibt ein  Write Ahead Log im lokalen Dateisystem oder in einem Shared Directory. Damit hält der Ignite-Knoten seine Daten selbst und ist nicht auf ein Backend-System angewiesen.

Dokumenten-orientierte Datenbanksysteme

Dokumenten-orientierte Datenbanksysteme sind im Rahmen der  NoSQL-Bewegung entstanden und haben sich in den letzten Jahren zu stabilen und etablierten Produkten entwickelt. Im aktuellen Ranking von Datenbanksystemen steht mit der kommerziellen MongoDB ein Document-Store unter den Top 5, aber auch die in einer vollwertigen Community-Edition vorliegende Couchbase ist, wenn auch deutlich schwächer, vertreten.  

Was sind Dokumente?

Ein Dokument ist eine Datenstruktur, die ähnlich wie eine Datenbank-Tabelle einem definierten Schema genügt. Allerdings wird dieses Schema von der Datenbank in der Regel nicht über Constraints beim Schreiben des Datensatzes geprüft („Schema on write“), sondern erst bei Abfragen („Schema on read“):  Dokumente, die der Query entsprechen, werden in die Treffermenge aufgenommen, nicht-passende Dokumente eben nicht. Eine Dokumenten-orientierte Datenbank benötigt deshalb nicht unbedingt verschiedene Tabellen-Definitionen, sondern kann alle Dokumente in einer einzigen Collection oder einem „Bucket“ ablegen.

So können beispielsweise in einer Datenbank sämtliche Dokumente einer Reiseagentur (Fluglinien, Flughäfen, aber auch Flugpläne und Routen) gemeinsam abgelegt werden.

Dokumente werden über eine innerhalb der Datenbank eindeutigen Document-ID identifiziert.

Links versus Joins

Im Gegensatz zu einem relationalen Modell unterstützen Dokumente Server-seitige Joins nicht unbedingt. Es ist eher üblich, Dokumente zu Verlinken und damit im Endeffekt dem Client das Nachladen von Assoziationen zu überlassen. 

Dokumenten-Formate

Als Quasi-Standard für das Format von Dokumenten hat sich JSON herauskristallisiert. Dies ist einesteils etwas überraschend, da JSON bis heute keinen wirklichen Standard für Links definiert hat. Hier ist XML klar überlegen. Andererseits werden Dokumente sehr häufig im Rahmen einer RESTful Architektur benutzt, so dass in der Praxis als Implementierung http genutzt wird. Und dafür ist JSON die natürliche Wahl.

Clusterbetrieb

Dokumenten-orientierte Datenbanksysteme sind immer auf einen Cluster-Betrieb ausgerichtet. Das ergibt sich klar aus dem Bezug zur NoSQL-Bewegung und damit dem „Big Data“-Umfeld. 

Für die Umsetzung eines dynamisch skalierenden Clusters bieten sich zwei Strategien an:

  • Sharding: Hier werden die Dokumente auf Grund eines Sharding Keys, der nicht unbedingt der Document-ID entsprechen muss, auf die verschiedene Knoten des Clusters verteilt. Ein zentraler Master oder Router nimmt alle Anfragen entgegen und verteilt diese dann an Hand von Konfigurations-Informationen auf die Knoten, die die Daten enthalten. Die MongoDB ist ein Beispiel für diese Cluster-Architektur. Die folgende Abbildung entstammt der Dokumentation unter https://docs.mongodb.com/manual/core/sharded-cluster-components/:
Ein einfacher Mongo Cluster
Couchbase Cluster