Gute Implementierungsstrategien zum Einkapseln gemeinsam genutzter Daten in eine Software-Pipeline


13

Ich arbeite daran, bestimmte Aspekte eines vorhandenen Webdienstes neu zu faktorisieren. Die Implementierung der Service-APIs erfolgt über eine Art "Verarbeitungs-Pipeline", in der Aufgaben nacheinander ausgeführt werden. Es ist nicht überraschend, dass für spätere Tasks möglicherweise Informationen erforderlich sind, die von früheren Tasks berechnet wurden. Derzeit erfolgt dies durch Hinzufügen von Feldern zu einer "Pipeline-Status" -Klasse.

Ich habe gedacht (und gehofft?), Dass es einen besseren Weg gibt, Informationen zwischen Pipelineschritten auszutauschen, als ein Datenobjekt mit zig Feldern zu haben, von denen einige für einige Verarbeitungsschritte und andere nicht sinnvoll sind. Es wäre ein großer Schmerz, diese Klasse threadsicher zu machen (ich weiß nicht, ob es überhaupt möglich wäre), es gibt keine Möglichkeit, über ihre Invarianten zu argumentieren (und es ist wahrscheinlich, dass es keine gibt).

Ich habe das Buch mit den Gang of Four-Designmustern durchgeblättert, um Inspiration zu finden, aber ich hatte nicht das Gefühl, dass darin eine Lösung steckt (Memento war einigermaßen im selben Sinne, aber nicht ganz). Ich habe auch online gesucht, aber sobald Sie nach "Pipeline" oder "Workflow" suchen, werden Sie entweder mit Unix-Pipes-Informationen oder proprietären Workflow-Engines und Frameworks überflutet.

Meine Frage lautet: Wie würden Sie das Problem der Aufzeichnung des Ausführungsstatus einer Software-Verarbeitungs-Pipeline angehen, damit spätere Aufgaben die von früheren berechneten Informationen verwenden können? Ich denke, der Hauptunterschied zu Unix-Pipes ist, dass Sie sich nicht nur um die Ausgabe der unmittelbar vorhergehenden Aufgabe kümmern.


Wie gewünscht, ein Pseudocode zur Veranschaulichung meines Anwendungsfalls:

Das Objekt "Pipeline-Kontext" verfügt über eine Reihe von Feldern, die die verschiedenen Pipeline-Schritte füllen / lesen können:

public class PipelineCtx {
    ... // fields
    public Foo getFoo() { return this.foo; }
    public void setFoo(Foo aFoo) { this.foo = aFoo; }
    public Bar getBar() { return this.bar; }
    public void setBar(Bar aBar) { this.bar = aBar; }
    ... // more methods
}

Jeder der Pipelineschritte ist auch ein Objekt:

public abstract class PipelineStep {
    public abstract PipelineCtx doWork(PipelineCtx ctx);
}

public class BarStep extends PipelineStep {
    @Override
    public PipelineCtx doWork(PipelieCtx ctx) {
        // do work based on the stuff in ctx
        Bar theBar = ...; // compute it
        ctx.setBar(theBar);

        return ctx;
    }
}

Ähnliches gilt für eine FooStepHypothese, für die der von BarStep berechnete Balken zusammen mit anderen Daten erforderlich sein könnte. Und dann haben wir den wirklichen API-Aufruf:

public class BlahOperation extends ProprietaryWebServiceApiBase {
    public BlahResponse handle(BlahRequest request) {
        PipelineCtx ctx = PipelineCtx.from(request);

        // some steps happen here
        // ...

        BarStep barStep = new BarStep();
        barStep.doWork(crx);

        // some more steps maybe
        // ...

        FooStep fooStep = new FooStep();
        fooStep.doWork(ctx);

        // final steps ...

        return BlahResponse.from(ctx);
    }
}

6
Überqueren Sie nicht den Pfosten, sondern markieren Sie, dass sich ein Mod bewegen soll
Ratschenfreak

1
Ich denke, ich sollte mehr Zeit damit verbringen, mich mit den Regeln vertraut zu machen. Vielen Dank!
RuslanD

1
Vermeiden Sie persistente Datenspeicherung für Ihre Implementierung, oder steht derzeit etwas zur Diskussion?
CokoBWare

1
Hallo RuslanD und herzlich willkommen! Dies ist in der Tat eher für Programmierer als für Stack Overflow geeignet, daher haben wir die SO-Version entfernt. Denken Sie daran, was @ratchetfreak erwähnt hat. Sie können die Aufmerksamkeit auf die Moderation lenken und eine Frage stellen, die auf eine geeignetere Site migriert werden soll, ohne dass ein Cross-Post erforderlich ist. Als Faustregel für die Auswahl zwischen den beiden Standorten gilt, dass Programmierer Probleme haben, die auftreten, wenn Sie sich vor dem Whiteboard befinden, auf dem Ihre Projekte erstellt werden, und dass Stapelüberlauf eher technische Probleme verursacht (z. B. Implementierungsprobleme). Weitere Details finden Sie in unseren FAQ .
Yannis

