Daten & Tabellen

Vindle optimiert massive CSV-Importe durch Ersetzung von PHP mit Rust

Vindle reduzierte die Verarbeitungszeit für 27 GB Produktdaten, indem es das CSV-Filtern von PHP auf ein eigenes Rust-Tool verlagerte und redundante Parsing-Durchläufe eliminierte.

Illustration of a conveyor belt sorting data blocks efficiently
Für diesen Artikel erstellte Illustration

Automatisch aus dem englischen Original übersetzt.

Vindle, eine Preisvergleichsplattform, hat kürzlich seine Daten-Ingestion-Pipeline überarbeitet, um eine neue, umfangreiche Integration mit Bol.com zu bewältigen. Angesichts einer Steigerung des Produktvolumens um drei Größenordnungen ersetzte das Engineering-Team den ursprünglichen PHP-basierten Parser durch eine eigene Rust-Anwendung. Dieser Wechsel ermöglichte es ihnen, 27 GB gzipped CSV-Feeds effizient zu verarbeiten und dabei strenge stündliche Update-Zeitpläne einzuhalten.

Was passiert ist

Die Herausforderung begann, als Vindle Bol.com integrierte, einen Einzelhändler mit mehr als 73 Millionen Produkten. Die Daten wurden in 26 gzipped CSV-Dateien geliefert, die insgesamt etwa 27 GB umfassten, wobei die einzelnen Dateigrößen zwischen 1,5 MB und 4,8 GB lagen. Da Vindle die Preise stündlich aktualisiert, musste das System diese Einträge schnell parsen, filtern und speichern. Die erste Implementierung nutzte die fgetcsv()-Funktion von PHP innerhalb einer Symfony-Anwendung. Obwohl dieser Ansatz schnell entwickelt war und für kleinere Feeds ausreichte, wurde er zum Engpass bei der Verarbeitung von Millionen Zeilen, von denen über 99 % aufgrund irrelevanter Kategorien oder fehlender Daten herausgefiltert wurden.

Um die Leistungslücke zu schließen, führte das Team zunächst Xan ein, ein in Rust geschriebenes Kommandozeilen-CSV-Tool. Dies ermöglichte es ihnen, das Filtern von Zeilen und die Auswahl von Spalten aus PHP auszulagern und nur relevante Daten zurück in die Hauptanwendung zu streamen. Vindle benötigte jedoch auch detaillierte Statistiken darüber, warum Zeilen verworfen wurden, z. B. wegen ungültiger Marken oder nicht unterstützter Versandregionen. Xan konnte diese Metriken nicht in einem einzigen Durchlauf bereitstellen, was das Team zwang, das Tool zweimal auszuführen: einmal für die Statistiken und einmal für die gefilterten Daten. Dieser Zwei-Durchlauf-Ansatz fügte jedem Feed 20–40 Sekunden hinzu und duplizierte die aufwendige CSV-Parsing-Arbeit.

Ein Versuch, diese beiden Xan-Prozesse unter Verwendung von Unix-Pipes und tee zu parallelisieren, scheiterte an Backpressure-Problemen. Der langsamere Zweig blockierte die gesamte Pipeline, und die Komplexität der Fehlerbehandlung nahm zu, ohne Geschwindigkeitsgewinne zu liefern. Letztendlich schrieb das Team ein kleines, dediziertes Rust-Programm, das Filterung und Statistiksammlung in einem einzigen Durchlauf durchführte. Dieses Tool liest von Standard Input, wendet Ausschlussregeln an, schreibt gültige Zeilen nach Standard Output und sendet Statistiken an Standard Error. Diese Lösung stellte die Effizienz eines einzelnen Durchlaufs wieder her und lieferte die erforderlichen Erkenntnisse, ohne dass ein vollständiger Rewrite des Symfony-Kerns erforderlich war.

Wichtige Details

  • Bol.com stellt über 73 Millionen Produkte in 26 gzipped CSV-Dateien mit einer Gesamtgröße von etwa 27 GB bereit.
  • Die ursprüngliche PHP-Implementierung nutzte fgetcsv(), hatte aber Probleme mit dem Datenvolumen, das starkes Filtern erforderte.
  • Das Filtern entfernt zwischen 90 % und 99,9977 % der Zeilen basierend auf Kriterien wie Kategorie, Preisgültigkeit und Versandort.
  • Die Verwendung von Xan zur Vorverarbeitung verbesserte die Geschwindigkeit, erforderte jedoch zwei separate Durchläufe, um sowohl gefilterte Daten als auch Ablehnungsstatistiken zu erfassen.
  • Die Parallelisierung von Xan-Prozessen über Unix-Pipes führte zu Backpressure-Engpässen und verbesserte den Gesamtdurchsatz nicht.
  • Ein eigenes Rust-CLI-Tool unter Verwendung der simd-csv-Bibliothek handhabt nun Filterung und Statistiken in einem einzigen Durchlauf und streamt die Ergebnisse direkt an PHP.

