MySQL und Barrakudas

Eigentlich sollte man sich um einen Datenbankserver möglichst wenig kümmern müssen. Regelmäßige Updates zur Sicherheit sind auch kein Thema. Bei der Konfiguration sollte man in der Regel auch nicht alle Tage was neues ausprobieren.

Spannend wird das nur, wenn dann etwas nicht funktioniert und man auf die Suche geht. So etwas ist mir heute einmal wieder passiert. Auslöser war eigentlich eine harmlose Sache: Vor kurzem gab es eine Fehlermeldung, dass ein Datensatz eines Benutzers nicht gespeichert werden konnte – mit dem Hinweis:

MySQL Error 1118 (Row size too large)

Soweit so ärgerlich – ich habe da erst ein wenig gesucht, aber nichts gefunden – da der Fehler sich auch irgendwie nicht adhoc reproduzieren lies habe ich mich erst mal um das Tagesgeschäft gekümmert. Bis mir das gleiche Problem heute selbst auf die Füße gefallen ist, allerdings nicht an ursprünglicher Stelle sondern bei einer ganz anderen Aktion: Eigentlich wollte ich nur “mal eben schnell” feststellen ob die Performanceprobleme einer neuen Funktion nur darauf zurück zu führen sind, dass unser Entwicklungssystem schon etwas älter ist und damit nicht immer gerade als Vergleich herhalten kann (es hat gewisse Vorteile ein solches System zu verwenden: Was da schon nicht prickelnd läuft, ist in der Regel ein Hinweis, dass man sich die Programmierung nochmal anschauen sollte).

Nunja in diesem Fall hat die Analyse der Programmierung leider nicht so recht gefruchtet – daher wollte ich einen Versuchsballon starten und die aktuelle Entwicklung auf unserem Produktiv-System in einer Spielwiese testen. Normalerweise ist das kein Problem und wenn die Performance dort dank mehr Rechenleistung ausreichend ist, kann man es ja erst mal so lassen. Ich nehme also meinen Entwicklungs-Dump und versuche diesen in die Spielwiese zu laden. Aber – oh Wunder – ich renne genau in den Fehler von oben hinein. Allerdings mit einem Dump, den ich aus einer bestehenden Datenbank extrahiert habe – die Größe müsste also ja passen, denn sonst wäre es ja gar nicht erst reingekommen.

Aber der Reihe nach: Ich suche natürlich erst einmal nach einem Problem mit der Größe, dabei stoße ich darauf, dass es eine Veränderung an der Art und Weise gab, wie die Datenbank mit ihrer InnoDB-Engine die Daten ablegt – das neue Format wurde “Barracuda” getauft. Dieses soll unter anderem effizienter mit dem Speicher haushalten, bzw. man kann festlegen wie ggf. gespeichert werden soll. Generell gilt für die InnoDB: Nur große Objekte (also TEXT, BLOB und Consorten) werden außerhalb der Seite abgelegt, alles andere bleibt in der Page. Das kann zu dem oben genannten Fehler führen. Allerdings muss man es da schon recht dicke treiben, denn die Blattgröße ist maximal 8126 Bytes bezüglich der Daten. Selbst wenn ich nun vom absoluten Worst-Case für meine Datenbank ausgehe und für jedes VARCHAR-Feld pro Zeichen von maximal für Bytes (weil wir mit UTF-8 arbeiten) ausgehe, komme ich auf etwas mehr als 600 Bytes. Also kann das nicht der Grund sein… ebenfalls würde das noch nicht erklären, warum es mit dem früheren Speicherformat (names “Antelope”) bisher funktioniert hat.

Auch wenn es auf den ersten Moment komisch erscheint, habe ich dann im Testsystem mal nach einem weiteren StackExchange-Eintrag experimentiert, und ins Schwarze getroffen. Mit der veränderten Einstellung lief der Import plötzlich sauber durch. Ganz wichtig dabei ist, dass man es wie hier beschrieben macht. Also erst den Server-Prozess sauber beenden, damit wirklich alle Änderungen aus dem Log geschrieben sind. Dann zusätzlich die Index, Log und Cache-Files für die InnoDB aus dem Weg räumen, dann umstellen und den Serverprozess wieder starten. Der kümmert sich dann darum, dass die notwendigen Verwaltungs-Dateien automatisch wieder erzeugt werden und diesmal die neue Größe haben.

Problem gelöst, nur weshalb trat der Fehler eigentlich auf? Denn man möchte ja etwas dabei lernen. Die Ursache ist in der Art und Weise zu suchen, wie der MySQL-Dump funktioniert: Im Prinzip ist die Datei nur eine große Sammlung von Statements, die nacheinander ausgeführt werden. Dabei wird natürlich auch Rücksicht auf Abhängigkeiten genommen, z.B. die Fremdschlüssel – denn es kann ja passieren, dass Tabelle “A” auf eine Spalte in der Tabelle “Z” referenziert, die Tabelle “Z” aber noch nicht erzeugt wurde, wenn “A” rekonstruiert wird. Die Daten werden dabei (wie man es auch ganz regulär machen würde) als Insert-Statements in die Tabellen geschrieben. InnoDB beachtet das ACID-Prinzip, jedes Statement wird daher als atomare Operation betrachtet. Um effizienter zu arbeiten, verwendet der Dump folgendes Konstrukt

insert into table a values ('a','b','c'), ('d','e','f'), .... ('xyz','aa','cc');

Das ist völlig legitim. Man kann alternativ auch folgende Statements ausführen.

insert into table a values ('a','b','c');
insert into table a values ('d','e','f');
....
insert into table a values ('xyz','aa','cc');

Das Ergebnis ist das Gleiche. Nur intern ist es ein kleiner aber feiner Unterschied. Um die ACID-Kriterien zu erfüllen, wird im zweiten Fall nach jedem einzelnen Statement ein Commit durchgeführt (was etwas Performance kostet), für InnoDB ein Zeichen, dass man die Log-Datei “wegschreiben” kann und damit dann auch wieder leeren kann, es ist also Platz für neue Daten in der Datei vorhanden. Passiert das nicht (wie im ersten Fall, in dem InnoDB davon ausgehen muss, es handelt sich um ein atomares Statement) kann die Log-Datei zu klein sein, was dann zur Fehlermeldung führt. Warum dann aber eine völlig fehlweisende Fehlermeldung auftaucht, die keinen Hinweis auf das Logfile enthält, bleibt vorerst noch Sache der jeweiligen Entwickler. Ich habe mal einen Bug-Report ausgefüllt, mal sehen ob das behoben werden kann (oder ob ich selbst einmal die Zeit finde mich dem Problem zu widmen).