PHP, MariaDB, Load Data Infile – manchmal ist der Umweg über Dateien performanter ….

Im Moment bin ich wider mit LAMP-Entwicklung beschäftigt. Mit Simulationen zu technischen Netzwerken in mehreren Dimensionen mit vielen Knoten, die kreuz und quer verbunden sein können. In solchen Netzen gibt es u.U. eine mit der Knotenmenge exponentiell wachsende Anzahl von Pfaden. Die Simulation des Informationstransports durch solche Netze ist ähnlich zum Transport von Signalen in “künstlichen neuronalen Netzwerken” – in unserem Fall ist aber die Komplexität aufgrund der Netzstruktur und der Qualität der transportierten Information erheblich größer.

Das Ziel meiner Auftraggeber sind datenbankgestützte Simulationen solcher technischen Netze mit PHP. Bei einer hinreichenden Anzahl von Knoten, Ebenen und Verbindungen erhält man dabei u.U. erhebliche Datenmengen, die man während der laufenden Rechnung systematisch auslagern muss. Das ist in Forschungslabors mit einer Armada von Prozessoren und SSDs nun heute selten ein grundsätzliches Problem. Im einem kleinen Büro oder auf billigen LAMP-Servern aber sehr wohl – insbesondere dann, wenn man nicht mit Multithreading in Web-Server-Umgebungen beginnen will oder aus Sicherheitsgründen gar nicht kann (pthread etc,. lassen das in Webumgebungen gar nicht zu). Oft hilft dann ein kleiner Umweg – und der führt u.U. über Ajax, Web-Clients und Dateien.

Ausgangslage

Aus bestimmten Gründen sollen die Berechnungen meiner Kunden auf LAMP-Servern angeboten werden. Hochgradige Programm-Effizienz ist also ein Muss. Günstige virtualisierte Mittelklasse-Server haben oft eine Ausstattung mit 4-6 Prozessoren, mehreren GB RAM und SSD-Speicherplatz. Zwei Prozessoren sollen ggf. einen Apache-Server mit PHP-Modul stützen, zwei andere eine Hauptdatenbank, die während der Simulation benutzt wird, und ein fünfter ggf. eine Bank für Ergebnisdaten.

Nun macht jeder mal die Erfahrung, dass Datenbank-INSERTs in der Regel grundsätzlich zu den langsamen Transaktionen zählen. Zudem behindern sie in einem PHP-Programm die Ausführung weiterer datengenerierender Funktionen, die möglicherweise gar nicht – oder zumindest nicht zeitnah – von den erzeugten Outputdaten abhängig sind. So kann es je nach Breite von Records schon mal im Bereich von Sekunden dauern, wenn man mehrere hunderttausend Datensätze “on the fly” in Datenbank-Tabellen schreiben will – und dabei auch noch mehrere Indices aufgebaut werden sollen. Wertvolle Zeit, in der andere Dinge – nämlich weitere numerische Berechnungen – passieren könnten. Wie kann man da CPU-Zeit sparen bzw. sinnvoll nutzen?

Ajax und der Web-Client zur Multitasking-Steuerung für Arme

Man vergisst oft, dass bei gut separierbaren Aufgaben auch der (Web-) Client ins Spiel gebracht werden kann. So kann ein Client über Ajax zwei (oder mehr) PHP-Threads auf einem Apache-Server starten – jeder in einer eigenen Umgebung (memory-, session- und prozess-bezogen). Per Ajax kann man den zweiten Prozess auch periodisch – und in Abhängigkeit von zwischenzeitlich dokumentierten Ergebnissen des kontinuierlich laufenden ersten Prozesses starten.

Der Hauptprozess – in unserem Fall unsere Simulation – kann dabei mit den weiteren periodisch gestarteten Prozessen signalartig über ein gemeinsames Medium kommunizieren – nämlich über spezielle Tabelleneinträge in der Datenbank. Die periodischen Jobs lesen diese Information aus und verhalten sich entsprechend. Ergebnisse werden per Ajax an den Client übermittelt, der dann weitere Nachfolgeaktionen einleitet.

Diese Art von getriggertem “Multitasking” über einen zentralen Web-Client und einen vermittelnde Ajax-Schicht hat einige Vorteile bei langwierigen Prozessen – wie etwa Simulationsläufen. Eine typische Anwendung ist etwa das Status-Polling zum Simulationsprozess. Ich hatte darüber bereits früher in diesem Blog berichtet.

Umgang mit großen zwischen-zeitlich aufkommenden Ergebnisdaten

Wie können wir das nun nutzen, um
zwischenzeitlich produzierte große Datenmengen halbwegs schnell in eine Datenbank aufzuräumen? Man vergisst im LAMP-Geschäft zu oft Dateien als Austauschmedium und als Puffer. MariaDB/MySQL-Systeme bieten zudem einen hochgradig effizienten Weg an, große Datenmengen in flache Tabellen einzulesen, nämlich “LOAD DATA INFILE”:

Vergleicht man die Zeit, die man in einem ausreichend mit RAM gepufferten System mit Write-Caching auf Linux-Ebene benötigt, um strukturierte Daten in ein File zuschreiben, so wird man feststellen, dass das u.U. erheblich (!) weniger Zeit kostet, als zwischenzeitlich eine Datenbanktabelle zu befüttern. Das gilt auch dann noch, wenn man die Zeit hinzurechnet, die ein LOAD DATA INFILE der MariaDB für das Füllen einer Tabelle aus den insgesamt generierten CSV-Filedaten (mit ggf. Millionen Datensätzen) benötigt. Das Schreiben in ein File, das anschließende Laden in eine Tabelle einer ansonsten wenig ausgelasteten Bank (ohne gleichzeitige Index-Generierung!) und die nachträgliche Erzeugung von Indices ist i.d.R. immer noch um Faktoren schneller als der direkte Transfer über (prepared) INSERTs – selbst wenn man hunderte Rows in ein INSERT-Statement packt. Zeit, die – wie gesagt – vom eigentlichen Simulationsprogramm genutzt werden kann!