Hintergrund

Das Parsen von CSV wird oft als trivial angesehen, doch im großen Maßstab wird es CPU- und I/O-intensiv. In Hochvolumenszenarien summieren sich die Kosten für das Lesen, Dekomprimieren und Tokenisieren jedes Zeichens in einer Datei schnell. Wenn die meisten Zeilen verworfen werden, ist der Overhead für das vollständige Parsen vor der Ablehnung verschwendete Rechenleistung. Tools wie Xan nutzen Low-Level-Optimierungen wie SIMD (Single Instruction, Multiple Data)-Instruktionen, um dieses Parsen zu beschleunigen. Generische Tools bieten jedoch möglicherweise nicht die spezifische Geschäftslogik, die für komplexes Filtern oder statistisches Tracking erforderlich ist. Das Schreiben einer kleinen, fokussierten Binary in einer Systemsprache wie Rust ermöglicht es Entwicklern, hochperformantes Parsen mit benutzerdefinierter Logik zu kombinieren und den Overhead von Interpretern wie PHP für enge Schleifen zu vermeiden.

Warum es wichtig ist

Für Teams, die Self-Hosted-Anwendungen betreiben, verdeutlicht dieser Fall die Bedeutung, die richtige Grenze zwischen High-Level-Anwendungscode und Low-Level-Datenverarbeitung zu identifizieren. Eine vollständige Migration des gesamten Ingestion-Workflows zu Rust hätte erhebliche Wartungsschulden und Komplexität eingeführt. Indem Symfony für Orchestrierung, Credentials und Datenbankpersistenz verantwortlich blieb, behielt das Team die Produktivitätsvorteile von PHP bei und löste den spezifischen Leistungsengpass mit einem spezialisierten Tool. Dieser hybride Ansatz ermöglicht es Ingenieuren, kritische Pfade zu optimieren, ohne die Entwicklungsfreundlichkeit ihres primären Frameworks zu opfern.

Darüber hinaus reduziert der Umstieg auf Streaming-Daten die Abhängigkeit vom Festplattenspeicher. Der ursprüngliche Prozess erforderte das Schreiben großer temporärer Dateien auf die Festplatte, was erheblichen Speicherplatz und I/O-Bandbreite verbrauchte. Das neue Rust-Tool verarbeitet Daten von Standard Input zu Standard Output und ermöglicht so einen disklosen Workflow. Dies ist entscheidend für Umgebungen mit begrenztem Speicher oder dort, wo I/O-Konkurrenz andere Dienste beeinträchtigen kann. Es zeigt, dass Leistungsverbesserungen oft aus architektonischen Änderungen resultieren, wie der Eliminierung redundanter Durchläufe und der Reduzierung von Zwischenspeichern, anstatt nur Code-Syntax zu optimieren.

Was Sie tun können

  • Profilieren Sie Ihre Daten-Ingestion-Pipelines, um zu identifizieren, ob Parsen oder Filtern der primäre Engpass ist, bevor Sie Code neu schreiben.
  • Erwägen Sie, schwere zeilenweise Verarbeitung auf externe Binaries auszulagern, die in Systemsprachen wie Rust oder Go geschrieben sind.
  • Vermeiden Sie die Parallelisierung identischer Parsing-Aufgaben über Unix-Pipes, wenn die Zweige ungleiche Arbeitslasten haben, da Backpressure den Durchsatz begrenzt.
  • Entwerfen Sie CLI-Tools so, dass sie Eingaben von Standard Input akzeptieren und Ausgaben nach Standard Output schreiben, um Streaming zu ermöglichen und die Festplattenutzung zu reduzieren.
  • Behalten Sie die Geschäftslogik und Orchestrierung in Ihrem primären Framework (wie PHP/Symfony) und lagern Sie nur rechenintensive Datenverarbeitungsschritte in spezialisierte Tools aus.

Weitere News

Alle News