Microsoft SQL-Server ab 2017 auch für Linux

English

Microsoft hat angekündigt, den MS-SQL-Server auch für Linux anzubieten.

Ja, die Zeit ist reif dafür. Es gibt genug Gründe, Vorbehalte gegen die Firmen Microsoft und Oracle zu haben, aber deren DB-Produkte sind gut und wenn es mehr Konkurrenz gibt, ist das auch gut. Ich denke, dass Oracle noch etwas besser ist, aber andererseits ist es nur ein sehr kleiner Teil der Projekte, bei denen es auf diesen Unterschied ankommt. Weiterhin stehen natürlich noch mit DB2, MariaDB und vor allem PostgreSQL noch interessante DB-Produkte zur Verfüng, auch unter Linux.

Links:
* Informatik Aktuell
* offizielle Ankündigung auf dem MS-Blog

Share Button

Transaktionsisolation

English

Dieser Artikel basiert auf einem Vortrag, den ich bei der Ruby on Rails User Group Schweiz in Zürich gehalten habe.

Naiver Zugang zu Transaktionen

Alle reden von Transaktionen, aber wer kennt sich wirklich gut damit aus?
Ist es vielleicht wie beim Multithreading, wo man sehr viele Leute findet, die behaupten, sich gut auszukennen, aber sehr wenige Multithreading-Programme, die wirklich unter Last auch korrekt laufen?

Immerhin haben wir so eine ganz brauchbare Vorstellung:

  • Wir beginnen implizit oder explizit eine Transaktion
  • Wir führen eine oder mehrere Operationen durch (z.B. SQL-Befehle)
  • Wir beschließen am Ende entweder ein „roll-back“ oder „commit“
  • Roll-back stellt den Zustand von vor der Transaktion wieder her
  • Commit akzeptiert die Änderungen definitiv

Das ist doch überschaubar. Warum brauchen wir da noch komplizierten Kram?

Wenn wir uns in einer einfachen Welt bewegen, wo nur ein DB-User mit einer Session auf der Datenbank ist.

Die komplizierte Welt

In einer komplizierten Welt finden leider parallele Aktivitäten statt.
Mehrere Threads, Prozesse oder auch externe Datenbank-Clients greifen auf die Datenbank zu.

Was machst das so kompliziert?
Wenn Dinge parallel werden, dann ist jeder Entwickler gut darin, das im Griff zu haben, aber komischerweise funktioniert es dann meistens gar nicht oder zumindest nur auf dem Laptop des Entwicklers, nicht aber langfristig auf dem Server unter Last.
Aber die Leute, die Datenbankprodukte entwickeln, sind wirklich gut. Datenbanken laufen mit vielen parallelen Zugriffen unter Last auf Millionen von Rechner und funktionieren dabei noch recht zuverlässig.
Nun haben wir aber ein paar zusätzliche Fragen:

  • Was passiert mit unvollständigen Transaktionen vor dem Commit?
  • Was passiert mit erfolgreich abgeschlossenen Transaktionen während eines langen SELECTs oder einer langen lesenden Transaktion?
  • Dasselbe betrifft auch lange Transaktionen, die Daten verändern, aber dabei auch lesend zugreifen.

Eine Naive Anforderung wäre, dass andere Clients die Transaktion entweder ganz oder gar nicht sehen. Reciht das?
Man stelle sich ein Problem vor, das in einer langweiligen Informatik I-Vorlesung in meinem Studium real war:

  • Die Vorlesung geht 90 min ohne Pause.
  • Der „Hörsal“ ist eine Maschinenhalle, die man provisorisch mit Stühlen ausgestattet hat.
  • Etwa 600 Leute sind anwesend.
  • Es gehen dauernd Leute raus.
  • Am Schluss der Vorlesung ist der Hörsal immer noch ziemlich voll.
  • Wie viele Leute sind zu einer bestimmten Zeit in dem Hörsal?
  • Eine präzise und korrekte Zahl existiert immer, denn so brutale Sachen wie halbe Studenten waren damals nicht üblich.
  • Aber das Zählen ist schwierig, weil während dem Zählen dauernd Leute den Raum verlassen.
  • Merke: Das Verlassen des Hörsals ist eine Transaktion. Nach dem Commit ist die betreffende Person draußen, vorher drin.

Da kommt nun das berühmte Snapshot too old ins Spiel, das einige von uns, die noch mit Oracle arbeiten, recht gut kennen. Oracle hat das Problem, die Hörer im Hörsaal zu zählen, nämlich gelöst. Und ähnliche Probleme auch. Das SELECT bei Oracle bekommt für seine Verarbeitungszeit einen konsistenten Datenstand, der auf dem Zustand beim Start der Abfrage basiert.
Transaktionen, die währenddessen erfolgreich mit einem Commit abgeschlossen werden, werden nicht berücksichtigt. Das wird „Snapshot“ genannt. Sehr lange laufende SELECTs, die auf Daten zugreifen, die sich häufig ändern, scheitern manchmal mit „Snapshot too old“.

Präzisierung

Diese Konzepte werden jetzt etwas präzisiert, aber die Details nachzulesen sprengt sicher den Rahmen eines Blog-Artikels.
Es geht um:

  • Anomalien (Read phenomena)
  • Transaction-Isolation-Levels
  • ACID

Anomalien bezeichnen ein unerwartetes oder unwillkommenes Verhalten beim Lesen.

„Dirty Read“ bedeutet, dass auf Daten zugegriffen werden kann, die in einer anderen Transaktion geändert werden, bevor sie committed worden sind. Das widerspricht jeder intuitiven Vorstellung von Transaktionen. Eine interessante Frage ist aber, ob die Transaktion, die die Daten verändert, bei ihren Zugriffen jeweils den Stand nach den eigenen Veränderungen sehen kann oder ob dies erst nach dem eigenen Commit möglich ist. Bei Oracle galt immer der erste Fall, außer es hat sich kürzlich geändert, was ich aber stark bezweifle.

