Introduction à Apache Storm

Introduction à Apache Storm

 

1. Vue d'ensemble

Ce tutoriel sera une introduction àApache Storm, a distributed real-time computation system.

Nous allons nous concentrer et couvrir:

  • En quoi consiste exactement Apache Storm et quels problèmes il résout

  • Son architecture, et

  • Comment l'utiliser dans un projet

2. Qu'est-ce que Apache Storm?

Apache Storm est un système distribué gratuit et à source ouverte pour les calculs en temps réel.

Il offre une tolérance aux pannes, une évolutivité et garantit le traitement des données, et est particulièrement efficace pour traiter des flux de données illimités.

Storm peut être utile dans le traitement des opérations de carte de crédit pour la détection de fraude ou le traitement de données provenant de maisons intelligentes pour détecter des capteurs défectueux.

Storm permet l'intégration de diverses bases de données et systèmes de mise en file d'attente disponibles sur le marché.

3. Dépendance Maven

Avant d'utiliser Apache Storm, nous devons inclurethe storm-core dependency dans notre projet:


    org.apache.storm
    storm-core
    1.2.2
    provided

Nous ne devons utiliser leprovided scope  que si nous avons l'intention d'exécuter notre application sur le cluster Storm.

Pour exécuter l'application localement, nous pouvons utiliser un mode dit local qui simulera le cluster Storm dans un processus local, dans ce cas, nous devrions supprimer lesprovided.

4. Modèle de données

Le modèle de données d'Apache Storm se compose de deux éléments: les tuples et les flux.

4.1. Tuple

UnTuple est une liste ordonnée de champs nommés avec des types dynamiques. This means that we don’t need to explicitly declare the types of the fields.

Storm a besoin de savoir comment sérialiser toutes les valeurs utilisées dans un tuple. Par défaut, il peut déjà sérialiser les types primitifs, les tableauxStrings etbyte.

Et comme Storm utilise la sérialisation Kryo, nous devons enregistrer le sérialiseur en utilisantConfig pour utiliser les types personnalisés. Nous pouvons le faire de deux manières:

Premièrement, nous pouvons enregistrer la classe à sérialiser en utilisant son nom complet:

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

Dans un tel cas, Kryo sérialisera la classe en utilisantFieldSerializer Par défaut, cela sérialisera tous les champs non transitoires de la classe, à la fois privés et publics.

Ou bien, nous pouvons fournir à la fois la classe à sérialiser et le sérialiseur que nous souhaitons que Storm utilise pour cette classe:

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

Pour créer le sérialiseur personnalisé, nous avons besoin deto extend the generic classSerializer qui a deux méthodeswrite andread.

4.2. Courant

UnStream est l'abstraction centrale de l'écosystème Storm. The Stream is an unbounded sequence of tuples.

Storms permet de traiter plusieurs flux en parallèle.

Chaque flux a un identifiant fourni et attribué lors de la déclaration.

5. Topologie

La logique de l'application Storm en temps réel est intégrée à la topologie. La topologie se compose despouts etbolts.

5.1. Bec

Les becs sont les sources des flux. Ils émettent des tuples vers la topologie.

Les tuples peuvent être lus à partir de divers systèmes externes tels que Kafka, Kestrel ou ActiveMQ.

Les becs peuvent êtrereliable ouunreliable. Reliable signifie que le spout peut répondre que le tuple qui n'a pas pu être traité par Storm. Unreliable signifie que le spout ne répond pas car il va utiliser un mécanisme d'incendie et d'oubli pour émettre les tuples.

Pour créer le spout personnalisé, nous devons implémenter la sinterfaceIRichSpout ou étendre toute classe qui implémente déjà l'interface, par exemple, une classe abstraiteBaseRichSpout.

Créons ununreliable 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"));
    }
}

NosRandomIntSpout personnalisés généreront un nombre entier et un horodatage aléatoires toutes les secondes.

5.2. Bolt

Bolts process tuples in the stream. Ils peuvent effectuer diverses opérations comme le filtrage, les agrégations ou les fonctions personnalisées.

Certaines opérations nécessitent plusieurs étapes et nous devrons donc utiliser plusieurs boulons dans de tels cas.

Pour créer lesBolt personnalisés, nous devons implémenterIRichBolt or pour des opérations plus simples, l'interfaceIBasicBolt.

Il existe également plusieurs classes d'assistance disponibles pour implémenterBolt.  Dans ce cas, nous utiliseronsBaseBasicBolt:

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

    @Override
    public void declareOutputFields(OutputFieldsDeclarer outputFieldsDeclarer) {

    }
}

CePrintingBolt personnalisé imprimera simplement tous les tuples sur la console.

6. Création d'une topologie simple

Regroupons ces idées dans une topologie simple. Notre topologie aura un bec et trois boulons.

6.1. RandomNumberSpout

Au début, nous allons créer un bec verseur peu fiable. Il générera des entiers aléatoires de la plage (0,100) toutes les secondes:

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

Ensuite, nous allons créer un boulon qui filtrera tous les éléments avecoperation égal à 0:

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

Ensuite, créons unBolt t plus compliqué qui regroupera toutes les opérations positives de chaque jour.

À cette fin, nous utiliserons une classe spécifique créée spécialement pour implémenter des boulons qui fonctionnent sur Windows au lieu de fonctionner sur des tuples uniques:BaseWindowedBolt.

LesWindows sont un concept essentiel dans le traitement des flux, divisant les flux infinis en blocs finis. Nous pouvons ensuite appliquer des calculs à chaque morceau. Il existe généralement deux types de fenêtres:

Time windows are used to group elements from a given time period using timestamps. Les fenêtres temporelles peuvent comporter un nombre d'éléments différent.

Count windows are used to create windows with a defined size. Dans un tel cas, toutes les fenêtres auront la même taille et la fenêtre seranot be emitted if there are fewer elements than the defined size.

NotreAggregatingBolt générera la somme de toutes les opérations positives à partir d'untime window avec ses horodatages de début et de fin:

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");
    }
}

Notez que, dans ce cas, obtenir directement le premier élément de la liste est sécurisé. En effet, chaque fenêtre est calculée en utilisant le champtimestamp  duTuple, sothere has to be at least one element in each window.

6.4. FileWritingBolt

Enfin, nous allons créer un boulon qui prendra tous les éléments avecsumOfOperations supérieur à 2000, les sérialiserons et les écrirons dans le fichier:

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
}

Notez que nous n'avons pas besoin de déclarer la sortie car ce sera le dernier boulon de notre topologie

6.5. Exécuter la topologie

Enfin, nous pouvons tout rassembler et gérer notre topologie:

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());
}

Pour que les données circulent dans chaque élément de la topologie, nous devons indiquer comment les connecter. shuffleGroup nous permet de déclarer que les données pourfilteringBolt proviendront derandomNumberSpout.

For each Bolt, we need to add shuffleGroup which defines the source of elements for this bolt. La source des éléments peut être unSpout  ou un autreBolt. Et si nous définissons la même source pour plus d'un boulon, la source émettra tous les éléments vers chacun d'eux.

Dans ce cas, notre topologie utilisera lesLocalCluster pour exécuter le travail localement.

7. Conclusion

Dans ce tutoriel, nous avons présenté Apache Storm, un système de calcul distribué en temps réel. Nous avons créé un bec, des boulons et les avons assemblés pour former une topologie complète.

Et, comme toujours, tous les échantillons de code peuvent être trouvésover on GitHub.