Millionen von Objekten täglich in Pimcore aktualisieren? Klar.
Von Miro Kodet
Wie man massiven Datendurchsatz innerhalb einer einzelnen Pimcore-Instanz beherrscht
Ein großes Projekt zu betreiben ist nicht einfach. Und ein großes Projekt zuverlässig zu betreiben ist noch schwieriger. Ein Kunde kam mit einer PIM-Implementierung zu uns, die sämtliche Produktdaten für sein Healthcare-Projekt bereitstellt. Mehrere Kanäle, jede Menge Varianten, hunderttausende untergeordnete Objekte mit zusätzlichen Daten. Ein fetter Server mit Skalierungsmöglichkeiten, keine Public Cloud.
Die Grundarchitektur hat uns eine Weile gekostet, da wir sie parallel zu verschiedenen Frontends und weiteren Teilprojekten aufgebaut haben — aber sie hat sich zu einem stabilen Zustand entwickelt. Der nächste Schritt war, den Datenfluss rein und raus ohne Aussetzer am Laufen zu halten. Wir haben klein angefangen — ein Objekttyp, eine externe Datenquelle und eine recht niedrige Kadenz. Selbst hier sind wir ziemlich schnell an die Grenzen des Pimcore-ORM gestoßen. Eine halbe Million Objekte in einem Rutsch zu aktualisieren ist schlicht keine gute Idee. Ein vollständiger Sync dauerte 19 Stunden — wir brauchten ihn unter 30 Minuten. Es musste sich etwas ändern.
CLI-Befehle?
Wir haben es versucht — mehrere Instanzen eines CLI-Befehls parallel starten, das sollte's richten. Nope. Wie soll ein armer Entwickler Datenkonsistenz sicherstellen, ohne sich dabei neue Kopfschmerzen einzuhandeln? Klar, man könnte die Objekte im aktuellen Loop tracken. Oder irgendein fancy Locking implementieren, damit die Daten auf Kurs bleiben. Wir wussten, es muss einen besseren Weg geben.
Direkt in der Datenbank?
Das wäre vom Prinzip her ziemlich straightforward und schnell. Aber bist du sicher, dass du auch alle Relationen korrekt aktualisierst? Bist du sicher, dass du all die kleinen Details und Operationen kennst, die das ORM stillschweigend im Hintergrund erledigt? Das ist hier nicht der richtige Weg.
Queue!
Das ist der Treffer. Wir haben eine schnelle Message Queue (in unserem Fall RabbitMQ) und eine Handvoll Consumer eingesetzt — genauer gesagt 20, verwaltet von Supervisor, der sie am Leben hält und bei Fehlern neu startet. Jedes Objekt bekommt sein Update per Nachricht, ACK stellt sicher, dass sie nicht mehrfach verarbeitet wird, und man kann das Ganze problemlos parallel laufen lassen. Alles gut, Problem gelöst.
Na ja, nicht ganz. Um das am Laufen zu halten — und täglich Millionen von Einträgen zu aktualisieren — muss man das Biest irgendwie füttern. Ein geplanter Befehl durchforstet die Datenquellen und schiebt Nachrichten in die Queue. Klingt einfach, aber die richtige Kadenz zu finden hat ein paar Iterationen gebraucht — zu schnell pushen und die Consumer sind überfordert, zu langsam und man verfehlt wieder das Sync-Fenster. 52 GB RAM, 16 Kerne und ein paar graue Haare obendrauf. Skalierung bei hohem Durchsatz ist etwas, das man von Anfang an einplanen muss.
Wir haben auch eine Datenbank-Queue (Symfony Messenger hätte gut zu unserem Setup gepasst) oder Redis in Betracht gezogen. Beide würden auf ähnliche Weise funktionieren, mit ihren jeweiligen Vor- und Nachteilen — unser Ziel, die Queue vom System zu entkoppeln, einfache Administration und insgesamt gute DX haben die anderen Lösungen schlicht übertrumpft. Aber es ist definitiv kein Allheilmittel — pick immer den Stack, der zu deinem aktuellen Projekt und seinen Anforderungen passt.
Was ist jetzt das Rezept?
Verteile deine Last auf mehrere asynchrone Prozesse. Wenn das in deiner aktuellen Sprache oder deinem Stack nicht nativ möglich ist, umgehst du das mit einer Technologie, die es kann. In unserem Fall war die externe Queue die beste Lösung — heute verarbeitet sie täglich ~2 Millionen Updates, ohne ins Schwitzen zu kommen.