„Non-repeatable read“ steht für Leseoperationen auf denselben Daten während einer Transaktion, die aufgrund in anderen Transaktionen vorgenommer Änderungen verschiedene Ergebnisse liefern. Das ist genau das Problem mit dem großen Hörsal. Oracle vermeidet das zum Beispiel standardmäßig.

„Phantom reads“ stehen für Leseoperationen, bei denen zwar Daten sich selbst nicht ändern können, aber durch DELETE und INSERT Datensätze wegfallen oder hinzukommen können.

Isolationslevel bezeichen eine Einstellung einer Datenbank und die Anforderungen dazu.

„Read Uncommited“ erlaubt „dirty read“. Mir ist kein Fall bekannt, in dem das tatsächlich so praktiziert wird. Diese Einstellung ist für parallel mehrfach verwendete Datenbanken kaum brauchbar. Man kann das konfigurieren und der Standard erlaubt Datenbanken „besser“ als das zu sein.

„Read Committed“ steht dafür, dass man nur Daten von vollständig abgeschlossenen Transaktionen sehen kann. Das bedeutet also, das „dirty read“ ausgeschlossen ist, aber „non-repeatable read“ möglich ist. Das ist die Grundeinstellung für viele Datenbanken, z.B. für PostgresSQL.

„Repeatable Reads“ steht für eine Einstellung, die die Daten gegenüber UPDATEs scheinbar konstant hält. Das unterbindet „non-repeatable-read“, aber nicht „phantom-read“.

Bei der Einstellung „Serializable“ werden alle Transaktionen komplett entflochten. Man bekommt das Verhalten als wären alle Transaktionen in einer Warteschlange (Queue) und es würde immer nur jeweils eine nach der anderen abgearbeitet, womit wir wieder am Anfang wären, aber das stimmt nicht ganz. Zunächst mal werden sogar phantom-reads unterbunden.

Reale Datenbanken

Reale-Datenbanken haben eine Standardeinstellung („default“) für die Transaktionsisolation, aber sie dürfen immer besser als der eingestellte Wert sein, ohne den Standard zu verletzen. Das gibt also die Möglichkeit, Mischformen zu implementieren. Oracle hat mit „read-only“ sogar einen zusätzlichen Wert.

Nun gibt es verschiedene Wege, so etwas zu implementieren, grob gesagt sind es Locking oder Multiversion. Beim Locking werden Datensätze, Bereich, Spalten, Tabellen oder ganze Schemata gelockt, also exklusiv von einer Transaktion in Anspruch genommen. Bei Multiversion werden verschiedene Kopien der Daten erstellt und jeweils mit diesen gearbeitet. Es bleibt eine Herausforderung, die Daten am Schluss zusammenzufügen.

Verschiedene Datenbankprodukte nutzen den Interpretationsspielraum des Standards recht gut aus. Z.B. ist Oracle natürlich der Standard und man muss nur so tun, als halte man sich an den SQL-Standard.

Ich empfehle am Anfang eines Projekts eine weise Entscheidung über die Wahl der Datenbank zu treffen. Gerade hier bei den Transaktions-Isolations-Levels zeigt sich einmal mehr, dass die verschiedenen Datenbanken nicht so gleich sind und deshalb der Astausch gegen ein anderes Produkt problematisch und teuer sein kann.

ACID

ACID ist das englische Wort für Säure. In der Chemie nennt man Substanzen mit einen pH-Wert < 7 Säuren.
Hier ist aber „ACID“ ein Acronym:

  • Atomicity
  • Consistency
  • Isolation
  • Durability

Ein paar Links

Share Button

MoSQL

Bei einem Meetup-Treffen in Zürich wurde ein neues Backend für mysql-Datenbanken vorgestellt, das die Skalierung erleichtern soll:
MoSQL.
Zur Zeit wird daran noch entwickelt, aber man kann Vorabversionen schon testen.

Nun stellt sich die Frage, ob man so etwas braucht, weil die meisten mysql-Installationen ja klein sind und für größere Datenbanken
PostgreSQL, Oracle, DB2 etc. existieren.
Dem kann man entgegensetzen, dass die wenigen großen mysql-Installationen eine besondere Bedeutung haben und deren Betreiber auch oft bereit sind, etwas zu investieren. Der Wechsel des Datenbankprodukts ist in der Praxis oft schwierig, deshalb bleibt man in der Regel bei dem DB-Produkt, mit dem man angefangen hat. Ein Argument am Anfang genau nachzudenken. Dummerweise werden kommerzielle DB-Systeme für richtig große Installationen oft richtig teuer. Nur die Einstiegsversionen sind günstig geworden.

Die andere Frage ist, warum man nicht auf die NoSQL-Schiene setzt. Das hängt vom Problem ab. Oft kann man NoSQL und SQL gut kombinieren für verschiedene Aufgaben. SQ_L als Abfragesprache ist recht mächtig und weit verbreitet und hat deshalb einen gewissen Reiz. NoSQL-Datenbanken haben oft „viel einfachere“ Abfragesprachen, wobei es Geschmackssache ist, ob die wirklich einfacher sind und wobei sich die Frage stellt, wie einfach komplizierte Dinge sind, die mit SQL zumindest möglich sind. Die andere Frage ist die Transaktionalität, die bei SQL-DB-Architektueren allerdings auch oft verwäsert wird, vor allem durch Caching. Gerade bei MySQL mit Clustering muss man hier aufpassen und auch bei MoSQL ist das ein Thema, wenn zueinander inkompatible Transaktionen parallel laufen. Durch die massive Parallelisierbarkeit kann so etwas passieren und dann muss eine Transaktion scheitern. Bei sequentieller Abarbeitung der Transaktionen (Modus SERIALZE) wäre das nicht passiert, aber man bekäme auch nicht die Performance. Umgekehrt können viele NoSQL-Datenbanken Transaktionen. Oft nur einfache oder Kombinationen aus zwei Statements, aber wer es wirklich wüst will kann die mit Two-Phase-Commit als „verteilte Transaktion“ kombinieren und hat dann sogennante „volle Transaktionalität“. Nur ist two-phase-commit nicht so wasserdicht wie ein lokales Commit….

