Über unsMediaKontaktImpressum
Roland Scheel 25. August 2020

Cassandra versus relationale DBMS in Entwicklung und Betrieb

Nicht-relationale Datenbanken finden eine zunehmende Verbreitung. Wir erläutern auf Basis eines realen Projektes unsere Erfahrungen und Problemlösungen, die mit dem Einsatz von Cassandra anstelle einer relationalen Datenbank einhergingen. Wir konzentrieren uns auf die Entwicklungs-, Betriebs- und Performance-Aspekte von Cassandra im Speziellen und nicht-relationalen Datenbanken im Allgemeinen. Es gibt neben Cassandra eine Vielzahl von nicht-relationalen Datenbanken auf dem Markt. Viele der geschilderten Erfahrungen lassen sich auf diese Datenbanken übertragen.

Daneben möchten wir betonen, dass das entstandene System durch die Wahl der Datenbank und durch das objektorientierte und zugleich zustandslose Design außerordentlich performant ist. Die Performance-Vorteile ergeben sich hauptsächlich durch den Verzicht auf die Modellierung der Stammdaten in der Datenbank. Anstelle einer datenbank-zentristischen Ablage dieser Stammdaten tritt eine framework-basierte In-Memory-Konfiguration. Das Framework bietet Dependency Injection mit vollem Lifecycle-Management, welches wir aus anderen Dependency Injection Frameworks nicht kennen.

Problemstellung

Unser Kunde bietet ein CCS (Cargo Communication System) an, das den Nachrichtenaustausch von Luftfrachtdaten realisiert.

Das Projekt hatte folgende Ziele:

  • Die Ablösung des bestehenden Message Brokers,
  • die Ablösung von Teilen der Data-Warehouse-Funktionalitäten,
  • die Erweiterung des Funktionsumfangs um Datenanalyse sowohl für den internen Gebrauch, als auch als Service für die Kunden des Systems und
  • die Sicherstellung einer sehr hohen Verfügbarkeit.

Die Kernfunktionen des Systems sind:

  • Das Weiterleiten von Nachrichten in sehr vielfältigen Formaten und Formatvarianten,
  • die vollständige Protokollierung aller Nachrichten und
  • die Erzeugung abrechnungsrelevanter Datenauszüge.

Das Projekt umfasste neben der Entwicklung des neuen Systems auch

  • vollständige Regressionstests gegen das Bestandsystem und
  • die Betriebsbetreuung des Systems durch uns als Dienstleister.

Die Betriebsbetreuung erstreckt sich auf Bereiche, die wir als reines Softwarehaus ohne Betrieb von Kundensystemen bislang nicht angeboten hatten. Zu diesen Bereichen zählen die Integration, das Deployment und die Betreuung von Softwareanteilen von Drittanbietern inklusive Versionshub – in diesem Fall ist das Cassandra in der Datastax Enterprise Edition.

Neben den oben genannten Projektinhalten gab es die Herausforderung, das System neu zu implementieren, wobei große Teile des Systemverhaltens nicht als Anforderungen vorlagen. Die Anforderungen an das System konnten daher nur über das Verhalten des abzulösenden Systems ermittelt werden. Zu diesem Zweck wurde eine sehr große Menge von durch das Altsystem verarbeiteten Nachrichten nebst erzeugten Ausgangsnachrichten verwendet, um das Verhalten des neuen Systems auf Deckungsgleichheit zu prüfen. Gleichzeitig war der zeitliche Realisierungsrahmen sehr eng. Um diese Datenmengen handhabbar zur Grundlage der Verifikation des neuen Systems zu machen, ergaben sich besondere Anforderungen zu Performance des Systems während der Entwicklung, denn die Zyklen zwischen Test und Weiterentwicklung mussten zwingend sehr klein sein. Im Ergebnis ist so ein System entstanden, das die Performance-Anforderungen im Betrieb um ein Vielfaches übertrifft.

Grobarchitektur

Zur Veranschaulichung des Systems hier in knapper Form die Architektur:

Das System verfügt über verschiedene Eingangs- und Ausgangskanäle. Vom System empfangene Nachrichten werden zunächst vom System vollständig verarbeitet. Der Versand der Ausgangsnachrichten erfolgt dabei nebenläufig.

Entwicklungsaspekte

Nicht-relationale Datenbanken haben häufig weniger Features als relationale Datenbanken. Dazu zählen fehlende ER-Modellierung (wie z. B. Foreign Keys), Einschränkungen bei Sekundär-Indexes, Einschränkungen der Abfrage (hier CQL) gegenüber SQL und keine Transaktionen über mehrere Schreiboperationen hinweg. Auf diese Einschränkungen wurde im Projekt mit alternativen Ansätzen reagiert.

