Die Philosophie der Datenfluß ist, dass PTransform
die Haupteinheit der Abstraktion und composability, das heißt, eine beliebige in sich geschlossene Datenverarbeitungsaufgabe als PTransform
eingekapselt werden soll. Dies beinhaltet die Aufgabe, sich mit einem Speichersystem eines Drittanbieters zu verbinden: Daten von irgendwo aufzunehmen oder irgendwo zu exportieren.
Nehmen Sie zum Beispiel Google Cloud Datastore. Im Code-Schnipsel:
PCollection<Entity> entities =
p.apply(DatastoreIO.readFrom(dataset, query));
...
p.apply(some processing)
.apply(DatastoreIO.writeTo(dataset));
der Rückgabetyp von DatastoreIO.readFrom(dataset, query)
ist eine Unterklasse von PTransform<PBegin, PCollection<Entity>>
, und die Art der DatastoreIO.writeTo(dataset)
ist eine Unterklasse von PTransform<PCollection<Entity>, PDone>
.
Es ist wahr, dass diese Funktionen unter der Haube sind implementiert, um die Source
und Sink
Klassen verwenden, aber für einen Benutzer, der gerade etwas zu Datastor lesen will oder schreiben, das ist ein Detail Implementierung, dass in der Regel nicht (jedoch sollte Angelegenheit, Siehe den Hinweis am Ende dieser Antwort zum Aussetzen der Klasse Source
oder Sink
). Jeder Connector oder jede andere Datenverarbeitungsaufgabe ist ein PTransform
.
Hinweis: Derzeit Anschlüsse, die von irgendwo gelesen neigen PTransform<PBegin, PCollection<T>>
zu sein, und Anschlüsse, die dazu neigen, irgendwo schreiben PTransform<PCollection<T>, PDone>
zu sein, aber wir erwägen Optionen zu erleichtern Anschlüsse in flexiblerer Weise zu nutzen (zum Beispiel Lesen von einem PCollection
von Dateinamen).
Allerdings ist dieses Detail natürlich wichtig für jemanden, der einen neuen Stecker implementieren möchte. Insbesondere können Sie fragen:
Q: Warum brauche ich die Klassen Source
und Sink
überhaupt, wenn ich nur meinen Stecker als PTransform implementieren könnte?
A: Wenn Sie Ihren Anschluss von nur mithilfe der integrierten in-Transformationen (wie ParDo
, implementieren können GroupByKey
usw.), das ist eine absolut gültige Möglichkeit, einen Connector zu entwickeln. Die Klassen Source
und Sink
bieten jedoch einige Low-Level-Funktionen, die für den Fall, dass Sie sie benötigen, mühsam oder unmöglich wären, sich selbst zu entwickeln.
Zum Beispiel BoundedSource
und UnboundedSource
bieten Haken zu steuern, wie parallelisiert geschieht (sowohl Anfangs- und dynamische Arbeit Rebalancing - BoundedSource.splitIntoBundles
, BoundedReader.splitAtFraction
), während diese Haken sind momentan nicht für beliebige DoFn
s ausgesetzt.
Sie technisch einen Parser für ein Dateiformat durch einen DoFn<FilePath, SomeRecord>
zu schreiben, die den Dateinamen als Eingabe verwendet, liest die Datei und sendet SomeRecord
implementieren könnte, aber diese DoFn
würde Lese Teile der Datei auf mehrere Arbeiter nicht in der Lage sein, dynamisch parallelisieren falls die Datei zur Laufzeit sehr groß ist. Auf der anderen Seite, FileBasedSource
hat diese Fähigkeit eingebaut, sowie die Handhabung von Glob-Filemustern und dergleichen.
Ebenso könnten Sie versuchen, einen Anschluss für ein Streaming-System implementieren, indem eine DoFn
Implementierung, die ein Blindelement als Eingabe verwendet, stellt eine Verbindung her und Bäche alle Elemente in ProcessingContext.output()
, aber DoFn
s derzeit nicht unterstützt unbegrenzten Mengen des Schreibens Sie können auch die Checkpointing- und Deduplizierungsmaschinerie unterstützen, die für die starken Konsistenzgarantien benötigt wird, die Dataflow Streaming-Pipelines verleiht. UnboundedSource
unterstützt das alles.
Sink
(genauer gesagt, die Write.to()
PTransform
) ist auch interessant: es ist nur eine zusammengesetzte Transformation, die Sie selbst schreiben könnte, wenn man wollte (dh es hat keine hartcodierte Unterstützung in der Datenfluß-Läufer oder Backend), aber Es wurde unter Berücksichtigung typischer verteilter Fehlertoleranzprobleme entwickelt, die beim parallelen Schreiben von Daten in ein Speichersystem auftreten, und bietet Hooks, die Sie zwingen, diese Probleme zu berücksichtigen.: zB weil Datenbündel parallel geschrieben werden, und einige Bundles können für Fehlertoleranz wiederholt oder dupliziert werden, es gibt einen Haken zum "Festschreiben" nur der Ergebnisse der erfolgreich abgeschlossenen Bundles (WriteOperation.finalize
).
Fassen wir zusammen: mit Source
oder Sink
APIs ein Verbinder hilft Ihnen in einer Weise strukturieren Sie den Code zu entwickeln, die in einem verteilten Verarbeitungs Einstellung funktionieren gut, und die Source-APIs geben Sie erweiterte Funktionen des Frameworks existieren. Aber wenn es sich bei Ihrem Connector um einen sehr einfachen Connector handelt, den Sie nicht benötigen, können Sie den Connector einfach aus anderen integrierten Transformationen zusammenstellen.
Frage: Angenommen, ich entscheide mich für die Verwendung von Source
und Sink
. Dann, wie verpacke ich meinen Verbinder als eine Bibliothek: sollte ich nur die Source
oder Sink
Klasse bereitstellen, oder sollte ich es in eine PTransform
verpacken?
A: Ihr Anschluss schließlich als PTransform
verpackt werden soll, so dass der Benutzer kann nur p.apply()
es in ihrer Pipeline. Unter der Haube kann Ihre Transformation jedoch Klassen Source
und Sink
verwenden.
Ein übliches Muster ist die Source
und Sink
Klassen sowie zu belichten, die Verwendung des Fluent Builder Muster zu machen, und lassen sie der Benutzer in eine wickeln Read.from()
oder Write.to()
verwandeln sich, aber dies ist keine strenge Anforderung.
Große Frage! Wir schreiben jetzt eine gründliche Antwort. –