Share Button

Datenbankmigrationen

Fast jede größere Software, mindestens serverseitige Software, benutzt Datenbanken. Früher war dies „immer“ Oracle, außer man war in einer sehr Mainframe-lastigen Umgebung, dann war es halt DB2, oder in einer sehr Microsoft-lastigen Umgebung, dann war es MS-SQL-Server.

Das scheint sich jetzt ein bißchen zu ändern. Oracle scheint viele Kunden mit seiner Lizenzpolitik verärgert zu haben und andererseits sind die Alternativen PostgreSQL und verschiedene mySQL-Derivate (z.B. MariaDB) inzwischen auf einem guten Stand, der es erlaubt, viele Aufgaben, für die man früher selbstverständlich auf Oracle-Datenbanken gesetzt hat, damit zu lösen, auch für größere Datenbanken. Mir scheint es immer noch so zu sein, dass man mit mySQL oder MariaDB an Grenzen stößt, und für viele Zwecke PostgreSQL die bessere Wahl ist.

Andererseits kommen auch vermehrt die sogenannten NoSQL-Datenbanken auf, z.B. Riak, Neo4J, MongoDB, Redis und etliche mehr.

Mit der Weiterentwicklung der Software gehen irgendwann auch Änderungen des Datenmodells einher. In der klassischen Welt der SQL-Datenbanken führt man dann eine Migration durch, wo neue Tabellen hinzugefügt werden, vorhandene geändert werden und Daten transformiert werden. Theoretisch geht das zumindest bei kleineren Änderungen, ohne die Applikation abzuschalten, aber für größere Änderungen fährt man doch gerne sicherheitshalber die Applikation so lange herunter, wenn man die Möglichkeit hat, und sei es nur, damit die Migration schneller fertig wird. Idealerweise wird die Software dann natürlich im gleichen Atemzug auch umgestellt und wenn danach alles gut läuft, ist es gut. Wenn man es merkt, dass etwas nicht stimmt, bevor die neue Software Daten angelegt hat, kann man einfach das Backup einspielen und es in der nächsten Nacht noch einmal probieren. Schlimmer ist der Fall, wenn man es erst nach einer Weile merkt, dass die Applikation nicht mehr rund läuft und wenn man dann schon Daten verloren hat oder zumindest in einem Zustand hat, der es nur mit großer Mühe erlaubt, das zu korrigieren. Deshalb empfielt es sich, so eine Migration gut zu testen und zwar nicht nur mit leeren Datenbanken, sondern mit richtigen Daten. Sinnvoll sind anonymisierte Daten aus der Produktivdatenbank.

Ein völlig anderer Ansatz drängt sich bei schemalosen Datenbanken wie MongoDB auf. Man kann bei einer Änderung der Software einfach alle Daten so lassen, wie sie sind, vorausgesetzt, die Software ist in der Lage alte und neue Daten zu unterscheiden. Das kann sich am vorhandensein eines Attributs erkennen lassen, aber auch an einem Versionsattribut. Wenn nun ein Dokument, so heißen die „Datensätze“ in MongoDB, verwendet wird, kann die Software in diesem Moment die Migration mit geeigneten Defaultwerten oder berechneten Werten für neu hinzukommenden Felder für das eine Dokument durchführen. So kann die Migration unter Umständen Tage oder sogar Jahre dauern, aber die Applikation kann abgesehen von der Problematik des eigentlichen Software-Updates durchgängig laufen.

Grundsätzlich ließe sich dieser Ansatz auch für SQL-Datenbanken denken. Man kann zum Beispiel bei der Migration eine leere Tabelle anlegen, die die migrierten Daten einer vorhandenen Tabelle aufnehmen kann und die Daten so nach und nach durch die Applikation transformieren und in die neue Tabelle verschieben. In der Praxis wird das aber kompliziert, wenn man noch so Dinge wie referenzielle Integrität berücksichtigen will. So wird uns der bewährte klassische Ansatz für Datenbankmigrationen wohl noch lange erhalten bleiben.

Share Button

Datenbanken und unveränderliche Objekte

English

Ein beliebter Ansatz in der funktionalen Programmierung, aber auch teilweise in der objektorientierten Programmierung ist es, Objekte nach Möglichkeit unveränderlich (engl. „immutable“) zu machen.

Speziell für Applikationen mit Multithreading ist das sehr nützlich, aber grundsätzlich erleichtert es auch im Griff zu haben, wie der Informationsfluss ist und unerwartete Seiteneffekte zu verhindern. Ich will das hier nicht als Dogma postulieren, denn es gibt durchaus legitime Verwendungen von veränderlichen Objekten, aber man sollte sich auf jeden Fall bewusst sein, was das bedeutet und ob sich die Veränderbarkeit (engl. „mutability“) in dem Fall überhaupt lohnt.

Unterscheiden muss man noch die echt unveränderlichen Objekte und diejenigen, die nur durch einen Wrapper oder ein zweites Interface unveränderlich weitergegeben werden, obwohl es noch für einen Teil des Codes Zugriff auf ein Interface gibt, das Veränderungen zulässt. Der typische Fall sind zum Beispiel Collections in Java, die in einem Objekt leben und mit Methoden dieses Objekts verändert werden können, aber die nur als immutable gewrappt herausgegeben werden.

Beispiel:

import java.util.*;

public class A {
    List l = new ArrayList();
    
    public void addStr(String s) {
        l.add(s);
    }

    public List getList() {
        return Collections.unmodifiableList(l);
    }
}

Obwohl also die mit getList() herausgegebenen Liste selbst immutable ist, kann sie mittels add aus A noch verändert werden.

