“Kris”, fragt man mich, “Kris, gibt es außer ‘Volltextsuche’ noch andere Gründe, für neue Anwendungen noch MyISAM zu verwenden? Im konkreten Fall geht es um eine verhältnismäßig einfache Datenstruktur, in die nur streng sequenziell geschrieben wird, nie gelöscht wird und viel und kreativ gelesen wird.” Na, das ist doch mal ein weites Feld.

Teile davon habe ich in Ein paar Gedanken zu Zeitreihendaten und Wie man einen Graph plottet ja schon beackert.

Aber gut. ‘Streng sequentiell geschrieben’ heißt, wir haben eine Datenstruktur, die immer genau am ‘Ende’ einer Tabelle Daten anfügt, also eine Logtabelle. ‘Nie gelöscht’ heißt nur ‘Wir haben uns noch keine Gedanken über Data Lifecycle Management gemacht’. Und ‘kreativ gelesen’ heißt nur ‘Wir haben uns unter anderem deswegen noch keine Gedanken über Data Lifecycle Management gemacht, weil unsere Metriken noch nicht definiert sind’.

Das ist legitim. Aber fangen wir rückwärts an.

Data Lifecycle Management

Die meisten Systeme arbeiten transaktional und generieren Daten in einem Online Transaction Processing System. Das sind Systeme, in denen Daten normalerweise halbwegs normalisiert vorliegen. Meistens in der Nähe der 3NF (Internet-kompatible Erklärung mit Katzen. Diese Strukturen sind populär, weil sie zum Verändern und Beschreiben optimiert sind.

Ein Orderlog

In den meisten dieser Systeme stecken kleine Data Warehouses, die über die Zeit wachsen und dann heraus wollen. Zum Beispiel hat ziemlich jeder Webshop ein Orderlog mit Bestellungen, die in normalisierter Form auf den Besteller in der Kundentabelle und die bestellten Artikel in der Artikeltabelle verweisen würden. Nach der Bearbeitung der Bestellung, der Auslieferung und der Bezahlung werden diese Einträge inaktiv und brauchen eigentlich nicht mehr in der Shopdatenbank zu stehen.

Genau so hat ein Monitoring-System eine Datenstruktur, in der Testvorschriften in Testergebnisse umgewandelt werden, fehlgeschlagene Tests in Alarme umgesetzt werden und versendete offene Alarme durch Markierung durch die Admins in erledigte Alarme umgewandelt werden. Am Ende der Lebensdauer eines solchen Vorfalls braucht der Alarm eigentlich nicht mehr im Alarmlog des Monitoringsystems zu stehen.

Anfangs ist das kein Problem, da es nur wenige Bestellungen oder erledigte Alarme gibt, aber über die Lebensdauer und das Wachstum des Systems werden das mehr und mehr Daten, die sich ansammeln. Generell kann man solche Daten leicht identifizieren, indem man sich das Datenmodell anschaut und sich überlegt, welche Tabellen wachsen, wenn alle Eingangsparameter (Anzahl der Kunden, Anzahl der Artikel, oder Anzahl der überwachten Rechner, der fehlgeschlagenen Tests usw) gleich bleiben. Solche wachsenden Tabellen haben oft eine Primärschlüsselkomponente, die eine Zeitangabe oder ein Zähler ist.

Für diese Tabellen braucht man einen Prozeß, der erledigte Fälle erkennt und aus dem primären System in regelmäßigen Intervallen entfernt.

Die Daten müssen dabei außerdem denormalisiert werden. Denn Anschriften von Kunden, Preise und Beschreibungen von Artikeln oder die Funktionen von überwachten Systemen ändern sich. In unseren Auswertungen wollen wir aber wissen, wohin wir den Artikel damals geliefert haben und was wir damals kassiert haben und nicht, wo der Kunde jetzt wohnt oder was das Teil jetzt gerade kostet. Und so können wir nicht die kunde_id, artikel_id oder system_id archivieren, sondern wir müssen diese Dinge auflösen und die tatsächliche Kundenanschrift (oder seine Postleitzahl) archivieren, und die tatsächliche Artikelbezeichnung, Farbe und so weiter.

Dabei läuft man in eine Reihe von interessanten Problemen, weil die Daten, die man speichern und auswerten will, eventuell gar nicht die Daten sind, die nach der Denormalisierung vorliegen: In den Auswertungen interessiert gar nicht, daß der Kunde damals im Knooper Weg 46, 24103 Kiel gewohnt hat, sondern nur, daß er in 24xxx gewohnt hat, weil man am Ende eine Statistik von Umsätzen nach Postleitzahlen über Zeit haben will. Und es interessiert auch nicht, daß eine ‘viktorianische Damenbluse’ in ‘Aprikose’ verkauft worden ist, sondern die Auswertung will nach Blusen oder gar nur nach Damenoberbekleidung gruppieren. Und in der Auswertung gibt es vielleicht nur 16 Farben und Aprikose ist eine Frucht, also muß man auch hier die Modenamen von Farben in jeder Saison auf irgendwelche RGB-Codes mappen und diese dann in Töpfen gruppieren.

Diesen Prozeß der Denormalisierung, Attributextraktion und Neugruppierung nennt man man Extract, Transform, Load, und er ist etwas, mit dem man sich beim Schreiben einer OLTP-Anwendung beim besten Willen nicht befassen kann und will, weil man ganz andere Probleme hat und ja auch mal fertig werden will, damit man dem Kunden endlich seine Blusen und Monitoring SMS andrehen kann (Ein primitives Beispiel).

Es ist also nicht nur üblich, daß in jeder OLTP-Anwendung ein Data Warehouse steckt, das raus will, sondern auch vollkommen normal und gut so.

Dennoch geht so etwas auf die Performance, und außerdem will ein Betrieb mit funktionierenden Managementstrukturen irgendwann auch mal quantitativ gemanagete Prozesse haben und braucht dafür geeignete Metriken.

Ein Orderlog, eine Faktentabelle

ETL des Orderlogs in eine Faktentabelle, dabei Denormalisierung (Binning nicht eingezeichnet).

Dann setzt man sich hin, überlegt sich, was man für Zeugs aus dem OLTP-System ETL’t gekommt und was man davon in welcher Form archivieren will. Im Ergebnis bekommt man einen Strom von zu archivierenden Attributen, die sich aus der Auflösung von Primärschlüsseln in der OLTP-Datenbank ergeben: artikel_ids werden zu Bezeichnungen, Preisen und Warengruppen von Artikeln, kunden_ids werden zu Anschriften, Postleitzahlen, server_ids werden zu Leistungsdaten und Systemgruppen von Servern, und Fehlercodes werden zu Fehlermeldungen und Fehlergruppen von Störungen.

Alle diese Dinge gehen in Faktentabellen, die in der Regel eine Zeitdimension haben. Häufig wendet man dabei schon Key Compression an, wie in Ein paar Gedanken zu Zeitreihendaten beschrieben, um das Volumen der Daten halbwegs überschaubar zu halten.

Und in einem nächsten Schritt erzeugt man dann Aggregationen, die die relativ freien Faktendaten gruppieren und in Zählkörbe entlang einer Maßdimension verteilen (‘binning’).

Bei dem Shop kann man entlang Warengruppen vereinfachen (‘viktorianische Bluse’ -> DOB), entlang Größen (S/M/L/XL), Preisgruppen (<10 EUR, <20 EUR, …) und Farben (Aprikose -> Rot-Töne, Frühlingspalette, …). Ebenso kann man Kunden klassifizieren und aggregieren, damit man überhaupt eine statistische Basis für eine Auswertung bekommt.

Häufig wird man die Faktendaten nach dem Binning gar nicht mehr brauchen, außer man nimmt Veränderungen an der Definition der Bins vor oder will gar eine ganz neue Dimension erzeugen. Dann ist es für eine solche Reklassifizierung unabdingbar, daß man die Fakten noch einmal durchlaufen läßt, um die Aggregate zu aktualisieren. In den meisten Data Warehouses geht man Speicherkompromisse ein, und hält die Aggregate ‘unbegrenzt’ (also länger als 10 Jahre :-) und die Fakten kürzer (je nach Storage-Kapazität - am Ende muß jemand das Geld für den Plattenplatz bewilligen und man muß ökonomische Kompromisse eingehen).

Mit den Fakten kann man dann manuelle Analysen fahren oder Machine Learning anwerfen, um zu versuchen, Korrelationen zu finden und einen Klassifikator zu bauen.

Den Fall ‘nie gelöscht’ gibt es in der Regel bei nennenswerten Datensammlungen eher nicht, oder wenn doch, dann liegen die eigentlichen Fakten relativ wenig genutzt irgendwo in einer Backup-Ecke (und niemand würde sie vermissen, löschte man sie), weil man sich eher mit den generierten Klassifikatoren beschäftigt oder allenfalls noch einmal mit den Dimensionen, entlang derer man versucht, eine solche Korrelation zu finden.

Logging, Locking und MyISAM

Man will so ein Logging in Faktentabellen unbedingt von dem operativen Betrieb des OLTP-Systems trennen. Die Datenbanken mit Lognatur sind also von den Datenbanken mit wahlfreiem Zugriff trennen, denn zu unterschiedlich sind die physischen Zugriffsmuster, der Speicherbedarf, die Anforderungen an Verfügbarkeit und viele andere Dinge, die auf die Systemleistung Einfluß haben. ‘Trennen’ heißt hier idealerweise ‘anderes Blech’, weil es sonst auch hintenrum durch die Virtualisierung hindurch zu unangenehmen Übersprecheffekten kommen kann. Um so etwas auseinander zu sortieren braucht man einen Debug-Virtuosen mit Zugriff auf alle Komponenten der Wirkungskette - und solche Personen sind in der Regel teurer als zusätzliche Hardware.

Loggt man in eine MyISAM-Tabelle, dann loggt man eine Datenstruktur mit Table Locks: MyISAM hat für jede Tabelle entweder den Zustand ‘frei’ (idle), ‘shared access’ (ein oder mehrere konkurrente Lesezugriffe) oder ‘exclusive access’ (genau ein Writer). Es gibt noch den Sonderfall ‘concurrent inserts’, aber der hat auch seine eigenen Probleme und erzeugt in der Hälfte aller Fälle mehr Probleme als er hilft.

MyISAM Prioritäten

Diese Table Locks haben emergente Effekte im Betrieb. Die Regeln sind eigentlich simpel: Wir können nur ENTWEDER Daten loggen ODER geloggte Daten abfragen. Per Default haben bereits laufende Abfragen Vorrang vor allem anderen - MyISAM wird niemals eine bereits laufende Query von sich aus abbrechen. Die nächste Vorrangstufe haben Schreibzugriffe und auf der letzten Ebene liegen Lesezugriffe.

Ein INSERT in MyISAM braucht eine gewisse Zeit (im Bereich von einer Millisekunde) und so kommt man mit einzelnen INSERT-Statements auf etwa 700 Operationen pro Sekunde für handelsübliche Hardware mit rotierendem Rost. Andere Konfigurationen können sich anders, aber nicht zwangsläufig schneller verhalten - Locking kann sehr interessante Debugsituationen erzeugen.

Ob das INSERT durchkommt hängt in erster Linie davon ab, ob schon eine Query läuft. Wenn länger laufende SELECT-Statements auf der Tabelle aktiv sind, dann haben diese bereits ein Lock und MySQL wird bereits laufende Queries nicht von sich aus unterbrechen. In der Praxis sieht man meist einen Mix von schnellen SELECT-Statements mit einem oder zwei schlecht zu optimierenden, langsam laufenden Queries dazwischen.

Alles dies läuft ohne Probleme bis eine schreibende Query das exklusive Lock anfordert. Diese Query kann das Lock nicht bekommen, weil mindestens eine lesende Query bereits ein Lock auf der Tabelle hat. Die Schreibquery, zum Beispiel ein INSERT, stellt sich nun mit dem Status ‘Locked’ in die Prozeßliste und wartet.

In Folge werden alle weiteren SELECT-Statements, die ja niedrigere Priorität haben als das INSERT, von dem wartenden INSERT auch in den Wartezustand geschickt - sie hängen ebenfalls ‘Locked’ in der Prozeßliste und warten darauf, daß das INSERT fertig wird.

Irgendwann enden alle bereits laufenden SELECT-Queries und geben ihre Locks auf der Tabelle auf. Das INSERT-Statement wacht aus seinem ‘Locked’ auf und ist binnen einer Millisekunde erledigt. Jetzt können auch alle im Zustand ‘Locked’ hängenden SELECT-Statements abgearbeitet werden. Die meisten von ihnen sind schnell beendet, da sie gut optimiert sind. Nur eine von diesen wartenden Queries ist komplizierter, nicht gut optimierbar und braucht länger. Während sie läuft tritt das nächste INSERT-Statement auf….

Diese schwallartige Abarbeitung von SELECT-Queries nennt man MyISAM-Interlocking. Sie wird zwar durch die Logik der MyISAM-Table Locks gefördert, hat aber ihre Ursache nur mittelbar in der Hierarchie von Locks, und sie ist auch durch Umpriorisierung nicht zu lösen. Die eigentliche Ursache liegt in konkurrenten schreibenden und lesenden Zugriffen und das ist nur durch eine Datenstruktur mit weniger oder ganz ohne Locks in den Griff zu bekommen.

Man braucht Multi Value Concurrency Control (MVCC) und InnoDB.

Weitere Anmerkungen zu MyISAM

Es sei an dieser Stelle noch einmal ausdrücklich darauf hingewiesen, daß solche Lockingeffekte ausgesprochen nichtlinear sind. Solange sie nicht auftreten, treten sie nicht auf (sic!). Sie sind vollkommen unspürbar und nicht leicht vorherzusagen - außer, man hat sie schon mal gesehen und weiß http://www.amazon.de/Guerrilla-Capacity-Planning-Tactical-Applications/dp/3540261389. Wenn sie erst einmal auftreten, dann sind sie selbstverstärkend und bei weiter steigender Last steht das System binnen kürzester Zeit mit einer überlaufenden Prozessliste da.

Das ist der Grund, warum ich im Rootforum und andernorts auf die Frage ‘Meine Apache/PHP sagt, er könne sich nicht mehr an die Datenbank verbinden, weil die Connections alle sind’ nicht mehr antworte: ‘Setze max_connections herauf’ ist nicht die korrekte oder auch nur eine hilfreiche Antwort. Eine wirklich hilfreiche Antwort hätte nicht nur Buchumfang oder das Format eines längeren Consulting-Einsatzes, sondern der Fragesteller will sie auch nicht hören oder ist typischerweise nicht gut vorbereitet, sie zu verstehen.

MyISAM ist in Situationen, in denen mit genau einem Thread geschrieben wird und Reads vollkommen abwesend sind, extrem gut und schnell in der Lage, Daten zu laden: Die Ladegeschwindigkeit von MyISAM ist in solchen Situationen extrem hoch und nahezu konstant und von der Datengröße unabhängig - vorausgesetzt man ist schlau genug ist, Indices zu disablen und nach dem Ende des Loads einmal gesammelt aufzubauen. Leider ist das kein sehr typischer Anwendungsfall.

Man kann den Anwendungsfall ‘typischer’ machen oder versuchen, daß heiße, schreibende Ende der Tabelle besser zu isolieren, indem man in kleine Tabellen mit temporalen Namen lädt und die Abfragen dann nur auf alten, inerten Daten macht. Auf diese Weise kommen sich Schreib- und Lesezugriffe nicht ins Gehege: Der Load erfolgt in logging_20120528_1401 und die Abfragen dann auf alten, kalten Tabellen. Dadurch hat man einen Monitoring-Lag von einer Einheit (also hier im Beispiel von einer Minute).

MyISAM-Daten lassen sich auch offline packen: Indem man eine Tabelle mit “RENAME TABLE work.logging_201205281401 offline.logging…” in einen Bereich verschiebt, in dem auf Grund der GRANTs kein Teil der Anwendung Zugriff hat, kann man dann mit “FLUSH TABLES offline.logging…” ein Schließen der Filehandles zu dieser Tabelle erzwingen und sicher sein, daß die Filehandles auch zu bleiben.

Nun kann man von außen “myisampack” anwenden, um die MYD-Datei zu packen und dann mit “myisamchk” die Indices neu aufbauen. Das Resultat ist eine komprimierte (also deutlich kleinere) read-only Version der Tabelle, die nun mit “RENAME TABLE offline.logging… work.logging…” in die Produktion zurück verschoben werden kann. Die resultierenden Tabellen sind nicht nur gepackt (das könnte die ARCHIVE-Engine auch), sondern sie haben auch nutzbare Indices.

In MySQL hat man seit MySQL 5.1 die Option, das general_log und das slow_query_log statt als traditionelle Logfiles als CSV- (default) oder MyISAM-Tabellen (optional) in mysql.* zu realisieren. Nach den Ausführungen oben kann man sich leicht überlegen, wieso ein solches Log als MyISAM-Tabelle ineffizient ist (Lösungshinweis: MySQL ist ein Multithreaded Server, jeder Thread kann Logevents generieren).

Mit Kenntnis von “a” (O_APPEND in open(2)) kann man sich das Verhalten von CSV-Dateien oder normalen Logdateien im Vergleich überlegen: Wie ist sichergestellt, daß auch bei konkurrenten Log-Events einem Multithreaded-Server ein CSV-Record geschrieben wird, ohne daß sich Daten von einem anderen Log-Event da hinein überschneiden? Wie ist sichergestellt, daß CSV adäquat performiert?

InnoDB und konkurrente Zugriffe

Lädt man Daten multithreaded oder hat man auch auf der heißen, aktiven Tabelle am Ende des Logs Lesezugriffe, weil man sich die Auwerteverzögerung nicht leisten kann, dann ist MyISAM vollkommen unbrauchbar. Und das wie bereits erwähnt relativ schlagartig, da Locks sich in der Regel stark nicht-linear zuziehen. In diesem Fall muß man seine Logtabelle als InnoDB-Tabelle realisieren, und dabei die interne Struktur von InnoDB und ihre Rückwirkungen auf die Performance im Auge behalten.

InnoDB-Tabellen sind nach Primärschlüssel geordnet. Das bedeutet erst einmal, daß man einen solchen definieren muß, damit keine schlimmen Dinge passieren. Wählt man den Primary Key so, daß die Daten physikalisch in der Reihenfolge des Schreibens angeordnet werden, muß man sich über den Change Buffer und seine Performanceimplikationen im Klaren sein.

Generell muß man damit rechnen, daß dieselben Daten in InnoDB 2-3 mal mehr Platz auf der Platte und im Speicher belegen als in MyISAM. Zwar kann man auch in InnoDB Compression aktivieren, und im Gegensatz zu MyISAM sind komprimierte InnoDB-Tables sogar normal beschreibbar. Dennoch braucht auch eine komprimierte InnoDB-Tabelle noch deutlich mehr Platz als eine komprimierte MyISAM-Tabelle. Das liegt einerseits am Verwaltungsoverhead von InnoDB (MVCC braucht ca. 20 Bytes pro Row zur Verwaltung), und andererseits an dem in den InnoDB Pages zwangsläufig vorhandenem freien Platz: jede InnoDB-Page enthält eine gewisse Menge freien Platz, die Updates an Ort und Stelle auch bei wachsenden Daten ermöglicht. Wäre das nicht so, würde jede Änderung an den Daten zu Seitenüberläufen und einem Rebalancing des B-Baumes auf der Platte führen.

Ein praktisches Beispiel und die Folgerungen

Das ‘Eventlog’-System ist eine interne Entwicklung meines Arbeitgebers, das man am grob mit ‘ein Business-Level Logsystem, das dem Syslog auf Systemebene noch am ehesten entspricht’ beschreiben kann. Eventlog generiert gigantische Datenmenge, da so gut wie jede Systemkomponente Daten an die Eventlog Listener sendet. Die Listener bearbeiten die Daten in mannigfaltiger Weise, aber am Ende langen sie in einer Datenbank.

Dies war vor geraumer Zeit ein MyISAM, und die MyISAM-Dateien sind nach einer Woche wie oben beschrieben gepackt worden. Nachdem die Load auf dem Eventlog so groß geworden ist, daß das System durch Zuziehen der MyISAM Write-Locks auf den heißen Tabellen Performanceprobleme bekam, haben wir die heißen Tabellen auf InnoDB umgestellt und konvertieren jetzt nach einer Woche in compressed MyISAM.

Problem sind nun eher die laufenden Aggregationen, die schon vor langer Zeit dadurch begrenzt wurden, daß jede einzelne Query in MySQL immer nur mit einem Core arbeiten kann. Durch Auslagern der Aggregationen aus der Datenbank in Clients und durch geschickte Programmierung der Clients kann nun über mehrere Cores und sogar mehrere Maschinen parallel aggregiert werden.

Dies ist jedoch ein unangemessener Programmieraufwand. Tatsächlich wird dieses System durch einen kleinen Hadoop Cluster mit Hive und Flume abgelöst.

Hadoop HDFS speichert Dateien, die danach Immutable sind - neueres HDFS hat jedoch einen Append Mode für diese Daten. Auf diese Weise können die Daten auf JBOD redundant gespeichert werden und man braucht sich nicht mehr um Storage und seine Kosten zu sorgen.

Hadoop MapReduce erlaubt es, Auswertunge auf diesen Daten automatisch intern konkurrent zu fahren.

Dafür Code zu schreiben ist jedoch unangemessen, daher kommt Hive oben drauf - Hive kompiliert Anfragen in einem SQLishen Dialekt in Java Sourcecode, der auf dem Hadoop MapReduce Framework parallel ausgeführt werden kann. Die Beschleunigung ist dabei nahezu linear - Hive-Anfragen wurden auf einem 5-Blade-Cluster mit 60 Cores in etwa 56 mal schneller ausgeführt als die identische Query auf einem MySQL. Mit HiveODBC können die Analysten dabei ihr bestehendes Tooling weiterverwenden - für sie wird nur das Backend sehr viel schneller.

Flume ist ein Framework, daß Logdaten live transformieren, voraggregieren und in Hadoop/Hive laden kann.

Die Kombination wird die Performance- und Skalierungsprobleme von Eventlogging grundsätzlich lösen und die MyISAM und InnoDB inhärenten Probleme durch neue, aufregende und bisher noch unbekannte Probleme ersetzen - und wie wir erwarten, auf einem höheren Level.