Sonntag , Februar 23 2020
Home / Business Intelligence / Fallstudie: Ableiten von Funkengebern und -schemata mithilfe von Implicits

Fallstudie: Ableiten von Funkengebern und -schemata mithilfe von Implicits

Klicken Sie hier, um mehr über den Autor Dávid Szakallas zu erfahren.

In den letzten Jahren hat sich die Größe und
Komplexität unseres Identitätsdiagramms, einem Datensee mit Identitätsinformationen
über Menschen und Unternehmen auf der ganzen Welt, bat um die Hinzufügung von Big Data
Technologien bei der Einnahme. Wir haben zuerst Apache Pig verwendet und dann migriert
vor ein paar Jahren zu Apache Spark. Hier ist ein genauerer Blick auf unsere Reise und
wichtige Lektionen, die wir dabei gelernt haben.

Unser Team hat Apache Spark ausgewählt
in erster linie, weil ein großer teil des aufnahmevorgangs aus verschlungenen geschäften besteht
Logik um das Auflösen und Zusammenführen neuer Kontaktpunkte und Agenten in der
vorhandenes Diagramm. Solche Regeln sind in SQL-ähnlichen Sprachen schwer auszudrücken.
Mit Spark hingegen ist es möglich, eine vollwertige Programmierung zu nutzen
Sprache wie Scala, Java, Python oder R. Wir haben uns für Scala entschieden, das
ermöglichte es uns, die Logik zu schreiben und einfach zu erweitern, was für eine schnelle Entwicklung wichtig ist
Integration neuer Anbieter.

Diese Flexibilität kam zu einem
kosten allerdings. Aufgrund seiner Komplexität ist es nahezu unmöglich, Tabellen anzuwenden
datenbezogene Optimierungen für benutzerdefinierten Scala-Code. Es wurde auch über offensichtlich
Mal, wenn Scala-Verschlüsse verwendet werden, ist die Speichernutzung so hoch und die CPU so schwer
Zyklen, die für die Speicherbereinigung und Objektserialisierung, die CPU und die
Speicher wird zum Engpass der verteilten Parallelverarbeitung statt
Festplatten- und Netzwerk-E / A. Zum Glück, als diese Probleme bei uns auftauchten
Ingestion-Pipeline enthielt Apache Spark bereits eine verwaltete SQL-Laufzeit
Die meisten dieser Mängel wurden behoben. Wir haben gemerkt, dass wir einsteigen müssen
um die Skalierbarkeit zu erhalten
das erwartete Wachstum der Datenmenge in den kommenden Jahren.

SQL zur Rettung aktivieren

In unserem Legacy-Code wurden die RDDs verwendet
von einfachen Scala-Objekten in Kombination mit Scala-Verschlüssen zur Durchführung aller
die Transformationslogik.

Die Hauptabstraktion Spark
offers ist ein resilient Distributed Dataset (RDD), bei dem es sich um eine Sammlung von Daten handelt
Elemente, die auf die Knoten des Clusters verteilt sind, in denen gearbeitet werden kann
parallel.

Dies war vor einigen Jahren die einfachste und empfohlenste Art, Spark-Programme in Scala zu schreiben. Darüber hinaus ähnelt die API der Scala-eigenen Erfassungsschnittstelle und leiht Konzepte und Terminologie aus der funktionalen Programmierung, die zu dieser Zeit gerade zum Mainstream wurde. Dieser Programmierstil war mit grundlegenden Scala-Kenntnissen leicht zu erlernen und fand seine Anhänger bei Leuten, die danach streben, funktionale Programmierung für Big Data-Anwendungsfälle auszuprobieren. Snippet 1. gibt einen Blick auf diesen Stil.

Snippet 1. Transitive Closure über einem gerichteten Graphen mit Spark RDDs

Im Laufe der Zeit wurde jedoch deutlich, dass die Programmierung unserer Ingestion-Pipeline auf diese Weise in Bezug auf die Leistung alles andere als ideal ist . Zu diesem Zeitpunkt hatten wir eine Monolith-Aufnahme-Anwendung, die sich auf verwirrende 30.000 SLOC belief, die ausschließlich mit RDDs und Scala-Verschlüssen geschrieben wurden. Wir hatten mit steigenden Kosten zu kämpfen, die auf höhere RAM- und CPU-Auslastung, Unzuverlässigkeit und Korrektheit zurückzuführen waren.