Interessant wird es aber nun, wenn man eine Datenbank ins Spiel bringt.
Datenbank-Inhalte werden in irgendeiner Weise auf Objekte abgebildet. Ob das nun mit Automatismen passiert oder explizit mit Mechanismen wie JDBC, DBI o.ä. und ob es spezifische Objekte oder generische universelle DB-Content-Objekte oder Collections sind, spielt keine so große Rolle, es gibt solche Objekte oder Datenstrukturen.

Nun können sich Datenbankinhalte ändern. Das kann durch die Software, auf die wir uns hier beziehen, selbst passieren, aber auch von außen. Allein das ist schon eine gewisse Komplikation, wenn man es genau anschaut. Die Objekte, mit denen sich die Software befasst, sind dann nämlich schon veraltet. Das kann so akzeptabel sein, wenn man darauf verzichtet, auf Software- oder Frameworkebene ein Caching von solchen Objekten aufzubauen und das Caching allein der Datenbank überlässt, die das im Gegensatz zu Frameworks oder anderer Software, die die Datenbank nur nutzt, problemlos transaktionskonform kann. Schön ist es dann natürlich wieder, wenn die DB netzwerkmäßig nicht zu weit von der Applikation weg ist, sonst ist der Cache leider auf der falschen Seite der Netzwerkverbindung…

Wenn also in der Software die Objekte so kurzlebig sind, dass man ausschließen kann, dass sie veralten, dann kann man sagen, dass sie hinreichend mit dem Stand in der Datenbank übereinstimmen. Mit Transaktionsisolation „serializable“ oder „phantom-read“ und dann innerhalb von einer Transaktion sind diese Objekte, die man in derselben Transaktion gelesen hat, garantiert noch aktuell. Wenn nicht bestimmte OR-mapping-Muster für Collections vorkommen, ist das schon bei „repeatable-read“ der Fall. Man lebt also gut mit „immutable“-Objekten, solange diese nur aus der Datenbank gelesen (SELECT oder READ) werden. Auch das Löschen (DELETE) kann man sich noch vorstellen, aber in diesem Fall muss man schon darauf achten, dass das betreffende Objekt nicht noch an zu vielen Stellen der Software in Gebrauch ist, obwohl es in der Datenbank schon gelöscht ist. Wenn man aber sowieso sicherstellt, dass diese Objekte, die Datenbankinhalte darstellen, nicht länger leben als die entsprechenden Transaktionen, dann sollte auch das handhabbar sein. Auch das Erzeugen neuer Objekte und das Anlegen der entsprechenden Daten in der Datenbank (INSERT oder CREATE) ist möglich.

Schwierig wird es aber mit dem Ändern von Daten in der Datenbank (UPDATE). Bei gängigen OR-Mappings funktioniert das so, dass man das entsprechende vorher gelesen Objekt ändert und dann speichert, in der Hoffnung, dass das Framework die Unterschiede herausfindet und ein entsprechendes Update, eventuell auch eine Kombination aus Updates, Inserts und Deletes initiiert. Das wird mit unveränderbaren (immutable) Objekten schwierig. Man kann auch da Wege finden, aber das geht dann leicht auf Kosten der Eleganz des Frameworks und man hat sich auf jeden Fall damit auseinanderzusetzen, dass die Objekte jetzt auch innerhalb der Transaktion veralten und durch solche mit den aktualisierten Inhalten ersetzt werden müssen.

Die provokante Frage ist, ob man das Update überhaupt braucht. Natürlich braucht man das, aber auch hier kann man genauer schauen, welches die Fälle sind, wo das wirklich so ist und wann andere Ansätze besser sind.
Ein klassisches Beispiel ist eine Software, die irgendwelche Buchungen und irgendwelche Personen verwaltet. Das kann eine Bankensoftware sein, wo man Konten hat. Oder ein Billingsystem einer Telefongesellschaft, wo man die Rechnungen für Kunden generiert und verwaltet. Man hat dort oft so eine Buchungstabelle, in der grundsätzlich nur Datensätze eingefügt werden und niemals Daten gelöscht oder verändert werden, wenn wir einmal Fragen der Archivierung sehr alter Daten ausklammern. Dazu hätte man gerne noch einen Kontostand. Den kann man strenggenommen immer berechnen, indem man einfach alle Buchungen für das Konto aufaddiert. Leider ist die Software dann nachher langsam und für keinerlei praktische Zwecke brauchbar, aber theoretisch sehr schön. Man braucht also schon einen effizienten Mechanismus, um Kontostände zu ermitteln. Vielleicht kann jede Buchung den aus ihr resultierenden Kontostand beinhalten und man muss nur nach der neuesten Buchung suchen. Das ist gefährlich, weil zwei etwa gleichzeitig erzeugte Buchungen sich auf denselben Vorgängerkontostand beziehen. Mit Transaction-Isolation „serializable“ wäre das nicht passiert, aber das bremst natürlich die Datenbank schon sehr aus, ausgerechnet bei der größten Tabelle, in der die ganze Arbeit läuft und wo es sowieso schon den Performance-Flaschenhals gibt. Man kann auch auf Datenbankebene eine Lösung implementieren, etwa mit Triggern oder Materialized Views, wo jedes Insert in der Buchungstabelle ein Update des entsprechenden Kontostands bewirkt. Das hat den Vorteil, dass beliebige DB-Zugriffe von beliebiger Software richtig verarbeitet werden, solange man in der betreffenden Buchungstabelle Delete- und Update-Zugriffe unterbindet. Der Nachteil ist aber, dass die Businesslogik sich jetzt auf die Datenbank und die Applikation verteilt. Das kann man akzeptieren, wenn man das als eine Art abstrakten Constraint ansieht, der von der Datenbank „aktiv“ eingehalten wird. Oder man kann es vermeiden, indem man auf Applikationsebene eine entsprechend Funktionalität zum Einfügen einer Buchung einführt, die den Kontostand anpasst. Wiederum wird das zu falschen Ergebnissen führen, wenn man erlaubt, dass eine zweite Buchung für dasselbe Konto eingefügt wird, während schon eine Transaktion für das Einfügen einer Buchung für das Konto läuft. Man sieht also, dass die richtige Lösung dieser Buchungstabelle nicht einfach ist. Wenn dann noch hinzukommt, dass die Buchungen wiederum zu Transaktionen gruppiert sind, also z.B. die Gutschrift auf einem Konto in derselben Transaktion wie die Abbuchung von einem anderen Konto laufen soll, dann wird es schon interessant, wie man das System so baut, dass es absolut zuverlässig und korrekt ist, auch unter Last, und doch auch performant ist. Und in der Applikation muss man nun mit Konto-Objekten, die den Kontostand enthalten, sehr vorsichtig sein, weil dieser Kontostand veraltet, sobald eine Buchung dazukommt.