Ersatz für ER-Modellierung

Im Projekt wurde anstelle der ER-Modellierung mittels Modellierung aller Entitäten durch objektorientierte Modellierung mit Java verwendet. Das betrifft sowohl eher statische Daten als auch dynamische Daten. Eher statische Daten entsprechen im Sprachgebrauch den so genannten Stammdaten in einer relationalen Datenbank, dynamische Daten den so genannten Bewegungsdaten. Die mit Java modellierten Daten werden in Cassandra unstrukturiert als JSON-Daten abgelegt.

Die Stammdaten des Systems (seine Konfiguration) befinden sich in einem kohärenten Modell. Konkret sind das hier alle technischen Angaben zu den Nutzern des Systems bestehend aus Adressierungsinformation, verknüpfter Kommunikationsschnittstelle, unterstützten Formaten, kundenspezifischen Konvertierungen und vieles mehr. Insgesamt sind das einige hundert verschiedene Entitäten, zu denen im System einige tausend Instanzen verwaltet werden.

Die Bewegungsdaten des Systems werden ebenfalls durch Java-Objekte beschrieben, die auch im JSON-Format in der Datenbank gespeichert werden. Die Bewegungsdaten enthalten dabei alle Informationen, die zur späteren Verarbeitung benötigt werden.

Dies soll am folgenden Beispiel einer Ausgangsnachricht erläutert werden:

{
  "@c": ".ExternalizableMqPusher",
  "mqServer": {
    "type": "MQ",
    "id": "4c6a2104-338b-41c5-8912-8d89d810885a",
    "host": "111.111.111.111",
    "port": "1111",
    "queueManager": "SECRET",
    "channel": "SECRET.CHANNEL",
    "user": "USER",
    "password": "PASSWORD"
  },
  "queueName": "ZCSTXXH.TYPEB",

  "payload": […]
}

Der Inhalt dieser JSON-formatierten Ausgangsnachricht kann ohne Umgebungsbezug versendet werden, weil neben dem Nachrichteninhalt auch die technischen Informationen Teil des Objektes sind. Durch diese Form der Darstellung sind deshalb auch keine Fremdschlüsselbeziehungen nötig.

Für die Verwaltung der Stammdaten gibt es im Projekt die Anforderung, dass mehrere Benutzer Änderungen parallel durchführen können. Oder sogar zukünftige Änderungen vorab erfassen können, um sie zu einem späteren Zeitpunkt in das laufende System einzubringen. Im Projekt haben wir ein eigenes Framework eingesetzt, das den Umgang mit kohärenten Konfigurationsmodellen vereinfacht. Insbesondere das dort integrierte Lifecycle-Management hat sich als sehr effizient im Hinblick auf die Entwicklungsgeschwindigkeit erwiesen.

Zur Unterstützung der parallelen Änderung der Konfiguration verwendet das Framework einen Drei-Wege-Merge, wie man es von Sourcecode-Verwaltungen gewohnt ist. Ein Benutzer editiert dabei immer eine bestimmte Revision der Konfiguration. Speichert der Benutzer diese Konfiguration, so ermittelt das Framework die Änderungen und pflegt diese in die aktuelle Konfiguration ein. Die aktuelle Konfiguration muss dabei nicht dem editierten Stand entsprechen. Viele der Konfigurationsobjekte sind allerdings aktiv, wie beispielsweise Eingangskanäle. Dazu stellt das Framework ein Lifecycle-Management bereit, das diese aktiven Objekte zur Laufzeit austauscht, ohne dass dabei unveränderte Objekte betroffen sind. Der Austausch der Konfiguration erfolgt dabei atomar, also ohne Übergangszeit und ohne Auszeit. Das laufende System ist dennoch "immutable", d. h. es enthält keine dynamischen Zustandsinformationen.

Die Oberfläche zur Bearbeitung der Konfiguration ist eng an die Funktionsweise des Systems angelehnt. Dadurch ergibt sich ein tieferes Verständnis der Bediener für die Funktion des Systems.

Durch die objektorientierte Modellierung der Konfiguration ergeben sich auch Vorteile im Systemdesign. Aus unserer Erfahrung werden in Projekten mit Einsatz von relationalen Datenbanken oft Alternativen zu polymorpher Modellierung gewählt, beispielsweise durch Steuerattribute, die das Verhalten des Systems beeinflussen. Im besprochenen Projekt stellt sich die polymorphe Modellierung durch den entfallenden Abbildungsbruch als sehr einfach dar, weshalb umfangreich davon Gebrauch gemacht wurde. So stehen im System dutzende verschiedener Präprozessoren und Postprozessoren zur Verfügung. Eine relationale Modellierung wäre ein hoher Aufwand ohne Mehrwert.