Abbildung 1. Probleme mit unserem Legacy-Code

Die Leistungsprobleme von
Die Verwendung einfacher RDDs war zu diesem Zeitpunkt bereits gut dokumentiert. Außerdem Spark
SQL, eine optimierte API und Laufzeit für teilstrukturierte, tabellarische Daten, war schon
stabil für ein Jahr.

Spark SQL ist ein Spark-Modul für
strukturierte Datenverarbeitung. Im Gegensatz zur grundlegenden Spark-RDD-API sind die Schnittstellen
bereitgestellt von Spark SQL stellt Spark weitere Informationen zur Struktur zur Verfügung
sowohl der Daten als auch der durchgeführten Berechnung. Intern Spark SQL
verwendet diese zusätzlichen Informationen, um zusätzliche Optimierungen durchzuführen.

Die Migration einer so großen Anwendung nach Spark SQL ist keine leichte Aufgabe, obwohl wir auf derselben Plattform bleiben und der RDD- und der Spark SQL-Code gemischt werden können. Dies bedeutet, dass die Migration inkrementell erfolgen kann, was hilfreich ist, da andere Arbeiten, wie z. B. die Entwicklung von Funktionen, die vom Unternehmen benötigt werden, in den Refactoring-Prozess eingebunden werden können.

Snippet 2. Spark-SQL-Version des Algorithmus für den transitiven Abschluss unter Verwendung der untypisierten DataFrame-API. Beachten Sie, dass Spalten als Zeichenfolgen bezeichnet werden.

Warum ist Spark SQL so schnell?

Mehrere Materialien sind online in Spark SQL verfügbar [1] [2] . Daher werde ich hier nur die wichtigsten Fakten behandeln.

Spark SQL nutzt eine Abfrage
Optimizer (Catalyst), eine optimierte Laufzeit und schnelle In-Memory-Codierung
(Wolfram) für halbstrukturierte, tabellarische Daten. Dies ermöglicht Optimierungen, die
vorher waren unmöglich. Zusätzlich zu grundlegenden regelbasierten Optimierungen – z.
Verschieben von Filtern vor der Auswahl (oft als Filter-Pushdown bezeichnet)
Optimierungsregel in relationalen Abfrageplanern) – Catalyst sammelt Statistiken
über die Datensätze, um kostenbasierte Optimierungen durchzuführen, z. Joins neu anordnen
die Zwischengröße niedrig zu halten oder die bestmögliche Strategie für zu wählen
individuelle Verknüpfungen.

Spark-SQL wird effizient kompiliert
Code für den physischen Plan, mit dem rohe Binärdaten direkt bearbeitet und umgangen werden
Speicherbereinigung und Verringerung des Speicherbedarfs.

Während die Vorteile Funken
Letztendlich würde unser Identitätsgraph schon sehr früh klar sein, davor
In Zukunft müssten wir uns mit den Nachteilen der Lösung auseinandersetzen.

Ein Schritt zurück in die Ergonomie?

In unserer Entscheidung, umzuziehen
Verwenden Sie Spark SQL direkt von Anfang an, da wir wussten, dass wir vielleicht einen Schritt machen werden
Zurück in der Ergonomie. Hier sind einige der Dinge, die wir zu Beginn erlebt haben:

  • Mangel an Ausdruckskraft: Natürlich haben wir damit gerechnet
    dass bestimmte Codeteile nicht migriert werden konnten, da SQL weniger aussagekräftig ist als
    Scala. Außerdem verlassen wir uns auf einige in Java geschriebene externe Bibliotheken. Dies
    Dies war jedoch keine große Einschränkung, da RDD-Code gemischt werden kann
    with SQL.
  • Eingeschränkter Satz von Datentypen: Unterstützung für Spark SQL
    Die beliebtesten Datentypen in strukturierten Daten. Für die meisten reicht es
    Anwendungsfälle, jedoch das Darstellen und Arbeiten mit beliebigen Java-Objekten ist a
    Nicht-Triviales.
  • Verlust der Sicherheit beim Kompilieren: Wohl eines
    Eine der überzeugendsten Funktionen der typisierten RDD-API ist die Beschreibung
    verteilte Berechnung ähnlich, als ob der Code auf der lokalen Scala funktionieren würde
    Sammlungen. Das Design stellt sicher, dass die meisten Fehler beim Kompilieren abgefangen werden
    IDEs und Code-Editoren können das Typensystem der Sprache verwenden, um
    Autovervollständigung und andere Erkenntnisse. Leider kann uns Spark SQL dazu zwingen
    gib diese Vorteile auf.