Die Tabelle oder die Tabellen mit den Personen, Adressen werden gerne mit Updates aktualisiert, wenn sich Daten ändern. Leute heiraten, ändern die Telefonnummer oder ziehen um. Aber ist das wirklich der richtige Weg? In Wirklichkeit weiß man von dem Umzug schon etwas vorher und hat vielleicht am Umzugstag selbst keine Zeit, das zu erfassen. Und in Wirklichkeit muss das System auch Fragen beantworten können, wie den Namen und die Adresse des Kunden zu der Zeit, als Buchung 23571113a stattfand. Vielleicht kann man Adressänderungen auch als „Buchungen“ speichern, mit einem Gültigkeitsbeginn. Dann wird die Adresse für einen Kunden ermittelt, indem man den Adresseintrag für die Kundennummer sucht, der das größtmögliche Gültigkeitsdatum hat, das gerade noch nicht in der Zukunft liegt. Mit dem Ansatz muss man in dieser Tabelle vielleicht tatsächlich nur neue Datensätze einfügen, wenn sich etwas geändert hat. Und wenn ein Kunde gelöscht wird, dann fügt man einen Datensatz ein, der beinhaltet, dass der Kunde ab einem bestimmten Datum „gelöscht“ ist, aber man kann noch bei Bedarf Daten liefern.

Das Schöne ist nun aber, dass Daten, die in der Datenbank „immutable“ sind, also keine Updates erhalten dürfen, in der Software auch problemlos immutable sein können (und sollten) und dass man sie auch langlebiger als eine Transaktion machen kann. Vorsicht ist aber immer geboten, weil komplexe Objekte in der Applikation sich auch allein durch Inserts in einer Datenbanktabelle ändern würden, wenn etwa eine Collection enthalten ist. Man muss also das OR-Mapping genau kennen, egal wie faszinierend diese Automatismen auch sein mögen. Im Fall von einer Kundenadresse muss man die gültige Adresse immer in Verbindung mit einem Timestamp behandeln, der beim ermitteln der gültigen Adresse eingesetzt wurde. Wenn dieser Timestamp veraltet ist, muss man auch die Adresse neu abfragen, wobei es im Gegensatz zu den Buchungen die Adressänderungen selten auf die Mikrosekunde des Umzugszeitpunkts ankommt, solange man die Konsistenz wahrt und nicht in der Woche des Umzugs wilde Mischungen aus der alten und der neuen Adresse verwendet.

Share Button

Snapshot too old – Behandlung langlaufender SELECTs

Wer größere Datenbank-Applikationen entwickelt, wird sich mit dem Problem auseinandersetzen müssen, was bei langlaufenden Abfragen eigentlich passiert.

Man hat also ein „SELECT“ am laufen, dass mehrere Sekunden oder sogar Minuten dauert, vielleicht sogar eine Stunde. Das kann durchaus sinnvoll sein, aber es lohnt sich natürlich, dieses mit gutem Wissen über die Datenbank-Software genau anzuschauen und zu optimieren.

Trotz aller Optimierungen muss man aber prinzipiell davon ausgehen, dass sich die Daten während des Lesezugriffs ändern. Es finden dauernd Schreibzugriffe statt, die natürlich erst nach Abschluss der Transaktion mit einem „COMMIT“ wirklich sichtbar werden. Nun können aber während des Lesezugriffs jede Menge Transaktionen stattfinden. Würde man diese berücksichtigen, müsste der Lesezugriff dauernd von vorne anfangen oder er würde inkonsistente Daten zurückliefern, die von verschiedenen Ständen der Datenbank stammen.

Die Lösung ist, den Stand der Datenbank vom Beginn des Lesezugriffs quasi einzufrieren und für die Dauer des Lesezugriffs zur Verfügung zu stellen. Zugriffe, die später beginnen, bekommen einen anderen Stand zu sehen, auch wenn sie früher fertig werden. Im Fall von Oracle-Datenbanken werden hierfür sogenannte Snapshots verwendet. Es werden also ähnliche Mechanismen wie für Transaktionen verwendet, um mehrere Stände der Datenbank parallel zu speichern.

Das bedeutet aber, dass bei einer sehr aktiven Datenbank immer größere Unterschiede zwischen den beiden Ständen entstehen. Bei vielen Transaktionen oder vielen parallelen langlaufenden Lesezugriffen sind sogar oft viel mehr als zwei Stände. Dafür gibt es Bereiche in der Datenbank, sogenannte Rollback-Segmente. Diese muss man definieren und bereitstellen und wenn sie zu klein sind, laufen sie irgendwann über. Dann kommt es im Fall von Oracle zu dem Fehler „ORA-01555: Snapshot too old“ und der lange Lesezugriff scheitert. Wenn man Pech hat nach ein paar Stunden. Wenn es ein Zugriff durch eine Software ist, wird es interessant, ob die Fehlerbehandlung für diesen Fall funktioniert oder ob es eine obskure „Exception“ in der Log-Datei gibt, die niemand bemerkt.

Mehr dazu findet man unter anderem hier:

