Введение в Apache Storm

Введение в Apache Storm

 

1. обзор

Это руководство будет введением вApache Storm, a distributed real-time computation system.

Мы сосредоточимся и рассмотрим:

  • Что такое Apache Storm и какие проблемы он решает?

  • Его архитектура и

  • Как использовать это в проекте

2. Что такое Apache Storm?

Apache Storm - это бесплатная распределенная система с открытым исходным кодом для вычислений в реальном времени.

Он обеспечивает отказоустойчивость, масштабируемость и гарантирует обработку данных и особенно хорош при обработке неограниченных потоков данных.

Некоторые хорошие случаи использования для Storm могут обрабатывать операции с кредитными картами для обнаружения мошенничества или обрабатывать данные из умных домов для обнаружения неисправных датчиков.

Storm позволяет интегрироваться с различными базами данных и системами очередей, доступными на рынке.

3. Maven Dependency

Прежде чем использовать Apache Storm, нам нужно включитьthe storm-core dependency в наш проект:


    org.apache.storm
    storm-core
    1.2.2
    provided

Мы должны использоватьprovided scope  только в том случае, если мы собираемся запускать наше приложение в кластере Storm.

Чтобы запустить приложение локально, мы можем использовать так называемый локальный режим, который будет имитировать кластер Storm в локальном процессе, в этом случае мы должны удалитьprovided.

4. Модель данных

Модель данных Apache Storm состоит из двух элементов: кортежей и потоков.

4.1. Кортеж

Tuple - это упорядоченный список именованных полей с динамическими типами. This means that we don’t need to explicitly declare the types of the fields.

Storm должен знать, как сериализовать все значения, которые используются в кортеже. По умолчанию он уже может сериализовать примитивные типы, массивыStrings иbyte.

А поскольку Storm использует сериализацию Kryo, нам нужно зарегистрировать сериализатор, используяConfig, чтобы использовать настраиваемые типы. Мы можем сделать это одним из двух способов:

Во-первых, мы можем зарегистрировать класс для сериализации, используя его полное имя:

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

В таком случае Kryo сериализует класс с использованиемFieldSerializer. По умолчанию это сериализует все непереходные поля класса, как частные, так и общедоступные.

Или вместо этого мы можем предоставить как класс для сериализации, так и сериализатор, который мы хотим, чтобы Storm использовал для этого класса:

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

Чтобы создать собственный сериализатор, нам понадобитсяto extend the generic classSerializer t, у которого есть два методаwrite andread.

4.2. Ручей

AStream - это основная абстракция в экосистеме Storm. The Stream is an unbounded sequence of tuples.

Storms позволяет обрабатывать несколько потоков параллельно.

Каждый поток имеет идентификатор, который предоставляется и назначается во время объявления.

5. Топология

Логика приложения Storm в реальном времени упакована в топологию. Топология состоит изspouts иbolts.

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. Создание простой топологии

Давайте объединим эти идеи в простую топологию. Наша топология будет иметь один носик и три болта.

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 t, который будет объединять все положительные операции за каждый день.

Для этой цели мы будем использовать специальный класс, созданный специально для реализации болтов, которые работают с окнами, а не с отдельными кортежами:BaseWindowedBolt.

Windows - важная концепция потоковой обработки, разделяющая бесконечные потоки на конечные фрагменты. Затем мы можем применить вычисления к каждому фрагменту. Обычно есть два типа окон:

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

Обратите внимание, что в этом случае получение первого элемента списка напрямую безопасно. Это потому, что каждое окно вычисляется с использованием поляtimestamp  поляTuple, sothere has to be at least one element in each window.

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.