Über unsMediaKontaktImpressum
Thomas Haug 24. September 2019

Gremlins im Cosmos: Graphverarbeitung mit CosmosDB

Seit 2017 bietet Microsoft mit der Cosmos DB eine schema-freie Multi-Modell-Datenbank für die Azure Cloud an. Neben den Microsoft eigenen APIs (Document API und Table API) werden zusätzlich die Open-Source-Schnittstellen Cassandra API, MongoDB API und Gremlin API unterstützt. Als Multi-Model-Datenbank werden von der Cosmos DB die unterschiedlichen Abstraktionen der APIs auf ein internes Modell abgebildet. In diesem Artikel werden wir Cosmos DB als Graphdatenbank nutzen. Eine allgemeine Cosmos-DB-Einführung ist unter [1] zu finden.

Ein paar Grundlagen

Spielen die Beziehungen zwischen Entitäten eine große Rolle, eignen sich zur Speicherung nicht immer klassische (= relationale) Datenbanken. Vielmehr bieten sich in diesem Fall besonders Graphdatenbanken zur Verwaltung solcher Daten an. Typische Anwendungsbeispiele sind Beziehungen in sozialen Netzwerken, im Supplier-Chainmanagement und Threat-Bewertung in Computernetzen [2]. Üblicherweise werden sogenannte Property-Graphen genutzt, die folgenden Elemente besitzen

  • Knoten (Vertex)
  • Kanten (Edge)
  • Labels
  • Eigenschaften (Properties)

In der folgenden Abbildung ist das Zusammenspiel dieser Elemente skizziert.

Ein Graph besteht aus Knoten und Kanten. Sowohl Knoten als auch Kanten besitzen Labels, die sozusagen den Typ eines Knotens bzw. einer Kante charakterisieren. Neben diesen können Knoten und Kanten eine beliebig große Menge an Namen-Wert-Paaren, die sogenannten Properties, besitzen. Beliebig groß, da im Allgemeinen die Graphdatenbanken keine Schematas vorschreiben (in Ausnahmefällen, z. B. OrientDB, ist dies trotzdem möglich). Der wohl bekannteste und verbreitetste Vertreter von Graphdatenbanken ist die Neo4J-Datenbank. Als Abfragesprache bietet Neo4J die Sprache Cypher an [3]. Zusätzlich gibt es mit OpenCypher eine öffentlich zugängliche Spezifikation, so dass andere Graphdatenbank-Hersteller diese implementieren können. Trotz Neo4Js und Cyphers Bekanntheitsgrad (oder aus diesem Grund?) hat man sich bei Microsoft entschieden, anstelle von Cypher eine andere Abfragesprache zu unterstützen, und zwar Gremlin. Gremlin ist eine von der Apache Foundation bereitgestellte Graphdatenbank-Abfragesprache und ist Bestandteil des Apache-TinkerPop-Projekts [3].

Für die weitere Betrachtung von Cosmos DB und Gremlin soll das folgende vereinfachte Graph-Modell aus der statischen Code-Analyse herhalten.

In unserem Modell werden Klassen als Knoten mit dem Label Class modelliert, weitere Typen wie Schnittstellen, Enums, Structs und primitive Datentypen werden nicht unterstützt. Unser Modell kann Vererbungsbeziehungen zwischen Klassen über die derivesFrom-Kante beschreiben. Jegliche Zugriffe auf Methoden, Variablen und Properties (im Falle von .NET) werden als calls-Kanten definiert. Auf Basis dieses sehr einfachen Modells werden wir später Knoten und Kanten erzeugen und abfragen.

Los geht es: Datenbank aufsetzen

Die Cosmos-DB-Datenbank legen wir im Azure-Portal an:

Das wesentliche Kriterium ist die Angabe der gewünschten API. In unserem Fall gremlin (graph). Drücken wir nun den review + create-Button, so werden die eingegeben Daten validiert und anschließend die Datenbankinstanz erzeugt:

Nachdem nun die Datenbank erzeugt ist, können wir über den Data Explorer eine neue Graph Collection anlegen, z. B. CodeModel.

Ein relativ neues Feature bei der Erzeugung der Graph-Collection ist die zwingende Angabe eines Partition-Schlüssels. Generell werden die Daten in der Cosmos DB in sogenannten Containern ablegt. Sind diese größer als 10 GB oder wird mit mehr als 10.000 Request Units auf diese zugriffen, so verlangt die Cosmos DB eine Partitionierung der Daten [4][5].

Arbeiten mit der Gremlin Console

Die Datenbank ist angelegt, jetzt wollen wir Knoten und Kanten in unseren Graphen anlegen. Doch wie soll dies vonstatten gehen? Das Portal bietet rudimentäre Möglichkeiten, Knoten anzulegen und Gremlin-Scripte (= Anweisungen) auszuführen. Besser funktioniert dies aber mit der Apache TinkerPop Gremlin Console.

Über das Azure-Portal lässt sich die Gremlin Console bereits über das Quickstart-Beispiel beziehen. Hierbei wurden in der Vergangenheit die korrekten Verbindungsparameter gesetzt und man konnte direkt loslegen. Dies ist Stand heute (September 2019) nicht mehr der Fall und es müssen manuelle Änderungen durchgeführt werden. Haben wir die Console bezogen, so öffnen wir im Verzeichnis conf die Datei remote-secure.yaml, um diese anzupassen. Es muss grundsätzlich über eine HTTPS-Verbindung mit der Cosmos DB kommuniziert werden. Aus diesem Grund sind die übrigen Verbindungskonfigurationen für uns nicht von Interesse. Die besagte Datei muss den folgenden Inhalt besitzen (basierend auf den zuvor gemachten Angaben): 

hosts: [informatikgremlin.gremlin.cosmos.azure.com]
port: 443 
username: /dbs/graphdb/colls/CodeModel 
password: dOJTKnb…K5K6qCPg== 
connectionPool: {enableSsl: true} 
serializer: { className:               
      org.apache.tinkerpop.gremlin.driver.ser.GraphSONMessageSerializerV2d0,config: 
      {serializeResultToString: true }}

Haben wir das Quickstart-Beispiel bezogen, so ist der Host falsch konfiguriert, da informatikgremlin.graphs.azure.com anstelle von informatikgremlin.gremlin.cosmos.azure.com gesetzt wird (dies war in einer vorherigen Cosmos-DB-Version der korrekte Pfad). Auch die Serializer-Version ist in dem vorkonfigurierten Beispiel auf Version 2 anzuheben:

org.apache.tinkerpop.gremlin.driver.ser.GraphSONMessageSerializerV1d0
org.apache.tinkerpop.gremlin.driver.ser.GraphSONMessageSerializerV2d0

Haben wir anstelle der Quickstarts die Gremlin Console direkt von der Apache-Webseite bezogen oder einen neuen Graphen angelegt, so müssen wir als Usernamen den Graph angeben, hierbei sind dbs und colls unveränderliche Bestandteile und das Passwort erhalten wir über das Azure-Portal im Reiter Keys:

Nun sind wir bereit die Console (gremlins.bat bzw gremlins.sh – hierzu muss ein Java installiert sein) zu starten und uns mit dem Befehl

:remote connect tinkerpop.server conf/remote-secure.yaml

mit unserer Cosmos-DB-Instanz zu verbinden:

Wie oben erwähnt, möchten wir ein einfaches Code-Modell beschreiben. In der nächsten Abbildung ist für die Klasse Demo (in C#) ein mögliches Modell in Sinne eines Graphen skizziert.

In unserem Graphen existieren 3 Knoten mit dem Label Class, die jeweils eine Eigenschaft name besitzen, in welcher der Klassenname gespeichert wird. Zwischen den Knoten existieren eine Vererbungsbeziehung (derivesFrom) und eine Aufruf-Beziehung (calls). 

Die Knoten legen wir in der Gremlin Console mit folgendem Skript an: g.addV('Class').property('name','Demo'). Um das Skript an der Server (= Cosmos DB) zu senden, müssen wir ein submit in der Gremlin Console auslösen, dies geschieht durch die Zeichenfolge ‘:>‘, ansonsten wird versucht, das Skript lokal auszuführen:

Der Befehl g.V() liefert alle verfügbaren Knoten zurück, während g.V().hasLabel('Class') alle Knoten mit dem Label Class liefert.

Möchten wir den Knoten mit dem Namen Demo nutzen, so können wir ihn mittels der Anweisung g.V().hasLabel('Class').has('name','Demo') finden. Die Angabe des Labels ist in unserem einfachen Beispiel nicht notwendig. Sollte jedoch mehrere Knoten mit dem Namen Demo aber verschiedenen Labels existieren, so würden wir wieder eine Liste erhalten (Randbemerkung: selbstverständlich können wir mehrere Knoten mit dem Label Class und dem Namen Demo anlegen. Auch dann würden wir wieder mehrere Knoten als Antwort erhalten. Aber für die aktuelle Betrachtung gehen wir davon aus, nur einen Knoten als Antwort zu erhalten).

Damit wir die Liste aller Knoten erhalten, die mit dem Namen Demo starten, müssen wir uns eines Kunstgriffs bedienen. Manche gremlinfähigen Datenbanken unterstützten sogenannte Lambda-Expressions in Groovy. Cosmos DB tut dies nicht, d. h. die folgende Anweisung führt zu einem Fehler: g.V().hasLabel('Class').filter{ it.get().value('name').startsWith('Demo')}. Aus diesem Grund simulieren wir eine startsWith-Abfrage mit folgendem Skript: g.V().hasLabel("Class").has('name',between('Demo','DemoZ')).

Da wir nun wissen, wie wir Knoten anlegen und finden, beginnen wir nun Beziehungen zu beschreiben. Mit der Anweisung addE (E = edge) erzeugen wir die Beziehung von Klasse Demo zu seiner Superklasse in unserem Graph:

g.V().hasLabel('Class').has('name','Demo').addE('derivesFrom')
        .to(g.V().hasLabel('Class').has('name','DemoSuperClass'))

Und die call-Beziehung erzeugen wir analog:

g.V().hasLabel('Class').has('name','Demo').addE('calls')
        .to(g.V().hasLabel('Class').has('name','Console'))

Im Azure Portal können wir unser einfaches Modell visualisieren:

Mit g.E() können wir auf die Menge der Kanten zugreifen, z. B. die Anzahl der vorhandenen Kanten zählen: g.E().count() und g.E().hasLabel('calls').count(). Die erste Abfrage liefert das Ergebnis 2, da alle Kanten unabhängig ihres Labels gezählt werden, während das zweite Statement 1 zurück gibt.

Erweitern wir nun unser Modell um die Klasse Object. Von dieser sind die Klassen Console und DemoSuperClass abgeleitet. Die notwendigen Statements zum Hinzufügen von Knoten (addV) und Kanten (addE) haben wir bereits zuvor gesehen. Anschließend wird unser Graph im Azure-Portal wie in Abb. 11 dargestellt.

Wie können wir alle Klassen finden, die von einer bestimmten Klasse abgeleitet sind? Hierzu können wir in unserem Beispiel die Klasse Objekt verwenden und mittels der inE()-Anweisung eingehende Kanten finden:

g.V().hasLabel('Class').has('name','Object').inE('derivesFrom')

Uns interessieren aber die Kanten nicht so sehr, vielmehr möchten wir die zugehörigen Klassen wissen. Deshalb "fragen" wir einfach die zurückgelieferten Kanten, von welchen Knoten sie entspringen. Hierzu nutzen wir die outV()-Anweisung:

g.V().hasLabel('Class').has('name','Object').inE('derivesFrom').outV()

Die Kombination aus inE() und OutV() bzw. outE() und InV() sind sehr gebräuchlich. Aus diesem Grund existieren in Gremlin die Hilfsmethoden in() und out(), um die entsprechenden Abfragen einfacher formulieren zu können. Die Anweisung

g.V().hasLabel('Class').has('name','Object').in('derivesFrom') 

liefert alle Knoten zurück, die eine eingehende Kante mit dem Label derivesFrom in den Knoten Object besitzen. Bei den genannten Operationen (inE, outE und in bzw. out) kann auf die Angabe eines Labels verzichtet werden. In diesem Fall werden entsprechend alle ein- bzw. ausgehenden Kanten berücksichtigt. 

Wie geschrieben werden mit der letzten Anweisung alle Knoten zurückgeliefert, die direkt von Object abgeleitet sind. Aber nicht der Knoten Objekt. Wenn wir diesen auch in der Ergebnismenge erhalten wollen, so können wir mit der as-Anweisung auf (Teil)-Ergebnisse zugreifen.

Das folgende Statement bewerkstelligt dies:

g.V().hasLabel('Class').as('from')
.out('derivesFrom').has('name','Object').as('to').select('from','to').by('name')   

Wir suchen alle Knoten mit dem Label Class und "markieren" diese mit dem Bezeichner from. Knoten mit den Namen Object, die eine direkte eingehende Kante vom Label derivesFrom haben, werden mit dem Bezeichner to versehen. Nun selektieren (select-Anweisung) wir sowohl die Bezeichner from und to und wenden auf die Ergebnismenge die Operation by() an, damit lediglich der Name der Knoten zurückgeliefert wird. Als Ergebnis dieser Anweisung erhalten wir:

Möchten wir feststellen, welche Klassen direkt oder indirekt von Object abgeleitet sind, so können wir dies mit den repeat-until-Anweisungen durchführen:

g.V().hasLabel('Class').repeat(out('derivesFrom')).until(has('name','Object')).path().by('name')

Möchten wir die von Robert C. Martin definierte Metrik efferent Coupling (Anzahl der ausgehenden Abhängigkeiten einer Klasse [6]) berechnen, so können wir in unserem einfachen Modell z. B. die calls-Beziehung bemühen: wir zählen einfach, wie viele unterschiedliche Klassen eine Klasse aufruft:

g.V().hasLabel('Class').has('name','Demo').out('calls').count()

Mit der out('calls')-Anweisung erhalten wir die Liste aller Knoten (= Klassen), die von der Klasse Demo aufgerufen werden und mit count() zählen wir die Anzahl der zurückgelieferten Knoten. So weit so gut. Würden wir aber eine weitere calls-Kante von Demo zu Console hinzufügen, so würden wir in diesem Fall eine Ergebnismenge von zwei Kanten erhalten. Im Rahmen unserer einfachen efferent-Coupling-Messung wäre dies aber falsch, da wir nur die Liste aller unterschiedlicher Klassen abzählen wollen, d. h. eine Mehrfachnennung ist in diesem Fall nicht korrekt. Deshalb deduplizieren wir die Ergebnismenge mit der dedup-Anweisung:

g.V().hasLabel('Class').has('name','Demo').out('calls').dedup().count()

Die calls-Kante können wir auch zur Bestimmung von zirkulären Abhängigkeiten nutzen. Stellen wir uns vor, es würde eine weitere Klasse CircularCaller in unserem Modell existieren und diese besäße eine calls-Kante zur Klasse Demo, die ihrerseits eine entsprechende Kante zum CircularCaller-Knoten besitzt. Um in unserem Graphen alle zirkulären Abhängigkeiten zu finden, nutzen wir die zuvor beschriebene repeat-until-Anweisungen. Diesmal verwenden wir jedoch das cyclicPath-Kommando:

g.V().hasLabel('Class').repeat(out('calls')).until(cyclicPath()).path().by('name')

Als Ergebnis liefert uns nun die Cosmos DB [Demo,CircularCaller,Demo] und [CircularCaller,Demo,CircularCaller].

Mit diesem Beispiel schließen wir unseren kurzen Ausflug in die Gremlin-Sprache ab. Selbstverständlich haben wir nur einen kleinen Ausschnitt der Sprachfeatures gesehen. Weiterführende Beispiele und Dokumente finden sind auf der TinkerPop-Webseite [3].  

Cosmos DB mit C# ansprechen

Nachdem wir nun die Cosmos DB mittels der Gremlin-Console angesprochen haben, stellt sich die Frage, wie die Datenbank in ein eigenes Programm integriert werden kann. Wie eingangs geschrieben, bietet die Cosmos DB als Multi-Modell-Datenbank unterschiedliche APIs an. Abhängig vom gewählten API wird in der Azure-Console eine Quickstart-Anwendung unter Verwendung des zugehörigen SDKs bereitgestellt. Hierbei werden für gängige Programmiersprachen wie Java, C#, Python, etc. Beispiele zur Verfügung gestellt.

Da wir die Graph-API gewählt haben, wird uns von Microsoft nur eine C#-Demo-Anwendung angeboten. Für andere Programmiersprachen müssen wir die TinkerPop-Dokumentation konsultieren. Bei der Anbindung mittels C# hat sich in den vergangenen Monaten einiges getan. In der Vergangenheit hat Microsoft eine eigene client-seitige Bibliothek angeboten (Assembly Microsoft.Azure.Graphs), über die Gremlin-Skripte ausgeführt werden können. Da aber seit einiger Zeit eine offizielle Gremlin-Bibliothek für .NET existiert (Nuget Package Gremlin.NET), wird diese nun von Microsoft direkt verwendet:

Analog zu unserem Beispiel mit der Gremlin-Console können wir dieses Beispiel für unsere Belange anpassen:

  1. Zuerst sollten wir die Gremlin.NET-Version von einem Release Candidate (3.4.0-rc2) auf eine stabile Version anheben (z. B. 3.4.3, Stand August 2019)
  2. Des Weiteren müssen wir die Verbindungsdaten auf unseren CodeModel-Graph ändern.  

Der Programmcode zum Hinzufügen eines neuen Knotens ist im folgenden Screenshot von Visual Studio gezeigt:

Diese Art des Zugriffs auf gremlin-basierte Datenbanken wird als skript-basierter Ansatz bezeichnet. Analog zum vorherigen Abschnitt formulieren wir unsere Abfrage als Zeichenkette.

Mit den im ersten VisualStudio-Screenshot dargestellten Variablen erzeugen wir ein GremlinServer-Objekt. Anschließend instanziieren wir in Zeile 69 einen GremlinClient unter Verwendung der zuvor erzeugten GremlinServer-Instanz. Zusätzlich müssen wir Serialiserungseinstellungen für die Übertragung der Daten vornehmen. Im Falle von Cosmos DB ist dies GraphSON2, ein JSON-basiertes Format. Kryo, ein weiteres von TinkerPop unterstütztes Format, wird von der Cosmos DB jedoch nicht unterstützt. Unsere Abfrage wird nun als String über die SubmitAsync-Methode an die Cosmos DB übertragen. Als Resultset erhalten wir eine Liste, die JSON-Objekte enthält. Im Falle unseres Beispiels das Ergebnis der addV-Funktion. Neben dem eigentlichen Knoten (erstes Element der zurückgelieferten Liste), erhalten wir noch zusätzliche Cosmos-DB-spezifische Ergebnisobjekte, welche den Erfolg der Operation charakterisieren.

Favorisiert wird mittlerweile der sogenannte byte-code-Zugriff, der einen fluent API-Ansatz verfolgt. Das folgende Codefragment liefert alle Knoten zurück, die von der Klasse Demo aufgerufen werden:

   using (var gremlinClient = new GremlinClient(gremlinServer,
                                                new GraphSON2Reader(),
                                                new GraphSON2Writer(),
                                                GremlinClient.GraphSON2MimeType))
   {

      GraphTraversalSource g = Traversal().WithRemote(new
                                              DriverRemoteConnection(gremlinClient));
     

      IList<Vertex> list = g.V().HasLabel("Class")
                                .Has("name", "Demo")
                                .Out("calls")
                                .ToList();

   }

Leider führt dieser Code bei Cosmos DB (noch) zu einem Fehler:

  Gremlin.Net.Driver.Exceptions.ResponseException
  HResult=0x80131500
  Message=MalformedRequest:
  ActivityId : 9083027f-63cf-431b-a10a-1c187ace02e6
  ExceptionType : GraphMalformedException
  ExceptionMessage :
     Gremlin Malformed Request: Unsupported request operation: 'bytecode'.
     GremlinRequestId : 9083027f-63cf-431b-a10a-1c187ace02e6
     Context : global
     GraphInterOpStatusCode : MalformedRequest
     HResult : 0x80131500
     Source=Gremlin.Net
     StackTrace:
      at Gremlin.Net.Driver.Messages.ResponseStatusExtensions.
                          ThrowIfStatusIndicatesError(ResponseStatus status)
      at Gremlin.Net.Driver.Connection.TryParseResponseMessage(
                          ResponseMessage`1 receivedMsg)
      at … 

D. h. diese Zugriffsvariante wird aktuell noch nicht von der Cosmos DB unterstützt.

Eine weitere Zugriffsvariante, die in vorherigen Releases genutzt wurde, verwendet zum Teil die Document API, um anschließend über Extensions Methods auf Graph-Objekte (Vertex und Edge) zuzugreifen. Der Zugriff über die Document API ist immer noch möglich, da für diese API ein Endpunkt im Azure-Portal angeboten wird – dies ist aber nicht mehr zu empfehlen:

Fazit

In diesem Artikel haben wir die Verwendung der Cosmos DB als Graphdatenbank gesehen. Schwerpunkt war hierbei der Zugriff über Gremlin. Es zeigt sich, dass Microsoft in den vergangenen Monaten weitreichende Änderungen in der Unterstützung von Gremlin unternommen hat. Einige Gremlin-Sprachkonstrukte, die in der Vergangenheit nicht unterstützt wurden, funktionieren nun sehr gut. Auch für .NET-Anwendungen gibt es deutliche Fortschritte. Insbesondere die Vermischung von Document API und einer eigenen Graph API wurde zugunsten der Gremlin.NET-Implementierung fallen gelassen. Als Wermutstropfen müssen wir aktuell die fehlende Unterstützung des Byte-code-Ansatzes nennen.  

Aspekte wie Sicherheit, Skalierung, Performance und Kosten haben wir im Zuge des Artikels außer Acht gelassen. Für eine produktiv eingesetzte Lösung müssen wir diese Aspekte selbstverständlich betrachten, um entscheiden zu können, ob Cosmos DB für unseren Anwendungsfall eine Alternative zu anderen Graphdatenbanklösungen ist.

Quellen
  1. Eine Einführung in die Azure Cosmos DB
  2. R. Brath und D. Jonker; 2015: Graph Analysis and Visualization, Wiley
    Stefan Schubert: Einstieg in die Graphdatenbank Neo4j, Informatik Aktuell
  3. Apache TinkerPop
  4. Microsoft Azure: Partitionierung in Azure Cosmos DB
  5. Microsoft Azure: Verwenden eines partitionierten Graphen in Azure Cosmos DB
  6. Robert C. Martin; 2012: Agile Software Development Principles, Patterns, and Practices, Pearson

Autor

Thomas Haug

Thomas Haug arbeitet als CTO für die MATHEMA Software GmbH und Redheads Ltd. Sein Schwerpunkt liegt auf verteilten Java- und .NET-Enterprise-Systemen.
>> Weiterlesen
Das könnte Sie auch interessieren
Kommentare (0)

Neuen Kommentar schreiben