Grundsätzlich muss diese Fragestellung bei allen ernsthaften transaktionalen Datenbanken gelöst werden und es ist sicher interessant, wie das im Detail aussieht. Vielleicht ist bei anderen Datenbanken so etwas wie eine „Lese-Transaktion“ („read transaction“) die Antwort. Als Isolationslevel muss man mindestens „repeatable read“ oder „serializable“ wählen. Wenn man mit niedrigeren Isolationsleveln arbeitet, muss man genau wissen, was man tut, da es sonst überraschende Fehler gibt.

Bei PostgreSQL muss man das mit
BEGIN TRANSACTION ISOLATION LEVEL REPEATABLE READ;
oder
BEGIN TRANSACTION ISOLATION LEVEL SERIALIZABLE;
explizit einstellen, sonst wird „READ COMMITTED“ gewählt.

Bei mariaDB und mysql hängt es von der DB-Engine ab. InnoDB verwendet als Default „REPEATABLE READ“, DB2 scheint eher auf „READ COMMITTED“ zu setzen, MS-SQL-Server auch auf READ COMMITTED.

Höhere Isolation-Level bedingen natürlich auch das Risiko von Verklemmungen („Deadlocks“), was erkannt werden sollte und zum Rollback einer der beiden Transaktionen führen sollte.

Es lohnt sich also, bei anspruchsvollen DB-Applikationen genau zu schauen, wie man mit diesen Transaktion-Isolationen umgehen will und ob man mit den Default-Eintstellungen der verwendeten Datenbank-Software zufrieden ist.

Vor einigen Jahren habe ich übrigens feststellen müssen, dass man diese niedrigeren Isolationslevel bei Oracle wohl einstellen konnte, dass die Datenbank oder der JDBC-Treiber sich dann aber fehlerhaft verhalten haben. Man sollte also auch herausfinden, ob die entsprechende Eintstellung von der verwendeten Datenbanksoftware unterstützt wird und zwar nicht nur auf dem Papier, sondern real und absolut zuverlässig. Eine unzuverlässige transaktionale Datenbank kann man billig durch einen Zufallszahlengenerator ersetzen.

Share Button

Warum Baumstruktur

Für Dateiverzeichnisse (Filesysteme) hat sich eine Baumstruktur etabliert. Wir haben uns daran gewöhnt und für die meisten Zwecke ist das auch eine sinnvolle Strukturierung.

Relativ oft wird man aber mit dem Problem konfrontiert, daß es zwei oder mehr Aspekte gibt, nach denen man seine Dateien oder Unterverzeichnisse gruppieren will. Machen wir es mal abstrakt mit Farbe, Form und Größe.

Dann kann man etwa so etwas haben wie

$MAINDIR/rot
$MAINDIR/blau
$MAINDIR/gruen
$MAINDIR/gelb

und in der nächsten Ebene dann jeweils so etwas wie

$MAINDIR/rot/quadrat
$MAINDIR/rot/kreis
$MAINDIR/rot/dreieck
$MAINDIR/rot/sechseck
$MAINDIR/blau/quadrat
$MAINDIR/blau/kreis
...
$MAINDIR/gelb/dreieck
$MAINDIR/gelb/sechseck

und darunter jeweils

.../klein
.../mittel
.../gross

Aber manchmal möchte man es genau anders haben haben, also zuerst nach Form differenzieren, dann nach Größe und zuletzt nach Farbe.
Und es kann oft sehr unpraktisch sein, wenn Dinge, die eigentlich zusammengehören und die man zusammen bearbeiten will, verstreut über das Verzeichnissystem herumliegen.

Es hilft sicher, daß man sich vorher gut überlegt, wie man seine Verzeichnisstruktur aufbaut, so daß es zumindest in möglichst vielen Situationen einigermaßen paßt. Es gibt sicher Möglichkeiten, damit umzugehen. Man könnte zum Beispiel alle sechs Verzeichnisbäume anlegen und via Hardlinks oder Softlinks die Dateien, die man im ersten Verzeichnisbaum richtig eingetragen hat, von den anderen Verzeichnisbäumen jeweils passend verlinken (Softlink) oder dort auch eintragen (Hardlink). Das geht unter Linux und Unix mit deren eigenen Dateisystemen gut, es kann aber eine Herausforderung sein, das konsistent zu halten.

Man kann auch die Daten auf Dateisystemebene irgendwie ablegen, vielleicht sogar in einer der möglichen Strukturierungen, aber den Zugriff über eine Applikation anbieten, die es ermöglicht, so darauf zuzugreifen, als wären sie in einer der anderen fünf Arten strukturiert. Ein typisches Beispiel sind Musikprogramme (Player), die es ermöglichen, die Musikstücke nach Genre, Sänger, und vielen anderen Kriterien zu strukturieren und dann mit diesen Daten darauf zuzugreifen. Es gibt dann typischerweise irgendwo Metadaten, die diese Zusatzinformationen enthalten. Hier hat man noch den Zugriff über das Dateisystem, was sehr praktisch ist, gerade bei Musik, aber die Applikation muß Änderungen erkennen und gegebenenfalls die Metadaten anpassen oder sogar interaktiv abfragen und ergänzen.

Noch weiter geht es, wenn die Datenspeicherung völlig intransparent ist, im Extremfall sogar unter Benutzung sogenannter „Raw-Devices“, und wenn man dann nur noch über so eine Applikation an die Daten herankommt. Im Grunde genommen sind alle Datenbanken in dieser Rubrik angesiedelt, aber auch die Repositories vieler Versionsverwaltungssysteme oder auch Content-Management-Systeme, die natürlich wiederum eine Datenbank verwenden können. Eine Folge davon ist, daß man an diese Daten nur noch mit der Applikation oder deren Hilfsprogrammen herankommt und nicht mehr wirklich auf Dateisystemebene damit arbeiten kann. Eine kleine Anzahl von Datenbankprodukten und das zugehörige Knowhow leistet man sich gerne, denn das braucht man und es lohnt sich. Aber wenn jetzt Software unnötigerweise ihr eigenes Repository-Format statt einer einfachen Dateispeicherung verwendet, kann es schon schwieriger werden, die ganzen Daten im Griff zu behalten.

Das Problem ist also in der Praxis dort, wo es eine große Rolle spielt, häufig irgendwie gelöst. Trotzdem wäre es manchmal schön, wenn die Verzeichnisstruktur nicht wie ein Verzeichnisbaum aufgebaut wäre, sondern zumindst in manchen Teilbäumen wie ein dünn besetzter Hyperquader, wo man nach einer Reihe von Kriterien die passenden Dateien suchen und in einem temporären virtuellen Verzeichnisbaum zugänglich machen könnte. Diese Mehrfachindizierung war übrigens auch in Datenbanken vor etwa 30 Jahren eine Herausforderung, wenn die einzelnen Indizes für sich genommen nicht selektiv genug sind. Man hat wohl das Konzept des Z-Index (oder der Z-Kurve) entwickelt, womit eine Verflechtung von verschiedenen Indizes möglich ist, die für sich genommen nicht selektiv genug sind, um für einen bestimmten Indexwert nur noch eine so kleine Menge von Daten zurückzuliefern, daß eine erschöpfende Suche in diesen Daten unproblematisch wäre.

In der Systemadministration stellt sich diese Frage häufig. In der Linux- und Unix-Welt ist es üblich, größere Applikationen so zu installieren, daß etwa 4-6 Orte im Verzeichnissystem verwendet werden:

  • ausführbare Programme in /usr/bin oder /usr/local/bin oder /bin o.ä.
  • Bibliotheken und statische Hilfsdaten dazu in /usr/lib oder /usr/local/lib oder /opt. Zum Teil architekturunabhängige statische Hilfsdaten und Bibliothken auch in /usr/share. Oft ist die ganze Applikation unter /opt installiert und es gibt softlinks nach /usr/local/bin
  • Man-Seiten unter /usr/man, /usr/local/man, /opt/man o.ä.
  • Konfiguration unter /etc
  • Variable Daten (z.B. Tablespaces für Datenbanken) unter /var/lib, /var/opt o.ä.
  • logs unter /var/log

Das ist das, was sich eigentlich etabliert hat und wie die Software, die mit Linux-Distributionen mitkommt, üblicherweise installiert wird. Man hat so nicht die ganze Applikation zusammen in einem Verzeichnis, aber dafür hat man eine Trennung in statische und dynamische Daten und kann auf Servern viel gezielter Entscheidungen darüber treffen, welche Daten in welches Backup kommen, welche Raid-Konfiguration man verwendet, ob man die Daten lokal oder remote hält, ob man sie mit anderen Servern teilen kann und ob man SSDs einsetzt oder auch wer Schreib- und Leseberechtigungen hat. Diese Fragen wird man eher für alle Daten in /var/lib gleich beantworten als für alle Daten unter /tomcat.
So wird die zweitbeste Vorgehensweise, eben alles, was zu einer Applikation gehört, komplett in einem Unterverzeichnis zu halten, trotz ihrer offensichtlichen Reize, eher selten gewählt. Hier wäre so eine Matrixstruktur für das Dateisystem schön, man könnte also Zeilen für die Applikationen haben und Spalten für die Art der Daten (lib, bin, share, var, log, conf, man,…).

Share Button

Große Tabellen

Es ist interessant zu sehen, wie große Tabellen sich in Echtzeit oder zumindest interaktiv ohne gefühlte Verzögerung handhaben lassen.

Vielleicht war es einmal praktisch, daß Telefonnummern oder Nummernschilder in vielen regionalen kleinen Tabellen strukturiert waren, die dann jeweils maximal wenige Millionen Einträge enthielten. Auch Bankkonten wurden an den einzelnen Standorten geführt und man mußte zum Abheben bei einer anderen Filiale derselben Bank warten, bis die zuständige Filiale kontaktiert worden war. Fahrkarten konnte man für inländische Zugverbindungen kaufen, aber für ausländische Verbindungen war ein Sonderschalter zuständig, und für Auslandsreservierungen mußte man zweimal vorbeikommen. Telefonbuchabfragen im Internet waren möglich, aber man mußte doch erst den Ort angeben und dann den Namen, selbst wenn es ein seltener Name war.

Heute sieht man, daß es kein Problem mehr ist, einen flachen Nummernraum für ein ganzes Land für Telefonnummern, Nummernschilder oder andere in ähnlicher Häufigkeit vorhandene Daten zu haben. Praktisch ist das zum Beispiel, weil man innerhalb von der Schweiz umziehen kann, ohne die Telefonnummer zu ändern, aber für die Mobiltelefone hat man ja schon von Anfang an auf regionalisierte Nummern verzichtet, früher weil es so wenige Mobiltelefone gab und heute, weil man mit 100 Millionen Mobiltelefonnummern in Echtzeit umgehen kann.

Share Button

Wo bleiben die Transaktionen bei den NoSQL-Datenbanken?

Jetzt war mal wieder so ein Vortrag bei einer der vielen User-Groups, in der ich drin bin. Diesmal ging es um Riak und der Vortrag war von einem der Entwickler und war wirklich gut. Ein Stück weit wurde die Grundsatzthematik der NoSQL-Datenbanken behandelt, wobei natürlich Riak im Zentrum stand.

Während es bei den SQL-Datenbanken vielleicht etwa vier gibt, die einigermaßen austauschar sind, was die Funktionalität betrifft (Oracle, MS-SQL-Server, DB2 und PostgreSQL, mit Einschränkungen auch mySQL und MariaDB), ist hier die Frage relevant, das richtige Werkzeug für die richtige Aufgabe zu verwenden. Und das richtige Werkzeug sind oft die relationalen transaktionalen Datenbanken. Die Austauschbarkeit der relationalen Datenbankprodukte gilt natürlich nur, wenn man eine neue Applikation entwickelt, nicht wenn man Software nachträglich umschreiben muß und die Datenbankadministrationsprozesse nachträglich ändern muß.