Abb. 2 illustriert die am Systemdesign orientierte Oberfläche, hier am Beispiel eines kundenspezifischen Präprozessors. Der gezeigte Präprozessor hat eigene Attribute, die der Bediener pflegen kann. Diese Attribute können neben einfachen Daten auch komplexe Objekte darstellen. Die editierbaren Objekte befinden sich jeweils an der Position in der Konfiguration, an der sie im System auch verwendet werden.

An dieser Stelle muss hervorgehoben werden, dass es noch einen sehr erheblichen Zusatznutzen der kohärenten Konfiguration gibt. Die gesamte Konfiguration existiert in-memory im laufenden System. Stammdaten-Abfragen an die Datenbank während der Nachrichtenverarbeitung sind deshalb unnötig. Die sich daraus ergebenden Performancevorteile sind sehr beachtlich. Im weiter oben genannten Test des neuen Systems gegen Testdaten des Altsystems wurde eine Performance erzielt, die die Zielperformance für Produktionszwecke um das 10.000-fache überschreitet. Im produktiven Einsatz, der zusätzlich zu den durchgeführten Tests auch das Persistieren der Daten beinhaltet, wird die erforderliche Performance um etwa das 100-fache übertroffen, wobei ein einzelner, vergleichsweise klein dimensionierter Server zum Einsatz kommt.

Ersatz für sekundäre Indizes

In der relationalen Welt gilt die Vermeidung von Redundanz als oberstes Ziel. Bei nicht-relationalen Datenbanken wird häufig das genaue Gegenteil proklamiert. Warum?

In relationalen Systemen wird versucht, alle Datenabfragen durch das Anlegen von entsprechenden Indizes zu unterstützen. In Cassandra fehlt jedoch die Unterstützung mit kleineren Ausnahmen weitgehend. Deshalb ist es eine Best Practice, die Datenablage an den späteren Lesefällen auszurichten.

Im System werden die Daten ex ante in vielen Varianten in der Datenbank abgelegt, um alle späteren Leseanforderungen zu bedienen:

Der Nachteil dieses Vorgehens ist, dass die semantische Konsistenz vollständig auf Seiten des Systems sichergestellt werden muss. In der Praxis hat sich dieses aber als gangbar erwiesen.

Ersatz für Transaktionen

Im Kontext des hier besprochenen Systems haben wir das Glück, dass das Fehlen von Transaktionen keine besondere Hürde darstellt.

Im System werden die Nachrichten idempotent verarbeitet. Das heißt, dass eine weitere Verarbeitung derselben Nachricht nicht zu anderen Ergebnissen führt als bei der vorangegangenen. Der Mehrfach-Versand von Nachrichten in geringem Umfang ist unproblematisch.

Bei den Eingangskanälen handelt es sich nicht um transaktionale Quellen, ebenso wenig bei den Ausgangskanälen. Das System stellt wie folgt sicher, dass keine Nachricht verloren geht: Nach der vollständigen Verarbeitung der Nachrichten werden diese in der Datenbank gespeichert. Erst nach der erfolgreichen Speicherung erfolgt das Entfernen der Nachricht im jeweiligen Eingangskanal. Der Ablauf wird am Beispiel einer über MQ empfangenen Nachricht erläutert:

Nachdem die Nachricht im System eingetroffen ist, wird sie zunächst vollständig verarbeitet. Im Anschluss werden die Ausgangsnachrichten versendet. Parallel dazu werden die Daten der Eingangsnachricht in die betreffenden Tabellen geschrieben. Das Schreiben der Daten erfolgt ebenfalls parallelisiert. Cassandra bietet eine sehr einfach zu verwendende Möglichkeit, sehr viele Operationen asynchron und parallel durchzuführen. Solche Möglichkeiten fehlen unserer Kenntnis nach bei allen relationalen Datenbanken, welche ein sequentielles Aufrufprotokoll aufweisen. Nachdem die letzte Bestätigung der Speicherung erfolgt ist, quittiert das System die Nachricht beim MQ Server als empfangen. Dieser gesamte Vorgang dauert nur wenige Millisekunden.

Der nebenläufige Versand der Ausgangsnachricht erzeugt zu einem beliebigen anderen Zeitpunkt eine Bestätigung des Versandes der Nachricht. Diese Bestätigung wird dann ebenfalls in der Datenbank festgehalten.