1
Wenn Sie die Architektur in eine verarbeitende DAG (Directed Acyclic Graph) anstelle einer Pipeline ändern, können Sie die Ergebnisse früherer Schritte explizit übergeben.
Patrick

Antworten:


4

Der Hauptgrund für die Verwendung eines Pipeline-Designs besteht darin, dass Sie die Stufen entkoppeln möchten. Entweder, weil eine Stufe in mehreren Pipelines verwendet werden kann (wie die Unix-Shell-Tools), oder weil Sie einen gewissen Skalierungsvorteil erzielen (dh Sie können problemlos von einer Architektur mit einem Knoten zu einer Architektur mit mehreren Knoten wechseln).

In jedem Fall muss jeder Phase in der Pipeline alles gegeben werden, was sie für ihre Arbeit benötigt. Es gibt keinen Grund, warum Sie keinen externen Speicher (z. B. eine Datenbank) verwenden können, aber in den meisten Fällen ist es besser, die Daten von einer Stufe zur nächsten zu übertragen.

Dies bedeutet jedoch nicht, dass Sie mit jedem möglichen Feld ein großes Nachrichtenobjekt übergeben müssen oder sollten (siehe unten). Stattdessen sollte jede Phase in der Pipeline Schnittstellen für ihre Eingabe- und Ausgabenachrichten definieren, die nur die Daten identifizieren, die die Phase benötigt.

Sie haben dann viel Flexibilität bei der Implementierung Ihrer eigentlichen Nachrichtenobjekte. Ein Ansatz besteht darin, ein großes Datenobjekt zu verwenden, das alle erforderlichen Schnittstellen implementiert. Eine andere Möglichkeit besteht darin, Wrapper-Klassen um eine einfache herum zu erstellen Map. Eine weitere Möglichkeit besteht darin, eine Wrapper-Klasse für eine Datenbank zu erstellen.


1

Es gibt ein paar Gedanken, die einem in den Sinn kommen, von denen einer ist, dass ich nicht genug Informationen habe.

  • Produziert jeder Schritt Daten, die über die Pipeline hinaus verwendet werden, oder interessieren uns nur die Ergebnisse der letzten Phase?
  • Gibt es viele Probleme mit Big Data? dh Speicherprobleme, Geschwindigkeitsprobleme usw

Die Antworten ließen mich wahrscheinlich genauer über das Design nachdenken, aber auf der Grundlage dessen, was Sie sagten, gibt es zwei Ansätze, über die ich wahrscheinlich zuerst nachdenken würde.

Strukturieren Sie jede Stufe als eigenes Objekt. Die n-te Stufe hätte 1 bis n-1 Stufen als eine Liste von Delegierten. Die einzelnen Stufen kapseln die Daten und die Verarbeitung der Daten. Reduzierung der Gesamtkomplexität und der Felder in jedem Objekt. Sie können auch in späteren Phasen nach Bedarf auf die Daten zugreifen, indem Sie die Delegierten überqueren. Sie haben immer noch eine ziemlich enge Kopplung über alle Objekte, da die Ergebnisse der Phasen (dh alle Attribute) wichtig sind, diese jedoch erheblich reduziert sind und jede Phase / jedes Objekt wahrscheinlich besser lesbar und verständlicher ist. Sie können die Thread-Sicherheit erhöhen, indem Sie die Liste der Stellvertreter verzögern und die Stellvertreterliste in jedem Objekt nach Bedarf mit einer thread-sicheren Warteschlange füllen.

Alternativ würde ich wahrscheinlich etwas Ähnliches tun, wie Sie es tun. Ein massives Datenobjekt, das Funktionen durchläuft, die jede Phase darstellen. Dies ist oft viel schneller und leichter, aber komplexer und fehleranfälliger, da es sich nur um eine große Menge von Datenattributen handelt. Offensichtlich nicht threadsicher.

Ehrlich gesagt habe ich die spätere öfter für ETL und einige andere ähnliche Probleme gemacht. Ich habe mich eher auf die Leistung als auf die Wartbarkeit der Daten konzentriert. Sie waren auch Unikate, die nicht mehr verwendet werden würden.


1

Dies sieht aus wie ein Kettenmuster in GoF.

Ein guter Ausgangspunkt wäre, sich anzusehen, was die Commons-Chain macht.

Eine beliebte Technik zum Organisieren der Ausführung komplexer Verarbeitungsabläufe ist das "Chain of Responsibility" -Muster, wie es (unter anderem) im klassischen "Gang of Four" -Musterbuch beschrieben ist. Obwohl die grundlegenden API-Verträge, die zur Implementierung dieses Entwurfsmusters erforderlich sind, äußerst einfach sind, ist es nützlich, eine Basis-API zu haben, die die Verwendung des Musters erleichtert und (was noch wichtiger ist) die Komposition von Befehlsimplementierungen aus verschiedenen Quellen fördert.