Nun sind die Daten ja immer sehr wichtig, sonst könnte man sich den Aufwand ja sparen, sie zu speichern. Und wenn die Datenbank nicht transaktional ist, dann ist das gruselig, weil die Daten nicht zuverlässig und nicht genau genug gespeichert werden. Das Versprechen der transaktionalen Datenbanken für Transaktionen wird ja kurz mit „ACID“ bezeichnet:

  • „Atomic“ bedeutet, daß die Transaktion entweder komplett oder gar nicht ausgeführt wird
  • „Consistent“ bedeutet, daß die Daten immer in einem konsistenten Zustand sind.
  • „Isolated“ bedeutet, daß verschiedene gleichzeitig laufende Transaktionen sich nicht gegenseitig beeinflussen.
  • „Durability“ bedeutet, daß die Daten nach Abschluß der Transaktion garantiert dauerhaft gespeichert sind.

Super, da kann nichts mehr schiefgehen. Und Software von Oracle oder IBM ist serienmäßig komplett fehlerfrei, von Microsoft sowieso und bei PostgreSQL bauen die Entwickler der Software, die die Datenbank verwendet, allfällige Fehler der DB-Software selbst noch kurz in der Nacht aus… Aber fairerweise muß man sagen, daß die Kernfunktionalität der Datenbankprodukte tatsächlich recht zuverlässig funktioniert und die nervigen Fehler eher am Rande bei unwichtigen Dingen wie Installern oder Security-Features aufgetreten sind, die hier nicht das Thema sind. 😉

Also gut, man kann also mal annehmen, daß die DB-Software vernünftige Zuverlässigkeit hat. Was ist mit der Hardware? In einem großen Rechenzentrum muß rein rechnerisch immer irgendwo ein Hardwaredefekt sein. Ist aber kein Problem, man baut die Hardware und damit die Datenbankinstallation einfach redundant auf. Und es gibt tolle Mechanismen, die mit erheblichem Aufwand sicherstellen, daß ACID immer noch gilt. Man hat also eine Datenbank, die auf mehreren Rechnern läuft und sich gegen außen wie einer verhält. Eine Transaktion kann auch mehrere dieser Rechner involvieren. Egal was passiert, soll immer ACID-Transaktionalität gelten. Mit „Two-Phase-Commit“ und solchen Werkzeugen kann man das hinbekommen. Zumindest sagt man, daß es in der Praxis zuverlässig funktioniert. Vielleicht bis zu einer gewissen Größe, denn wenn eine einzige Datenbank ein ganzes großes Rechenzentrum beansprucht, kann man sich sicher auf einen hohen Stromverbrauch verlassen, aber mehr will ich dazu hier nicht sagen. Reale Rechenzentren, die transaktionale Datenbanken betreiben, haben erfahrungsgmäß auch viele mehr oder weniger unabhängige Datenbanken am Laufen.

Man kann also mit recht großen ACID-transaktionalen Datenbanken die Unzuverlässigkeit der Hardware recht gut in den Griff bekommen. Das ist nicht billig und es lohnt sich gute Datenbankberater heranzuziehen.

Wie sieht es mit der Applikationssoftware aus? Die wird heutzutage ja oft in Java geschrieben und läuft deshalb in der Sandbox, was ja alle Probleme verhindert, weil die Sandbox unterbindet, daß irgendwas gefährliches gemacht wird… 😉

Ist die Applikationssoftware fehlerfrei? Oder zumindest so fehlerfrei, daß nie mit den Daten etwas schief gehen kann? Vielleicht, wenn man optimistisch ist? Wenn wir schon bis hierher Optimismus aufbauen konnten… Die transaktionale Datenbank ist ein nütziches Werkzeug, wenn man sie mit hinreichend korrekter Software benutzt. Aber das machen wir alle und auf dem Laptop des Entwicklers hat es wirklich funktioniert, es sind halt die Benutzer schuld, die mehrere Requests gleichzeitig abschicken. Was ist mit dem „I“ aus ACID passiert? Ja, die Datenbank macht es schon richtig, aber die Applikation stürzt ab oder erzeugt korrumpierte Daten. Oder verwendet zu kleine oder zu große Transaktionen. Schade…

Nun kommt aber noch die Software- und Systemarchitektur ins Spiel. Man fängt an, ein großes System über mehrere Server zu verteilen, Caching wird verwendet, Teilbereiche der Daten werden über Services angesprochen. Natürlich arbeitet man mit tollen Frameworks und die Datenbank ist für den Applikationsentwickler schon recht weit weg, aber immer noch macht man irgendwo implizit oder explizit schöne Tranaktionen auf und beendet sie mit commit oder rollback. Niemand weiß mehr, in welcher Schachtelungsteiefe von solchen Methoden, die implizit Transaktionen durchführen, man sich befindet und mit bestimmten Mustern kann man das aus Versehen austricksen, aber so etwas passiert natürlich nicht. Wie sieht es jetzt mit ACID aus?

Kurz gesagt, für reale Applikationen muß man genauer hinschauen, ob ein etas schwächeres Modell als ACID wirklich ein Nachteil ist.

Nun muß man aber noch einmal auf den Ausgangspunkt zurückkommen. Transaktionen lassen sich sehr gut für nicht-relationale Datenkbanken definieren und auch implementieren. Umgekehrt kann man für manche Zwecke durchaus relationale SQL-Datenbanken verwenden, die nicht transaktional sind. Oder man hat wie bei MongoDB Transaktionen für einzelne DB-Operationen, kann diese aber nicht zusammenfassen.

Share Button

MongoDB im RAM

Hier ist eine interessante Beschreibung, wie man MongoDB (unter Linux) komplett im RAM betreiben kann:

How to use MongoDB as a pure in-memory DB (Redis style)

Die Idee ist einfach und interessant, weil sie sich auch für viele andere, ähnlich gelagerte Anwendungsfälle eignet.

Für die Entwicklung von Mongo-DB-basierenden Applikationen und vor allem für die Unit-Tests kann das auch eine sehr nützliche Sache sein.

Share Button