Hinweis:
Ganz entscheidend für den effizienten Einsatz von “LOAD DATA INFILE” ist hierbei, alle Indices von der zu befüllenden Tabelle gelöscht zu haben, die Daten zu laden und erst danach die notwendigen Indices zu generieren. In all unseren Tests haben sich hierbei übrigens MyISAM-Tabellen performanter als InnoDB-Tabellen verhalten.

Schrittfolge

Wir werfen kurz einen Blick auf die Schrittfolge (habe leider gerade keine Zeit, schöne Grafiken zu zeichnen):

  • Schritt 1: Der Web-Client startet das Simulationsprogramm “S”, das durchgehend auf der Hauptdatenbank-Instanz arbeitet.
  • Schritt 2: Der Web-Client startet unmittelbar darauf auch noch zum ersten Mal und danach periodisch ein weiteres Programm “O”, das für die Behandlung von Output-Daten zuständig ist (sobald halt welche angefallen sind) und das dazu eine spezielle Tabelle der Hauptdatenbank auf Einträge zu fertiggestellten Output-Dateien abfragt. Der Client startet “periodisch” aber nur dann weitere Jobs vom Typ “O”, wenn er zuvor Statusmeldungen des aktuellen “O”-Prozesses erhalten hat, und sich der aktuelle “O”-Prozess beendet hat.
  • Schritt 3: Der zweite PHP-Prozess “O” widmet sich dann jeweils der zuletzt fertiggestellten Datei, prüft ggf. deren Struktur und übergibt dann die weitere Behandlung an einen LOAD DATA INFILE-Prozess der MariaDB – ggf. einer zweiten separaten Datenbank-Instanz, der eigene Prozessoren und memory-Anteile zugewiesen wurden.
  • Schritt 4: Nach einem erfolgreichen Laden der Daten durch den Prozess “O” beendet sich dieser, nachdem er dem Client noch eine Meldung per Ajax zugeschickt hat.
  • Schritt 5: Der Client startet ein weiteres Mal das für Outputs zuständige Programm vom Typ “O”.
    etc.

Das Ganze dauert so lange, bis vom Hauptprogramm ein “Signal” (per DB-Eintrag) gesetzt wurde, dass kein weiterer Output mehr zu erwarten ist.

Das Vorgehen entspricht einer Art selbstgesteuertem, dynamischen Batch-Processing von Daten auf einem LAMP-Server. Es erfordert ein wenig mehr Programmieraufwand (insbesondere zur Fehlerbehandlung) als ein Standard-Vorgehen im Rahmen eines einzigen Threads – es bringt u.U. aber Faktoren an Performance – solange die Simulation nicht auf Zwischenergebnisse in der Bank warten muss. Letzteres ist bei uns oft genug der Fall.

Wir haben so Programm-Läufe, die in Tests 25 Sekunden dauerten, auf unter 5 Sekunden gebracht. Es
war der Mühe wert – und wir können anderen, die langwierige PHP-Programme für Simulations-Berechnungen auf LAMP-Servern nur empfehlen, eine solche Vorgehensweise an kritischen Engpässen mal als Alternative zu untersuchen.

Zusammenfassung

Fallen bei langwierigen PHP/LAMP-Programmen zwischenzeitlich große Datenmengen an, können diese u.U. ohne Beeinträchtigung des Hauptprogramms parallel durch weitere PHP-Programme einer Behandlung durch eine Datenbank zugeführt werden. Teil der Steuerung kann dabei ein gemeinsamer Ajax-Client sein, der die Ergebnislieferung der verschiedenen PHP-Programme überwacht. Der Datenaustausch zwischen den parallel laufenden PHP-Prozessen erfolgt dabei über die Datenbank, Ajax-Meldungen und Datenfiles. Zur Behandlung großer Datenmengen kann dabei LOAD DATA INFILE seine Performance-Vorteile ausspielen. Insgesamt sind dadurch im Einzelfall erhebliche Programm-Beschleunigungen möglich.

phpmyadmin, charts, MariaDB – a strange bug with x-axis value order and ORDER clauses in sub-SELECTS

A customer of mine wants to use phpMyAdmin to create some charts whilst working with a complex database. I recommended a trial with phpMyAdmin and its chart functionality. Unfortunately, this did not always work out as expected:

You may e.g. create an ordered resultset first – with a suitable SELECT and ORDER clause – e.g. to get two columns – an index “i” and associatd values “val”. Rows of the resultset ordered e.g. ASC with the “i”-values. But after using the columns “i” a simple line graph the order on the x-axis may appear different than requested by your ORDER clause.

I have posted this as an issue to the (nice) developers of phpmyadmin – who are working on it :-).

There is a workaround. But what I found more interesting is the cause of the problem, which appears on MariaDB and NOT on MySQL. It has apparently to do with a different handling and interpretation of SQL standards for ORDER clauses in sub-SELECTs! See:
https://mariadb.com/kb/en/library/why-is-order-by-in-a-from-subquery-ignored/

This is of general interest for developers using MySQL or MariaDB-databases. I, myself, was not aware of it. Besides an already different handling of database replication, I regard this point as one more indication for a growing “distance” between both RDBM-systems…

If you stumble across the phpmyadmin-chart problem, see a discussion and a workaround on the following site:
https://github.com/phpmyadmin/phpmyadmin/issues/14239

I also want to explicitly thank M. Jayaratne who supplied me with the valuable information referenced above.