Diese beiden letzten Einschränkungen
veranlasste uns, unsere Köpfe hart zu kratzen, da sie zwei von uns direkt behinderten
Wichtigste Anforderungen:

  1. Um den Type Checker weiterhin nutzen zu können.
    Unser Team ist bestrebt, das Beste zu verwenden, was Scala für die Typensicherheit bieten kann. Wie ich bereits erwähnte, waren wir mit der RDD-API in diesem Punkt sehr zufrieden. Leider haben wir in letzter Zeit weniger Fortschritte beim Entwerfen typensicherer APIs für die Datenverarbeitung in Spark gemacht, wodurch Entwickler gezwungen sind, weniger idiomatischen Scala-Code zu schreiben, wenn sie mit Spark arbeiten, unter Berücksichtigung des Industriestandards. Dies kann zum Teil auf die Verlagerung des Fokus auf Python zurückgeführt werden, das eine größere Anwenderbasis vor allem unter Datenwissenschaftlern aufweist, und dass die Mechanismen von Scala zur typsicheren Programmierung sehr unkonventionell und eigenwillig sind, was es Entwicklern schwer macht, von verschiedenen Anbietern zu lernen languages.
  2. Um vorhandene Domänentypen wiederzuverwenden und die Kompatibilität mit vorhandenen Schemas zu gewährleisten.
    Wir haben unser Schema in Form von Scala-Fallklassen modelliert, von denen die meisten Member mit Typen außerhalb des unterstützten Bereichs von Spark SQL enthielten. Das allgegenwärtigste Beispiel ist java.util.UUID, das wir überall hatten. Es wäre mit einer Änderung jeder einzelnen Domänenklasse verbunden gewesen, und die Alternative, einen String oder ein Tupel von Longs zu verwenden, ist semantisch weniger aussagekräftig. Der zweite Übertreter war scala.Enumeration. Scala-Aufzählungen sind eine einfache Möglichkeit, disjunkte Alternativen auszudrücken. Wir haben die Werte als ganzzahlige Ordnungszahlen im serialisierten Format dargestellt.

Eine Geschichte von zweieinhalb APIs

Spark bietet zwei Frontends
für seine SQL-Plattform.

Eine davon sind SQL-Strings. Das war
Das anfängliche Front-End für Spark SQL, das in Spark 1.0 als Alpha enthalten ist
Komponente. Unnötig darauf einzugehen, wie umständlich und fehleranfällig es ist
Verfassen einer nicht trivialen Codebasis als eine Reihe von Schritten, die in Zeichenfolgen formuliert sind.
eingebettet in eine Programmiersprache.

Die Dataset-API ist die andere. Das
Autoren haben die DataFrame-API in Spark 1.3 ähnlich wie RDDs eingeführt, aber
untypisiert. In Spark 1.6 haben sie ein typisiertes Gegenstück dazu eingeführt, das sie
benannte die Dataset-API. In Spark 2.0 wurden die beiden APIs unter dem Datensatz vereint
name, der jetzt Funktionen beider Varianten mit opt-in-Typisierung bereitstellt. Wenn
die Nomenklatur war nicht verwirrend genug, PySpark- und SparkR-APIs, bei denen die
typisierter Datensatz existiert nicht, die untypisierte Version wird weiterhin als DataFrame bezeichnet, wohingegen dies in Java Dataset der Fall ist
verwendet für das gleiche Konzept.

Es sollte keine Debatte über
Wählen Sie zwischen Strings und Letzterem, wenn Sie mehr schreiben
Komplexer als einige Ad-hoc-Abfragen in einem Notizbuch, das SQL direkt akzeptieren kann.
Überlegungen zwischen der typisierten und untypisierten Fassade der Dataset-API
ist viel unkomplizierter.

Um ein klareres Bild zu erhalten, lassen Sie uns
sehen Sie, wie sie in Aktion funktionieren!

Der erste Unterschied zwischen den beiden Geschmacksrichtungen tritt sofort auf, wenn eine lokale Sammlung parallelisiert wird.

