Apache Stormの紹介

1.概要

このチュートリアルは、分散リアルタイム計算システムであるhttp://storm.apache.org/[Apache Storm]の紹介です。

私たちは焦点を当ててカバーします

  • Apache Stormとは一体何であり、それが解決する問題は何か

  • そのアーキテクチャ

プロジェクトでの使い方

2. Apache Stormとは何ですか?

Apache Stormは、リアルタイム計算用のフリーでオープンソースの分散システムです。

  • それはフォールトトレランス、スケーラビリティを提供し、そしてデータ処理を保証し、そして無制限のデータストリームを処理するのが特に得意です。 **

Stormの優れたユースケースには、不正検出のためのクレジットカード操作の処理や、スマートホームからのデータの処理による不良センサーの検出があります。

Stormは、市場で入手可能なさまざまなデータベースおよびキューイングシステムとの統合を可能にします。

3. Mavenの依存関係

Apache Stormを使う前に、https://search.maven.org/classic/#search%7Cga%7C1%7Cg%3A%22org.apache.storm%22%20AND%20a%3A%22storm-coreを含める必要があります。私たちのプロジェクトの%22[暴風雨コア依存]

<dependency>
    <groupId>org.apache.storm</groupId>
    <artifactId>storm-core</artifactId>
    <version>1.2.2</version>
    <scope>provided</scope>
</dependency>

アプリケーションをStormクラスタ上で実行する予定の場合は、____providedスコープのみを使用してください。

アプリケーションをローカルで実行するには、ローカルプロセスでStormクラスターをシミュレートする、いわゆるローカルモードを使用できます。その場合は provided. を削除する必要があります。

4.データモデル

Apache Stormのデータモデルは、タプルとストリームの2つの要素で構成されています。

4.1. タプル

Tuple は動的型を持つ名前付きフィールドの順序付きリストです。 これは、フィールドの型を明示的に宣言する必要がないことを意味します。

Stormは、タプルで使用されるすべての値をシリアル化する方法を知る必要があります。デフォルトでは、 Strings および byte 配列のプリミティブ型をすでにシリアル化できます。

そしてStormはKryoシリアライゼーションを使っているので、カスタム型を使うために Config を使ってシリアライザを登録する必要があります。これには2つの方法があります。

まず、フルネームを使用してクラスをシリアル化するために登録できます。

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

そのような場合、Kryoは __https://github.com/EsotericSoftware/kryo#fieldserializer[FieldSerializer]を使用してクラスをシリアル化します。 __デフォルトでは、これはプライベートとパブリックの両方の、クラスのすべての非一時的フィールドをシリアル化します。

代わりに、シリアル化するクラスとStormがそのクラスに使用するシリアライザの両方を提供することもできます。

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

カスタムシリアライザを作成するには、https://www.baeldung.com/kryo[ジェネリッククラスを拡張する]が必要です。

4.2. ストリーム

Stream はStormエコシステムの中心的な抽象概念です。 ** Stream は、無制限のタプルシーケンスです。

Stormsは複数のストリームを並行して処理することを可能にします。

すべてのストリームには、宣言中に提供および割り当てられたIDがあります。

5.トポロジ

リアルタイムStormアプリケーションのロジックはトポロジにパッケージ化されています。このトポロジは、「注ぎ口」と「ボルト」で構成されています。

5.1. 注ぎ口

  • 噴出口は流れのもとです。それらはトポロジにタプルを発行します。

タプルは、Kafka、Kestrel、ActiveMQなどのさまざまな外部システムから読み取ることができます。

注ぎ口は 信頼 または 信頼できない です。 Reliable は、注ぎ口がStormによる処理に失敗したタプルを返信できることを意味します。 Unreliable は、タプルを発射するために発射忘れ機構を使用しようとしているため、注ぎ口が応答しないことを意味します。

カスタムスパウトを作成するには、 __ IRichSpout interfaceを実装するか、既にインタフェースを実装しているクラス(たとえば、抽象 BaseRichSpout__クラス)を拡張する必要があります。

__信頼できない __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"));
    }
}

私たちのカスタム RandomIntSpout は毎秒ランダムな整数とタイムスタンプを生成します。

5.2. ボルト

  • Boltsはストリーム内のタプルを処理します** フィルタリング、集計、カスタム関数などのさまざまな操作を実行できます。

いくつかの操作は複数のステップを必要とします、そしてそのような場合そのように我々は複数のボルトを使う必要があるでしょう。

カスタムの Bolt を作成するには、よりシンプルな操作のために __IRichBolt __orを実装する必要があります。

__Boltを実装するために利用可能な複数のヘルパークラスもあります。 この場合は、 BaseBasicBolt__を使用します。

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

    @Override
    public void declareOutputFields(OutputFieldsDeclarer outputFieldsDeclarer) {

    }
}

このカスタム PrintingBolt は単にすべてのタプルをコンソールに表示します。

6.単純なトポロジを作成する

これらのアイデアを単純なトポロジにまとめましょう。私達のトポロジーは1つの口および3つのボルトがあります。

6.1. RandomNumberSpout

最初は、信頼性の低い注ぎ口を作成します。毎秒(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. フィルターボルト

次に、 operation が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. 集計ボルト

次に、毎日のすべてのポジティブオペレーションを集約する、より複雑なボルトを作成しましょう。

この目的のために、単一のタプルではなく、ウィンドウで動作するボルトを実装するために特別に作成された特定のクラス BaseWindowedBolt を使用します。

Windows は、無限ストリームを有限のまとまりに分割する、ストリーム処理における重要な概念です。その後、各チャンクに計算を適用できます。一般的に2種類のウィンドウがあります。

  • タイムウィンドウは、タイムスタンプを使用して特定の期間の要素をグループ化するために使用されます。タイムウィンドウには、さまざまな要素数があります。

  • カウントウィンドウは、定義されたサイズのウィンドウを作成するために使用されます** 。そのような場合、すべてのウィンドウは同じサイズになり、定義されたサイズよりも要素数が少ないとウィンドウは表示されません。

AggregatingBolt は、開始および終了タイムスタンプとともに、 time window からすべての正の操作の合計を生成します。

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

この場合、リストの最初の要素を直接取得するのが安全であることに注意してください。これは、各ウィンドウが __Tupleの timestamp __フィールドを使用して計算されるためです。

6.4. FileWritingBolt

最後に、2000を超える sumOfOperations を持つすべての要素を取得し、それらを直列化してファイルに書き込むボルトを作成します。

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
}
  • これは私たちのトポロジの最後のボルトになるので、出力を宣言する必要はありません。

6.5. トポロジを実行する

最後に、すべてをまとめてトポロジを実行します。

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

トポロジ内の各部分をデータが流れるようにするには、それらをどのように接続するかを示す必要があります。 shuffleGroup を使用すると、 filteringBolt のデータは randomNumberSpout から取得されることになります。

  • Bolt ごとに、このボルトの要素のソースを定義する shuffleGroup を追加する必要があります。それらのそれぞれにすべての要素を放出します。

この場合、私たちのトポロジは LocalCluster を使用してローカルでジョブを実行します。

7.まとめ

このチュートリアルでは、分散リアルタイム計算システムであるApache Stormを紹介しました。私達は注ぎ口、いくつかのボルトを作り、そして完全なトポロジーにそれらを一緒に引っ張った。

そして、いつものように、すべてのコードサンプルはhttps://github.com/eugenp/tutorials/tree/master/libraries-data[GitHubに追加]を見つけることができます。