Sollte das System während der Verarbeitung oder Speicherung der Nachricht nach dem Empfang abstürzen, wird dieselbe Nachricht erneut verarbeitet. Wie zuvor erwähnt ist das unproblematisch. Sollte das System nach dem Versand aber vor der Bestätigung des Versandes abstürzen, wird die Ausgangsnachricht erneut gesendet. Auch das ist unproblematisch.

Vorteile von nicht-relationaler Datenbank während der Entwicklung

Vereinfachte Datenmodellierung

  • kein "Impedance Mismatch" durch entfallende Abbildung (objektorientiert nach relational),
  • kein Bedarf für OR- oder RO Mapper (wie hibernate oder jooq) und
  • einfache Migration von Modellständen mit Java-Sprachmitteln.

Wegfall von Housekeeping

In Cassandra werden alle Daten ex ante mit einem Verfallsdatum versehen. Daher kann in den meisten Fällen auf ein explizites Löschen verzichtet werden.

Cassandra bietet insbesondere auch spezielle Kompaktierungsstrategien für zeitreihenartige Daten an, welche auch eine Partitionierung nach zeitlichen Kriterien erlauben. Allerdings ist dabei wichtig, dass es sich der Natur nach um Write-Once-Daten handelt. Fast alle Bewegungsdaten des Systems fallen in diese Kategorie.

Steile Lernkurve

Aufgrund des geringen Funktionsumfangs von Cassandra im Vergleich zu relationalen Datenbanken wird keine langjährige Erfahrung benötigt, um das DBMS sicher zu bedienen. Wir haben zuvor viele Jahre mit relationalen Datenbanken gearbeitet und können deshalb im Vergleich sagen, dass es bei Letzteren viel mehr Fallstricke zu beachten gilt.

Natürlich haben wir im Projekt auch Fehler beim Umgang mit Cassandra gemacht, die mit mehr Erfahrung hätten vermieden werden können. Im Projekt waren das hauptsächlich zu große Partitionen, ungeeignete Kompaktierungsstrategien und fehlende Zeitsynchronisation der Cassandra-Knoten.

Performance

Die Schreibzugriffe bei Cassandra sind unabhängig von der Menge der verwalteten Daten schnell, weil Änderungen von Daten durch das Anhängen dieser Änderungen an die bestehenden Daten durchgeführt werden. Ebenso ist die Latenz von Lesezugriffen im Wesentlichen unabhängig von der Datenmenge, wobei natürlich Effekte schlechteren Cachings in den jeweiligen Puffern mit zunehmenden Datenmengen auftreten können.

Die Laufzeit von Kompaktierungen nehmen mit der Größe der verwalteten Daten zu. An dieser Stelle sind Performance-Engpässe konstruierbar, im Projekt aber nicht aufgetreten. Grundsätzlich skaliert Cassandra linear mit der Anzahl der Knoten.

Die nachfolgende Abb. zeigt die Performance des Systems für die Zeitspanne von Empfang der Nachricht bis zur vollständigen Speicherung, sowie vom Empfang der Nachricht bis zum Versand der Nachrichten inklusive Empfangsbestätigung:

80 Prozent der eingehenden Nachrichten werden innerhalb von 8 Millisekunden verarbeitet. Vom Empfangszeitpunkt bis zum Versand vergehen im Median je nach Ausgangskanal zwischen 10 und 30 Millisekunden.

Betriebsaspekte

Die Entscheidung, Cassandra im System einzusetzen, war hauptsächlich durch Erwägungen betrieblicher Aspekte bestimmt. Der Kunde hatte keine eigenen Präferenzen. Gleichzeitig war klar, dass wir einen größeren Teil des Betriebes abdecken würden, als wir es gewohnt sind. Zentral sind hier die Verfügbarkeit, der Versionshub, das Backup und die Systempflege.

Verfügbarkeit

Die Verfügbarkeit eines Cassandra-Clusters ist sehr hoch. Durch die freie Wahl der Redundanz der Daten kann eine beliebige Verfügbarkeit realisiert werden. Natürlich, wenn man Kostenerwägungen außer Acht lässt. Im Projekt verwenden wir eine Redundanz von drei, sodass zwei Knoten ausfallen können, ohne dass die Daten nicht mehr verfügbar sind. Bei Cassandra gibt es keinen Master, so dass beim Ausfall eines Knotens nur minimale kurzzeitige Verzögerungen in der Verarbeitung auftreten.

Versionshub

Durch die inhärente Redundanz ist ein Versionshub ohne Downtime problemlos möglich. Wartungsfenster sind nicht erforderlich. Für eine Übergangszeit kann ein Verbund von Cassandra-Knoten mit unterschiedlichen Versionsständen betrieben werden.

