Blog
NiFi Scripted Components - das fehlende Bindeglied zwischen Skripts und vollständig benutzerdefinierten Komponenten

Benutzerdefinierte Komponenten
Wie wir wahrscheinlich wissen, ist die größte Stärke von Apache Nifi die große Anzahl an fertigen Komponenten. Natürlich gibt es Situationen, in denen wir etwas Spezielles benötigen, das wir mit den verfügbaren Prozessoren nicht realisieren können, da unser Flow sonst unlesbar, unordentlich und hässlich wird - eben so, wie Sie es Ihren Eltern nicht vorstellen möchten. Wenn Sie Erfahrung mit Nifi haben, dann haben Sie wahrscheinlich schon einige Möglichkeiten gefunden, dieses spezielle Problem zu lösen (einige davon werden in diesem Artikel vorgestellt). Die Lösung hängt weitgehend von der Komplexität der Verarbeitung ab. Auch die verfügbaren externen Komponenten sind ein wichtiger Faktor, aber heute werden wir uns auf die internen Funktionen von Nifi konzentrieren. Wenn es sich um etwas Einfaches handelt, das Sie mit einem oder zwei Bash-Befehlen erledigen können, wird der ExecuteStreamCommand-Prozessor den Rest der Arbeit für Sie erledigen. Es ist sehr einfach, in der Regel benötigen Sie nur eine Zeile Code und schon können Sie loslegen. Wenn das nicht ausreicht, ist es an der Zeit, die großen Geschütze aufzufahren.
Was stimmt also nicht mit den Drehbüchern?
Auch wenn Skript-Executors großartige Werkzeuge sind, haben sie einige Nachteile. Wir können sie mehr oder weniger in zwei Arten unterteilen, je nach Art, Ausführlichkeit und Entwicklungsphase, in der sie auftreten.
Entwicklungsphase, funktionale Einschränkungen
Dies sind einige, denen Sie bei der Gestaltung Ihres Ablaufs oder bei der Implementierung begegnen können, um nur einige zu nennen:
- nur zwei ausgehende Beziehungen
- Problem mit der Aktualisierung von Abhängigkeiten
- nur dynamische Eigenschaften
- prozessorähnlich arbeiten
Wie wir sehen können, beziehen sich diese ausschließlich auf die Entwicklungsphase. Wenn wir mehr Beziehungen haben möchten, müssen wir ein Attribut mit Status setzen und später die Flow-Datei mit dem RouteOnAttribute-Prozessor routen. In der Regel möchten wir die Größe unseres Flows auf ein Minimum beschränken. Wenn wir Abhängigkeits-Jars aktualisieren wollen, müssen wir auch die Prozessoren, die sie verwenden, ungültig machen, da sie sonst eine zwischengespeicherte (alte) Version des Jars verwenden. Dynamische Eigenschaften machen es uns unmöglich, sensible Eigenschaften zu verwenden. Schließlich ist die Arbeitsweise problematisch, wenn wir z.B. die Funktionalität eines Controller-Dienstes erreichen wollen.
Es ist möglich, diese Probleme zu umgehen, aber das sind eher Hacks als Lösungen.
Wartungsphase, Hürden der guten Praxis
Anstelle eines klaren und eindeutigen Mangels an Funktionalität können wir hier die sekundären Folgen des Designs sehen. Während sie dem Entwickler, der die erste PoC-Version des Ablaufs erstellt, vielleicht nicht so wichtig erscheinen, können sie jemanden, der ein Jahr später versucht, Änderungen am Ablauf vorzunehmen, definitiv dazu bringen, die Qualifikation und den Verstand des Erstellers in Frage zu stellen. Unter anderem:
- Fehlen von Test-Frameworks oder bewährten Verfahren
- Anreiz zur Verwendung impliziter Argumente
- keine Möglichkeit, die Eigenschaften zu beschreiben
Die erste ist ganz offensichtlich: Wir wollen unsere Änderungen testen und Regressionstests durchführen. Frameworks für automatische Tests sind ein Segen, den wir bei der normalen Programmierung oft nicht genug zu schätzen wissen. Zwei der folgenden Punkte bedürfen einer genaueren Erklärung.
Der Teufel liegt in der Selbstverständlichkeit
Wie wir wissen, ist Nifi voll von impliziten Werten, die wir im gesamten Ablauf in Form von Attributen weitergeben. Die Verwendung von Attributen ist praktisch, aber wir müssen bedenken, dass wir, wenn wir eine benutzerdefinierte Komponente erstellen, irgendwie sichtbar machen müssen, dass wir sie tatsächlich verwenden.
Stellen Sie sich eine Situation vor, in der Sie den Ablauf betreuen und eine Änderung vornehmen möchten, die den Wert eines Attributs verändert. Sie müssen überprüfen, ob das Attribut nirgendwo im Ablauf verwendet wird. Glücklicherweise hat jeder Prozessor eine Dokumentation, die Informationen darüber enthält, welche Attribute er verwendet... mit Ausnahme dieser Skripte. Sie müssen alle Skripte finden, die das Attribut innerhalb des Skriptkörpers verwenden. Mehr noch, sie rufen vielleicht eine Methode auf, die eine Flowfile-Referenz als Argument hat. Dann müssen Sie den Abhängigkeitscode finden und dort überprüfen.
Das kann mit jeder benutzerdefinierten Komponente in Nifi passieren, aber im Falle von Skripten gibt es keinen Anreiz, Werte sofort explizit zu machen. Außerdem ist die Übernahme von Werten aus Attributen in der Tat der bequemste Weg, um sie zu erhalten.
Dokumentation ist wichtig
Stellen Sie sich ein anderes Szenario vor: Sie möchten ein Skript, das jemand anderes erstellt hat, in Ihrem Ablauf verwenden. Der Ersteller war so vernünftig, alle Werte von dynamischen Eigenschaften zu übernehmen, so dass Sie sofort sehen können, welche davon verwendet werden. Das Skript ist groß und die Eigenschaften haben generische Namen. Es ist wohl an der Zeit, den Code zu studieren und die Absichten des Autors herauszufinden. Das ist normalerweise keine angenehme Erfahrung.
Benutzerdefinierte Komponenten zur Rettung
Apache Nifi bietet eine API für alle Arten von Komponenten, so dass der Benutzer problemlos benutzerdefinierte Prozessoren, Controller-Dienste und so weiter erstellen kann. Kurz gesagt, sie lösen alle oben genannten Probleme. Die Frage ist also, warum wir sie nicht standardmäßig verwenden?
Wo liegt also das Problem?
Wir können diese Frage in zwei separate Fragen aufteilen, die leichter zu beantworten sind. Erstens - warum überhaupt Skripte verwenden und zweitens - warum nicht auf benutzerdefinierte Komponenten umsteigen?
Warum Skripte?
Der Grund ist einfach: Sie sind schneller zu implementieren. Um einen benutzerdefinierten Prozessor zu schreiben, müssen Sie ein Projekt erstellen, es kompilieren und sicherstellen, dass alle Bibliotheken korrekt hinzugefügt werden. Das ist ziemlich mühsam. Wenn es sich hingegen um etwas Einfaches handelt, ist das Skript mehr als ausreichend. Das Problem ist, dass wir mit Skripten anfangen, weil die Logik, die sie implementieren, einfach ist, aber später wird es immer komplizierter und wir haben uns bereits für Skripte entschieden. Der einzige Weg ist dann die Migration.
Hürden für nicht-funktionale Verbesserungen
Migrationen zu besseren Lösungen und nicht-funktionale Verbesserungen sind etwas, das wir alle gerne in unserem Projekt durchführen würden. Ich könnte sagen, dass wir das alle gerne tun würden, aber seien wir ehrlich, wir wollen es nicht unbedingt... Es wird die Funktionalität der Lösung nicht verbessern und viel Zeit in Anspruch nehmen, und die einzigen, die das sehen werden, sind die Entwickler, so dass ein Unternehmen es nicht bemerken wird. Außerdem wollen sie neue Funktionalitäten, so dass wir tausend Einträge im Backlog haben. Als ob das nicht schon genug wäre, bedeutet eine neue Lösung auch Änderungen bei der Bereitstellung. Das ist also nicht nur eine Aufgabe für die Entwickler, sondern auch für die Devops... und wir wissen nicht einmal, ob es sich lohnt oder nicht.
Einige der oben genannten Faktoren werden sich nicht ändern, aber wenn wir einen schnellen PoC erstellen könnten, der funktioniert, hätten wir mehr Argumente, um eine solche Migration voranzutreiben.
Rettung für die Migration - geskriptete Komponenten
Nifi bietet mehrere Komponenten, die eine Zwischenlösung sein können, die einige der Migrationsprobleme lösen kann.
- InvokeScriptedProcessor
- ScriptedTransformRecord
- SimpleScriptedLookupService
- ScriptedActionHandler
- ScriptedLookupService
- ScriptedReader
- ScriptedRecordSetWriter
- ScriptedRecordSink
- ScriptedRulesEngine
- ScriptedReportingTask
Diese Komponenten funktionieren auf ganz einfache Weise. Sie stellen eine benutzerdefinierte Implementierung im Körper der Komponente bereit, die dann entsprechend transformiert wird.
Wie verwenden Sie es?
Nehmen wir das Beispiel des Prozessors, der zwei Eigenschaften und zwei Beziehungen hat.
class ExampleProc implements Processor {
public static final PropertyDescriptor REQUIRED_PROPERTY = new PropertyDescriptor.Builder()
.name("required property")
.displayName("Required Property")
.description("Description of the required property, can be as detailed as we want it to be")
.required(true)
.addValidator(StandardValidators.NON_BLANK_VALIDATOR)
.expressionLanguageSupported(ExpressionLanguageScope.VARIABLE_REGISTRY)
.build()
public static final PropertyDescriptor OPTIONAL_PROPERTY = new PropertyDescriptor.Builder()
.name("optional property")
.displayName("Optional Property")
.description("Description of the optional property, can be as detailed as we want it to be")
.required(false)
.addValidator(StandardValidators.NON_BLANK_VALIDATOR)
.expressionLanguageSupported(ExpressionLanguageScope.FLOWFILE_ATTRIBUTES)
.build()
public static final Relationship REL_SUCCESS = new Relationship.Builder()
.name("success")
.description("Description of the success relationship, can be as detailed as we want it to be")
.build()
public static final Relationship REL_FAILURE = new Relationship.Builder()
.name("failure")
.description("Description of the failure relationship, can be as detailed as we want it to be")
.build()
@Override
void initialize(ProcessorInitializationContext processorInitializationContext) {}
@Override
Set<Relationship> getRelationships() {
return new HashSet<>(Arrays.asList(REL_SUCCESS, REL_FAILURE));
}
@Override
void onTrigger(ProcessContext processContext, ProcessSessionFactory processSessionFactory) throws ProcessException {
}
@Override
Collection<ValidationResult> validate(ValidationContext validationContext) {
return validationContext.getProperties().entrySet().stream()
.map{e -> e.getKey().validate(e.getValue(), validationContext)}
.collect(Collectors.toSet())
}
@Override
PropertyDescriptor getPropertyDescriptor(String s) {
return getPropertyDescriptors().find {p -> p.getName().equalsIgnoreCase(s)}
}
@Override
void onPropertyModified(PropertyDescriptor propertyDescriptor, String s, String s1) {}
@Override
List<PropertyDescriptor> getPropertyDescriptors() {
return Arrays.asList(REQUIRED_PROPERTY, OPTIONAL_PROPERTY)
}
@Override
String getIdentifier() {
return "Example Processor"›
}
}
Dies ist der Code eines Prozessors, der buchstäblich nichts tut, aber wenn wir ihn in eine Script Body-Eigenschaft von InvokeScriptedProcessor einfügen, sehen wir dies:

Was sind also die Änderungen? Alle im Code definierten Eigenschaften sind im Prozessor sichtbar, sie sind nicht dynamisch und haben eine Dokumentation in der Nifi-Benutzeroberfläche. Wir können auch die im Code definierten Beziehungen sehen, ebenfalls mit Dokumentation.
Wie sieht es mit Tests aus?
Wenn wir davon ausgehen, dass Prozessoren mit Skripten irgendwo zwischen Skripten und benutzerdefinierten Komponenten angesiedelt sind, dann befinden sich Test-Frameworks zwischen Skripten und benutzerdefinierten Komponenten. Das liegt daran, dass Sie sie nicht im Hauptteil des Prozessors durchführen können, sondern ein Projekt mit Unit-Tests einrichten müssen. Beispiel:
class ExampleProcSpec extends Specification {
def setup() {
runner = TestRunners.newTestRunner(new ExampleProcessor())
runner.setProperty(ExampleProcessor.REQUIRED_PROPERTY, “some-value”)
}
def "test"(){
given:
runner.enqueue(input.getBytes("UTF-8"))
when:
runner.run(1)
then:
runner.getFlowFilesForRelationship(ExampleProc.REL_SUCCESS).size() == 1
}
}
Zeigen Sie mir, was Sie drauf haben!
Ok, sieht gut aus, aber was nun? Wie bereits erwähnt, ist dies ein Schritt in Richtung benutzerdefinierter Komponenten. Damit er gut ist, muss er zwei Funktionen haben:
- die Migration zu benutzerdefinierten Komponenten zu erleichtern (das Endziel wäre, alles, was wir wollen, als benutzerdefinierte Komponenten zu haben)
- in irgendeiner Weise besser sein als die Skripte (zeigen Sie die Vorteile der Migration)
Migrationshilfen
Es gibt einige Vorteile bei der Migration. Lassen Sie uns diese durchgehen
- Wir müssen keine neuen Bereitstellungspipelines erstellen - wenn wir den Code entweder in den Skriptkörper einfügen oder ein beliebiges Jar auf dem Worker verwenden, genau wie im Fall der Skripte.
- Weniger Aufwand bei der Erstentwicklung - normalerweise müssten wir die Komponentenklasse implementieren, ein Maven-Projekt erstellen, Abhängigkeiten konfigurieren und es auf Nifi bereitstellen sowie alle Arten von Problemen behandeln. Mit dieser Lösung können wir die Projekterstellung und -bereitstellung überspringen, was manchmal der zeitaufwändigste Teil sein kann.
Lassen Sie uns auch einen Blick darauf werfen, welche Verbesserungen gegenüber der Verwendung von Skripten möglich sind.
- Nicht nur dynamische Eigenschaften - wir können Validatoren für Eigenschaftswerte einrichten, Eigenschaften optional oder erforderlich machen und alles, was mit der Erstellung benutzerdefinierter Komponenten einhergeht.
- Mehr Arten von Komponenten - obwohl wir nicht alle haben können, sind wir nicht mehr nur an Prozessoren gebunden
- Es ist einfacher, gute Praktiken zu befolgen und die Dinge eindeutig zu halten - Dokumentation, klare Anforderungen an Eigenschaften helfen, die Flussstruktur sauber zu halten
Und das ist das Ende der guten Seiten
In dem Moment, in dem Sie denken, dass InvokeScripted* die benutzerdefinierten Komponenten vielleicht ersetzen könnte, nun... hier ist Ihr Eimer mit kaltem Wasser.
- Problem der Abhängigkeitsaktualisierung - immer noch vorhanden und nicht behoben
- Keine sensiblen Werte - aufgrund der Art und Weise, wie es implementiert ist, können keine sensiblen Werte gespeichert werden. Wenn wir außerdem versuchen, sensible Parameter zu verwenden, wird wahrscheinlich der gesamte Ablauf unterbrochen.
- Testen - Benutzerdefinierte Komponenten sind immer noch viel besser für Unit-Tests geeignet.
Und was machen wir jetzt?
Fazit: Wenn Sie das Gefühl haben, dass Ihre Skripte ein Upgrade gebrauchen könnten, haben Sie wenige Argumente, die dafür sprechen. Denken Sie daran, dass Skripte im Allgemeinen schneller zu erstellen sind und ihren Platz im Nifi-Ökosystem haben. Letztendlich hat jedes Projekt seine eigene Spezifikation und die Entscheidung liegt auf Ihren Schultern und denen Ihrer Teamkollegen. Prost!
Möchten Sie mehr über Apache NiFi lesen? Sehen Sie sich unsere Blog-Serie NiFi Ingestion Blog Series an
Verfasst von
Tomasz Nazarewicz
Unsere Ideen
Weitere Blogs
Contact



