Einführung in Apache Storm

Einführung in Apache Storm

 

1. Überblick

Dieses Tutorial ist eine Einführung inApache Storm, a distributed real-time computation system.

Wir konzentrieren uns auf und behandeln:

  • Was genau ist Apache Storm und welche Probleme löst es?

  • Seine Architektur und

  • Verwendung in einem Projekt

2. Was ist Apache Storm?

Apache Storm ist ein freies und Open Source verteiltes System für Echtzeitberechnungen.

Es bietet Fehlertoleranz, Skalierbarkeit und garantiert Datenverarbeitung und eignet sich besonders gut für die Verarbeitung unbegrenzter Datenströme.

Einige gute Anwendungsfälle für Storm können die Verarbeitung von Kreditkartenvorgängen zur Betrugserkennung oder die Verarbeitung von Daten aus Smart Homes zur Erkennung fehlerhafter Sensoren sein.

Storm ermöglicht die Integration in verschiedene auf dem Markt erhältliche Datenbanken und Warteschlangensysteme.

3. Maven-Abhängigkeit

Bevor wir Apache Storm verwenden, müssen wirthe storm-core dependency in unser Projekt aufnehmen:


    org.apache.storm
    storm-core
    1.2.2
    provided

Wir sollten nurprovided scope if verwenden, wenn wir unsere Anwendung auf dem Storm-Cluster ausführen möchten.

Um die Anwendung lokal auszuführen, können wir einen sogenannten lokalen Modus verwenden, der den Storm-Cluster in einem lokalen Prozess simuliert. In diesem Fall sollten wir dieprovided. entfernen

4. Datenmodell

Das Datenmodell von Apache Storm besteht aus zwei Elementen: Tupeln und Streams.

4.1. Tupel

ATuple ist eine geordnete Liste benannter Felder mit dynamischen Typen. This means that we don’t need to explicitly declare the types of the fields.

Storm muss wissen, wie alle in einem Tupel verwendeten Werte serialisiert werden. Standardmäßig können bereits primitive Typen,Strings undbyte Arrays serialisiert werden.

Und da Storm die Kryo-Serialisierung verwendet, müssen wir den Serializer mitConfig registrieren, um die benutzerdefinierten Typen zu verwenden. Wir können dies auf zwei Arten tun:

Zuerst können wir die Klasse registrieren, um sie mit ihrem vollständigen Namen zu serialisieren:

Config config = new Config();
config.registerSerialization(User.class);

In einem solchen Fall serialisiert Kryo die Klasse mitFieldSerializer. Standardmäßig werden alle nicht vorübergehenden Felder der Klasse, sowohl private als auch öffentliche, serialisiert.

Alternativ können wir sowohl die zu serialisierende Klasse als auch den Serializer bereitstellen, den Storm für diese Klasse verwenden soll:

Config config = new Config();
config.registerSerialization(User.class, UserSerializer.class);

Um den benutzerdefinierten Serializer zu erstellen, benötigen wirto extend the generic classSerializer , das zwei Methoden hat:write andread.

4.2. Strom

AStream ist die Kernabstraktion im Storm-Ökosystem. The Stream is an unbounded sequence of tuples.

Storms ermöglicht die parallele Verarbeitung mehrerer Streams.

Jeder Stream hat eine ID, die bei der Deklaration angegeben und zugewiesen wird.

5. Topologie

Die Logik der Echtzeit-Storm-Anwendung ist in die Topologie integriert. Die Topologie besteht ausspouts undbolts.

5.1. Tülle

Ausläufe sind die Quellen der Ströme. Sie senden Tupel an die Topologie.

Tuples können von verschiedenen externen Systemen wie Kafka, Kestrel oder ActiveMQ gelesen werden.

Ausläufe könnenreliable oderunreliable sein. Reliable bedeutet, dass der Auslauf antworten kann, dass das Tupel, das nicht von Storm verarbeitet wurde, nicht verarbeitet wurde. Unreliable bedeutet, dass der Auslauf nicht antwortet, da er einen Feuer-und-Vergessen-Mechanismus verwendet, um die Tupel auszugeben.

Um den benutzerdefinierten Auslauf zu erstellen, müssen wir dieIRichSpout -Sinterschnittstelle implementieren oder eine Klasse erweitern, die die Schnittstelle bereits implementiert, z. B. eine abstrakteBaseRichSpout-Klasse.

Erstellen wir einenunreliable -Spout:

public class RandomIntSpout extends BaseRichSpout {

    private Random random;
    private SpoutOutputCollector outputCollector;

    @Override
    public void open(Map map, TopologyContext topologyContext,
      SpoutOutputCollector spoutOutputCollector) {
        random = new Random();
        outputCollector = spoutOutputCollector;
    }

    @Override
    public void nextTuple() {
        Utils.sleep(1000);
        outputCollector.emit(new Values(random.nextInt(), System.currentTimeMillis()));
    }

    @Override
    public void declareOutputFields(OutputFieldsDeclarer outputFieldsDeclarer) {
        outputFieldsDeclarer.declare(new Fields("randomInt", "timestamp"));
    }
}

Unser CustomRandomIntSpout wird zufällige Integer und Zeitstempel jede Sekunde erzeugen.

5.2. Bolt

Bolts process tuples in the stream. Sie können verschiedene Operationen wie Filterung, Aggregation oder benutzerdefinierte Funktionen ausführen.

Einige Operationen erfordern mehrere Schritte, und daher müssen in solchen Fällen mehrere Schrauben verwendet werden.

Um die benutzerdefiniertenBolt zu erstellen, müssen wirIRichBolt or für einfachere Operationen implementieren.IBasicBolt Schnittstelle.

Für die Implementierung vonBolt.  stehen auch mehrere Hilfsklassen zur Verfügung. In diesem Fall verwenden wirBaseBasicBolt:

public class PrintingBolt extends BaseBasicBolt {
    @Override
    public void execute(Tuple tuple, BasicOutputCollector basicOutputCollector) {
        System.out.println(tuple);
    }

    @Override
    public void declareOutputFields(OutputFieldsDeclarer outputFieldsDeclarer) {

    }
}

Diese benutzerdefiniertenPrintingBolt drucken einfach alle Tupel auf die Konsole.

6. Erstellen einer einfachen Topologie

Lassen Sie uns diese Ideen zu einer einfachen Topologie zusammenfassen. Unsere Topologie wird einen Auslauf und drei Schrauben haben.

6.1. RandomNumberSpout

Am Anfang erstellen wir einen unzuverlässigen Auslauf. Er generiert jede Sekunde zufällige Ganzzahlen aus dem Bereich (0,100):

public class RandomNumberSpout extends BaseRichSpout {
    private Random random;
    private SpoutOutputCollector collector;

    @Override
    public void open(Map map, TopologyContext topologyContext,
      SpoutOutputCollector spoutOutputCollector) {
        random = new Random();
        collector = spoutOutputCollector;
    }

    @Override
    public void nextTuple() {
        Utils.sleep(1000);
        int operation = random.nextInt(101);
        long timestamp = System.currentTimeMillis();

        Values values = new Values(operation, timestamp);
        collector.emit(values);
    }

    @Override
    public void declareOutputFields(OutputFieldsDeclarer outputFieldsDeclarer) {
        outputFieldsDeclarer.declare(new Fields("operation", "timestamp"));
    }
}

6.2. FilteringBolt

Als Nächstes erstellen wir eine Schraube, die alle Elemente mitoperation gleich 0 herausfiltert:

public class FilteringBolt extends BaseBasicBolt {
    @Override
    public void execute(Tuple tuple, BasicOutputCollector basicOutputCollector) {
        int operation = tuple.getIntegerByField("operation");
        if (operation > 0) {
            basicOutputCollector.emit(tuple.getValues());
        }
    }

    @Override
    public void declareOutputFields(OutputFieldsDeclarer outputFieldsDeclarer) {
        outputFieldsDeclarer.declare(new Fields("operation", "timestamp"));
    }
}

6.3. AggregatingBolt

Als nächstes erstellen wir einen komplizierterenBolt -Stat, der alle positiven Operationen von jedem Tag zusammenfasst.

Zu diesem Zweck verwenden wir eine bestimmte Klasse, die speziell für die Implementierung von Schrauben erstellt wurde, die unter Windows anstatt unter einzelnen Tupeln ausgeführt werden:BaseWindowedBolt.

Windows sind ein wesentliches Konzept bei der Stream-Verarbeitung, bei dem die unendlichen Streams in endliche Blöcke aufgeteilt werden. Wir können dann Berechnungen auf jeden Block anwenden. Im Allgemeinen gibt es zwei Arten von Fenstern:

Time windows are used to group elements from a given time period using timestamps. Zeitfenster können unterschiedlich viele Elemente haben.

Count windows are used to create windows with a defined size. In diesem Fall haben alle Fenster die gleiche Größe und das Fensternot be emitted if there are fewer elements than the defined size.

UnsereAggregatingBolt generieren die Summe aller positiven Operationen aus atime window zusammen mit den Anfangs- und Endzeitstempeln:

public class AggregatingBolt extends BaseWindowedBolt {
    private OutputCollector outputCollector;

    @Override
    public void prepare(Map stormConf, TopologyContext context, OutputCollector collector) {
        this.outputCollector = collector;
    }

    @Override
    public void declareOutputFields(OutputFieldsDeclarer declarer) {
        declarer.declare(new Fields("sumOfOperations", "beginningTimestamp", "endTimestamp"));
    }

    @Override
    public void execute(TupleWindow tupleWindow) {
        List tuples = tupleWindow.get();
        tuples.sort(Comparator.comparing(this::getTimestamp));

        int sumOfOperations = tuples.stream()
          .mapToInt(tuple -> tuple.getIntegerByField("operation"))
          .sum();
        Long beginningTimestamp = getTimestamp(tuples.get(0));
        Long endTimestamp = getTimestamp(tuples.get(tuples.size() - 1));

        Values values = new Values(sumOfOperations, beginningTimestamp, endTimestamp);
        outputCollector.emit(values);
    }

    private Long getTimestamp(Tuple tuple) {
        return tuple.getLongByField("timestamp");
    }
}

Beachten Sie, dass es in diesem Fall sicher ist, das erste Element der Liste direkt abzurufen. Dies liegt daran, dass jedes Fenster anhand des Feldstimestamp vonTuple, sothere has to be at least one element in each window. berechnet wird

6.4. FileWritingBolt

Schließlich erstellen wir eine Schraube, die alle Elemente mitsumOfOperations größer als 2000 aufnimmt, serialisiert und in die Datei schreibt:

public class FileWritingBolt extends BaseRichBolt {
    public static Logger logger = LoggerFactory.getLogger(FileWritingBolt.class);
    private BufferedWriter writer;
    private String filePath;
    private ObjectMapper objectMapper;

    @Override
    public void cleanup() {
        try {
            writer.close();
        } catch (IOException e) {
            logger.error("Failed to close writer!");
        }
    }

    @Override
    public void prepare(Map map, TopologyContext topologyContext,
      OutputCollector outputCollector) {
        objectMapper = new ObjectMapper();
        objectMapper.setVisibility(PropertyAccessor.FIELD, JsonAutoDetect.Visibility.ANY);

        try {
            writer = new BufferedWriter(new FileWriter(filePath));
        } catch (IOException e) {
            logger.error("Failed to open a file for writing.", e);
        }
    }

    @Override
    public void execute(Tuple tuple) {
        int sumOfOperations = tuple.getIntegerByField("sumOfOperations");
        long beginningTimestamp = tuple.getLongByField("beginningTimestamp");
        long endTimestamp = tuple.getLongByField("endTimestamp");

        if (sumOfOperations > 2000) {
            AggregatedWindow aggregatedWindow = new AggregatedWindow(
                sumOfOperations, beginningTimestamp, endTimestamp);
            try {
                writer.write(objectMapper.writeValueAsString(aggregatedWindow));
                writer.newLine();
                writer.flush();
            } catch (IOException e) {
                logger.error("Failed to write data to file.", e);
            }
        }
    }

    // public constructor and other methods
}

Beachten Sie, dass wir die Ausgabe nicht deklarieren müssen, da dies die letzte Schraube in unserer Topologie ist

6.5. Ausführen der Topologie

Schließlich können wir alles zusammenführen und unsere Topologie ausführen:

public static void runTopology() {
    TopologyBuilder builder = new TopologyBuilder();

    Spout random = new RandomNumberSpout();
    builder.setSpout("randomNumberSpout");

    Bolt filtering = new FilteringBolt();
    builder.setBolt("filteringBolt", filtering)
      .shuffleGrouping("randomNumberSpout");

    Bolt aggregating = new AggregatingBolt()
      .withTimestampField("timestamp")
      .withLag(BaseWindowedBolt.Duration.seconds(1))
      .withWindow(BaseWindowedBolt.Duration.seconds(5));
    builder.setBolt("aggregatingBolt", aggregating)
      .shuffleGrouping("filteringBolt"); 

    String filePath = "./src/main/resources/data.txt";
    Bolt file = new FileWritingBolt(filePath);
    builder.setBolt("fileBolt", file)
      .shuffleGrouping("aggregatingBolt");

    Config config = new Config();
    config.setDebug(false);
    LocalCluster cluster = new LocalCluster();
    cluster.submitTopology("Test", config, builder.createTopology());
}

Damit die Daten durch jedes Teil in der Topologie fließen, müssen wir angeben, wie sie verbunden werden sollen. MitshuffleGroup können wir angeben, dass die Daten fürfilteringBolt vonrandomNumberSpout stammen.

For each Bolt, we need to add shuffleGroup which defines the source of elements for this bolt. Die Quelle der Elemente kann einSpout oder ein anderesBolt. sein. Wenn wir dieselbe Quelle für mehr als eine Schraubeeinstellen, gibt die Quelle alle Elemente an jedes von ihnen aus.

In diesem Fall verwendet unsere TopologieLocalCluster, um den Job lokal auszuführen.

7. Fazit

In diesem Tutorial haben wir Apache Storm vorgestellt, ein verteiltes Echtzeit-Rechensystem. Wir haben einen Ausguss erstellt, einige Schrauben und diese zu einer vollständigen Topologie zusammengefasst.

Und wie immer finden Sie alle Codebeispiele inover on GitHub.