Zu diesem Zweck modelliert die Ketten-API eine Berechnung als eine Reihe von "Befehlen", die zu einer "Kette" kombiniert werden können. Die API für einen Befehl besteht aus einer einzelnen Methode ( execute()), der ein "Kontext" -Parameter übergeben wird, der den dynamischen Status der Berechnung enthält, und deren Rückgabewert ein Boolescher Wert ist, der bestimmt, ob die Verarbeitung für die aktuelle Kette abgeschlossen wurde oder nicht ( true) oder ob die Verarbeitung an den nächsten Befehl in der Kette delegiert werden soll (false).

Die "Kontext" -Abstraktion dient zum Isolieren von Befehlsimplementierungen von der Umgebung, in der sie ausgeführt werden (z. B. ein Befehl, der entweder in einem Servlet oder in einem Portlet verwendet werden kann, ohne direkt an die API-Verträge einer dieser Umgebungen gebunden zu sein). Bei Befehlen, die Ressourcen vor der Delegierung zuweisen und anschließend bei der Rückgabe freigeben müssen (selbst wenn ein delegierter Befehl eine Ausnahme auslöst), bietet die Erweiterung "filter" zu "command" eine postprocess()Methode für diese Bereinigung. Schließlich können Befehle gespeichert und in einem "Katalog" nachgeschlagen werden, um die Entscheidung zu verschieben, welcher Befehl (oder welche Kette) tatsächlich ausgeführt wird.

Um den Nutzen der Chain of Responsibility-Muster-APIs zu maximieren, werden die grundlegenden Schnittstellenverträge auf eine Weise definiert, bei der keine anderen Abhängigkeiten als ein geeignetes JDK vorliegen. Komfort-Basisklassen-Implementierungen dieser APIs sowie speziellere (aber optionale) Implementierungen für die Webumgebung (dh Servlets und Portlets) werden bereitgestellt.

Da Befehlsimplementierungen so konzipiert sind, dass sie diesen Empfehlungen entsprechen, sollte es möglich sein, die Chain of Responsibility-APIs im "Front Controller" eines Webanwendungsframeworks (wie Struts) zu verwenden, sie jedoch auch im Unternehmen verwenden zu können Logik- und Persistenzstufen zur Modellierung komplexer Rechenanforderungen durch Komposition. Darüber hinaus ermöglicht die Aufteilung einer Berechnung in diskrete Befehle, die in einem Mehrzweckkontext ausgeführt werden, die einfachere Erstellung von Befehlen, die von einer Einheit getestet werden können, da die Auswirkung der Ausführung eines Befehls direkt gemessen werden kann, indem die entsprechenden Zustandsänderungen in dem bereitgestellten Kontext beobachtet werden ...


0

Eine erste Lösung, die ich mir vorstellen kann, ist, die Schritte explizit zu machen. Jeder von ihnen wird zu einem Objekt, das Daten verarbeiten und an das nächste Prozessobjekt übertragen kann. Jeder Prozess erzeugt ein neues (im Idealfall unveränderliches) Produkt, sodass keine Wechselwirkung zwischen den Prozessen und kein Risiko durch Datenaustausch besteht. Wenn einige Prozesse zeitaufwendiger sind als andere, können Sie einen Puffer zwischen zwei Prozessen platzieren. Wenn Sie einen Scheduler für das Multithreading korrekt ausnutzen, werden mehr Ressourcen zum Leeren der Puffer zugewiesen.

Eine zweite Lösung könnte darin bestehen, "Nachricht" anstelle einer Pipeline zu denken, möglicherweise mit einem dedizierten Framework. Sie haben dann einige "Akteure", die Nachrichten von anderen Akteuren empfangen und andere Nachrichten an andere Akteure senden. Sie organisieren Ihre Akteure in einer Pipeline und geben Ihre Primärdaten an einen ersten Akteur weiter, der die Kette initiiert. Es findet keine gemeinsame Nutzung von Daten statt, da die gemeinsame Nutzung durch das Senden von Nachrichten ersetzt wird. Ich weiß, dass das Schauspielermodell von Scala in Java verwendet werden kann, da es hier nichts Spezifisches von Scala gibt, aber ich habe es nie in einem Java-Programm verwendet.

Die Lösungen sind ähnlich und Sie können die zweite mit der ersten implementieren. Grundsätzlich besteht das Hauptkonzept darin, sich mit unveränderlichen Daten zu befassen, um die herkömmlichen Probleme aufgrund des Datenaustauschs zu vermeiden und explizite und unabhängige Einheiten zu erstellen, die die Prozesse in Ihrer Pipeline darstellen. Wenn Sie diese Bedingungen erfüllen, können Sie problemlos klare, einfache Pipelines erstellen und in einem parallelen Programm verwenden.


Hey, ich habe meine Frage mit einem Pseudocode aktualisiert - wir haben die Schritte tatsächlich explizit.
RuslanD
Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.