Backup

Cassandra legt durch die Redundanz inhärent Kopien aller Daten ab. Diese Kopien sind so gesehen bereits Backups. Darüber hinaus können Snapshots erstellt werden, die einen bestimmten Datenstand konservieren. Diese Snapshots sind über hard links realisiert und die Erstellung dauert quasi keine Zeit. Die Snapshots ihrerseits können nach freiem Ermessen auf andere Medien kopiert werden.

Systempflege

Bei relationalen Datenbanksystemen entstehen durch Updates und Deletes "in-place" Löcher in den Datendateien. Diese Löcher müssen häufig gefüllt werden. Dazu bieten RDBMS oft eigene Tools (z. B. Vacuum bei PostgreSQL).

Bei Cassandra ist die Pflege der Datendateien zentraler Bestandteil des Systems und findet während der Kompaktierung statt. Bei der Kompaktierung werden die Daten mehrerer Datendateien zusammengefasst und neu gespeichert. Diese Kompaktierungen laufen kontinuierlich ab, ohne dass ein administrativer Eingriff erforderlich ist.

Durch den Wegfall von Housekeeping und der automatischen Kompaktierung entstehen keine Aufwände im Betrieb.

Weitere fehlende Features

Cassandra bietet von Haus aus keine Möglichkeiten, analytische Abfragen über große Datenmengen hinweg durchzuführen. Solche Abfragen werden häufig für DWH-Aufgaben und -Reports benötigt. Ebenso sind Ad-hoc-Abfragen durch Benutzer nicht vorgesehen.

Im Projekt haben wir zusätzlich zu Cassandra auch Spark und SOLR eingesetzt, die als Aufsatz diese Lücken schließen. Spark dient dabei der Abbildung großer Joins und Aggregate. Im Projekt sind mit Spark die Aufbereitung der Daten für Kunden-Dashboard und die Ermöglichung von Ad-hoc-Abfragen durch Mitarbeiter des Kunden realisiert. SOLR ist ein Tool zur Erstellung von Volltext-Indizes. Im Projekt wird SOLR zur Recherche von Nachrichten verwendet.

Der Einsatz von Spark über Cassandra ist relativ einfach zu bewerkstelligen. Dazu gibt es eigene Treiber, die Daten aus Cassandra-Quellen für die Verarbeitung mittels Spark bereitstellen.

Spark erlaubt die Formulierung von SQL-Queries oder auch die programmatische Aufbereitung der Daten. Spark SQL Queries sehen dann beispielsweise so aus:

SELECT inbound.receivedat, inbound.format, nvl(fhl.awbnumber,nvl(fwb.awbnumber,
    concat_ws('',inbound.awbconsignmentidentification))) awbnumber,
    nvl(fhl.origin,nvl(fwb.origin,'unknown')) origin,
    concat_ws(',',fwb.stopovers) stopovers,
    nvl(fhl.destination,nvl(fwb.destination,'unknown')) destination,
    concat_ws(',',nvl(fhl.senders,fwb.senders)) sender,
    concat_ws(',',nvl(fhl.recipients,fwb.recipients)) recipient,
    array_contains(inbound.finalrecipients, 'REUAIR08HVN') forwarded
FROM inboundmessage inbound
left outer join fwbinvestigation fwb on (fwb.inboundmessageid = inbound.inboundmessageid)
left outer join fhlinvestigation fhl on (fhl.inboundmessageid = inbound.inboundmessageid)
where inbound.solr_query=
      'receivedat:[2019-01-01T00:00:00Z TO 2019-04-01T00:00:00Z] AND ' ||
      'originalrecipient:*REUAIR08HVN* AND (format:*FWB* OR format:*FHL*)'

Programmatische Abfragen können mittels Java oder Scala formuliert werden. In diesem Fall steht auch der vollständige Funktionsumfang der Java-Plattform zur Verfügung.

Der Einsatz von SOLR in Cassandra ist dagegen ein wenig aufwändiger, weil das Auslaufen der Gültigkeit von Daten besonders berücksichtigt werden muss. Im Projekt haben wir die kostenpflichtige Version DSE des Herstellers Datastax eingesetzt, die bereits eine Integration von SOLR enthält.

Autor

Roland Scheel

Roland Scheel ist Mitgründer der SCOOP Software. Seine Schwerpunkte sind High-Performance-Anwendungen und Datenbanksysteme.
>> Weiterlesen
Das könnte Sie auch interessieren
Kommentare (0)

Neuen Kommentar schreiben