Snippet 3. Beispieldatensatz von Büchern

Hoppla, ein Fehler beim Kompilieren. Hinweis:
Dies tritt möglicherweise nicht auf, wenn Sie in einer Notebook-Umgebung wie dieser ausgeführt werden
Habe normalerweise den erforderlichen Import.

Ich werde gleich darauf zurückkommen, aber lassen Sie uns vorerst mit einem realistischeren Anwendungsfall fortfahren und eine Parkettdatei lesen.

Die einzige Option ist das Lesen
in einen DataFrame und unter Verwendung der as-Methode mit einem Typ
Anmerkung, um es in das angegebene Schema umzuwandeln. Wenn wir die Methode nachschlagen
Definitionen stellen wir fest, dass beide Methoden eine implizite Encoder-Instanz erfordern. In der Tat, wenn wir uns die anderen Dataset-Methoden ansehen
Wenn Sie einen Datensatz [T] zurückgeben, müssen Sie jeweils einen Encoder [T] angeben.

Encoder

Der Encoder ist das Kernkonzept der typisierten API, die für die Konvertierung zwischen JVM-Objekten und der Laufzeitdarstellung zuständig ist. Spark SQL wird mit Encoder-Implementierungen für eine ausgewählte Klasse von Typen geliefert. Wie uns die Fehlermeldung mitteilt, können diese durch Importieren von spark.implicits._ in den Geltungsbereich gebracht werden. In den oben genannten Fällen werden die Fehlermeldungen ausgeblendet. Encoder werden für primitive JVM-Typen, ihre Box-Versionen, Zeichenfolgen, einige Zeittypen unter java.sql, java.math.BigDecimal, ihre Produkte und Sequenzen definiert (siehe Dokumentation). Karten werden auch mit bestimmten Einschränkungen unterstützt.

Angenommen, wir möchten jedem Buch eine ISBN (International Standard Book Number) zuweisen. Schließlich entwickeln wir ein digitales System. Eine mögliche Implementierung speichert die Nummer in einem einzelnen Long-Feld und stellt Extraktoren für die Teile bereit.

Snippet 4. ISBN-Klasse und modifizierte Buchbeispiele. Wir werden den Fehler hier nicht so einfach beseitigen können.

Das Ausführen dieses Codes führt zu einer Laufzeitausnahme wie in Snippet 5. ISBN ist eine benutzerdefinierte Scala-Klasse, Spark kann sie nicht codieren.

Snippet 5 Laufzeitfehler beim Codieren eines Produktfelds mit einem nicht unterstützten Typ

Das Problem ist leichter zu verstehen, wenn wir versuchen, die Klasse direkt wie in Snippet 6 zu codieren.

Snippet 6. Codieren eines nicht unterstützten Typs

Um dies zu beheben In diesem Fall müssen wir zuerst einen Encoder für ISBNs schreiben und ihn im Geltungsbereich der Website verfügbar machen. Spark bietet hierfür einen Mechanismus über die intern verwendete ExpressionEncoder-Case-Klasse. Snippet 7 stellt eine grundlegende Implementierung des ISBN-Codierers unter Verwendung des ExpressionEncoders von Spark dar.

Snippet 7. Grundlegendes Beispiel des ISBN-Codierers

