Apache Stormの紹介

Apache Stormの紹介

 

1. 概要

このチュートリアルは、Apache Storm a distributed real-time computation system.の概要です。

以下に焦点を当てて説明します。

  • Apache Stormとはどのようなもので、Apache Stormが解決する問題

  • そのアーキテクチャ、および

  • プロジェクトでの使用方法

2. Apache Stormとは何ですか?

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

フォールトトレランスとスケーラビリティを提供し、データ処理を保証します。特に、無制限のデータストリームの処理に優れています。

Stormの良い使用例としては、不正検出のためにクレジットカード操作を処理したり、スマートホームからのデータを処理して障害のあるセンサーを検出したりすることができます。

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

3. メーベン依存

Apache Stormを使用する前に、プロジェクトにthe storm-core dependencyを含める必要があります。


    org.apache.storm
    storm-core
    1.2.2
    provided

Stormクラスターでアプリケーションを実行する場合にのみ、provided scope を使用する必要があります。

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

4. データ・モデル

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

4.1. タプル

Tupleは、動的タイプの名前付きフィールドの順序付きリストです。 This means that we don’t need to explicitly declare the types of the fields.

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

また、StormはKryoシリアル化を使用するため、カスタムタイプを使用するには、Configを使用してシリアライザーを登録する必要があります。 これは、次の2つの方法のいずれかで実行できます。

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

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

このような場合、KryoはFieldSerializerを使用してクラスをシリアル化します。デフォルトでは、これにより、プライベートとパブリックの両方のクラスのすべての非一時フィールドがシリアル化されます。

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

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

カスタムシリアライザーを作成するには、2つのメソッドwrite read.を持つto extend the generic classSerializer が必要です。

4.2. ストリーム

Streamは、Stormエコシステムのコア抽象化です。 The Stream is an unbounded sequence of tuples.

ストームにより、複数のストリームを並行して処理できます。

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

5. トポロジー

リアルタイムStormアプリケーションのロジックは、トポロジにパッケージ化されています。 トポロジは、spoutsboltsで構成されます。

5.1. 注ぎ口

注ぎ口は小川の源です。 それらはトポロジーにタプルを放出します。

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

注ぎ口は、reliableまたはunreliableにすることができます。 Reliableは、Stormによる処理に失敗したタプルに注ぎ口が応答できることを意味します。 Unreliableは、ファイアアンドフォーゲットメカニズムを使用してタプルを放出するため、注ぎ口が応答しないことを意味します。

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

unreliable 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. Bolt

Bolts process tuples in the stream.フィルタリング、集計、カスタム関数などのさまざまな操作を実行できます。

一部の操作には複数の手順が必要なため、このような場合は複数のボルトを使用する必要があります。

カスタムBoltを作成するには、IRichBolt orを実装して、操作を簡単にするIBasicBoltインターフェースを実装する必要があります。

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. FilteringBolt

次に、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. AggregatingBolt

次に、毎日のすべてのポジティブな操作を集約する、より複雑なBolt を作成しましょう。

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

Windowsは、無限のストリームを有限のチャンクに分割する、ストリーム処理の重要な概念です。 その後、各チャンクに計算を適用できます。 通常、ウィンドウには2つのタイプがあります。

Time windows are used to group elements from a given time period using timestamps。 タイムウィンドウの要素数は異なる場合があります。

Count windows are used to create windows with a defined size。 このような場合、すべてのウィンドウのサイズは同じになり、ウィンドウはnot be emitted if there are fewer elements than the defined size.になります。

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 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, sothere has to be at least one element in each window.timestamp fieldを使用して計算されるためです。

6.4. FileWritingBolt

最後に、sumOfOperationsが2000より大きいすべての要素を取得し、それらをシリアル化してファイルに書き込むボルトを作成します。

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から取得されることを示すことができます。

For each Bolt, we need to add shuffleGroup which defines the source of elements for this bolt.要素のソースはSpout または別のBolt.である可能性があり、同じソースを複数のボルトに設定すると、ソースはすべての要素をそれぞれに放出します。

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

7. 結論

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

そして、いつものように、すべてのコードサンプルはover on GitHubで見つけることができます。