Die Verwendung des ExpressionEncoders weist erhebliche Nachteile auf. Erstens bekommen wir immer noch das gleiche
Fehler wie in Snippet 6 beim Versuch, unsere Bücher zu serialisieren, d. h. wir können sie nicht einbetten
die ISBN in ein beliebiges Produkt. Dies geschieht, weil der Produktgeber
versucht, beim Ableiten des Schemas für die Felder einen geschlossenen Satz von Alternativen abzugleichen, und berücksichtigt dies nicht
unser frisch definierter ISBN-Encoder. Zweitens, selbst wenn wir es könnten, würden wir es nicht tun
wollen den überflüssigen Strukturwrapper um unseren Wert haben (d. h. wir wollen
Behandeln Sie Fälle auf oberster Ebene und vor Ort unterschiedlich. Ohne welche können wir das nicht
zusätzlicher Kontext, in dem wir uns im Serialisierungsbaum befinden.

An dieser Stelle erhalten die Dinge eine
etwas entmutigend, da das Encoder-Framework dies eindeutig nicht war
Entwickelt, um die Kompilierungszeit zu verlängern.

Frameless

Frameless ist eine hervorragende Bibliothek, die mehr bietet
stark typisierte Dataset API unter anderem. Rahmenlos rollen ihre eigenen
Erweiterbares Encoder-Framework zur Kompilierungszeit, TypedEncoder.

Frameless baut stark auf Scala auf und hat seinen Namen von Shapeless, einer abhängigen typbasierten generischen Programmbibliothek. Aufgrund der Komplexität des Themas ist eine Einführung in die generische Programmierung auf Typebene hier nicht möglich. Glücklicherweise stehen dem interessierten Leser zahlreiche Online-Materialien zur Verfügung.

Ohne zu sehr ins Detail zu gehen, verwendet der Kern des TypedEncoder-Frameworks die implizite Rekursion zur Kompilierungszeit, um den Encoder für den T-Typ abzuleiten. Rahmenlos definiert Instanzen für primitive Typen wie Longs, Ints und übergeordnete Encoder, die diese als Blätter verwenden, wie z. B. Option [T] Seq [T] oder das rekursive Produkt, für das schwere Maschinen auf Typebene erforderlich sind die Arbeit getan. Das Grundgerüst für dieses Framework wird in Snippet 8 gezeigt.

Snippet 8. TypedEncoder-Grundgerüst

Der nächste Schritt ist das Schreiben unseres ISBN-Encoders unter Verwendung dieses Frameworks. Es verarbeitet Nullen und Daten der obersten Ebene für uns, was dazu dient, unseren Code zu vereinfachen (wie in Snippet 9 gezeigt). Hinweis: Vergewissern Sie sich, dass unsere vorherige Definition von Encoder [ISBN] nicht mehr im Geltungsbereich ist. Andernfalls wird ein mehrdeutiger impliziter Argumentfehler angezeigt.

Snippet 9

Schauen wir uns ein zweites Beispiel an und fügen eine Aufzählung mit dem hinzu Format des Ausdrucks. Aufzählungen werden nicht unterstützt, daher müssen wir auch einen Encoder für sie erstellen. Dies wird gezeigt in:

Snippet 10. Ein Enumerationscodierer

Es funktioniert, aber es ist nicht generisch. Jedes Mal, wenn eine neue Enumeration hinzugefügt wird, muss ein neuer Encoder mit demselben Verhalten definiert werden. Das Lösen dieses Problems erfordert ein wenig formlose Magie. Aufzählungstypen werden als Objekte dargestellt, daher müssen wir für jedes einzelne Aufzählungsobjekt einen Encoder generieren. Leider bietet die Sprache keine guten Mechanismen zum Abrufen der Instanz für den angegebenen Objekttyp. Wir müssen entweder die Definitionen ändern, indem wir sie zu impliziten Objekten machen, oder sie impliziten Werten zuweisen. Das erste erfordert, dass wir die Definitionsseite ändern, das zweite fügt Boilerplate hinzu. Das Zeugen-Typ-Tool extrahiert diese Informationen aus dem Compiler und generiert den impliziten Zeugenwert für unsere Singletons. Stellen Sie erneut sicher, dass die vorherige Definition nicht mehr im Gültigkeitsbereich verfügbar ist.

Snippet 11. Generalisierter Enumerationscodierer

Eine andere, typsicherere Methode zur Darstellung von Enums besteht in der Verwendung versiegelter Merkmale. Das Erstellen von Encodern für solche Aufzählungen – oder allgemeiner gesagt, für Koprodukte – erfordert mehr Programmierkenntnisse auf Typebene, als für diesen Beitrag geeignet wäre.

Fazit

Ich hoffe, Ihnen hat dieser Überblick über die Grundlagen von Spark gefallen
SQL und warum mein Unternehmen trotz dessen auf SQL migrieren musste
offensichtliche Einschränkungen in Bezug auf die Typensicherheit. Welche Erfahrungen haben Sie gemacht? Bist du
vor einer ähnlichen herausforderung? Hinterlassen Sie eine Anmerkung in den Kommentaren, die ich gerne hören würde
darüber.

About BusinessIntelligence

Check Also

Schockierende Möglichkeiten Big Data veränderte die Art der Geschäftskredite

Unternehmen, die nach neuem Kapital suchen, stehen vor einigen neuen Veränderungen, auf die sie vorbereitet …

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert.