Introdução ao Apache Storm
1. Visão geral
Este tutorial será uma introdução aApache Storm, a distributed real-time computation system.
Vamos nos concentrar e cobrir:
-
O que exatamente é o Apache Storm e quais problemas ele resolve
-
Sua arquitetura e
-
Como usá-lo em um projeto
2. O que é o Apache Storm?
O Apache Storm é um sistema distribuído de código aberto e gratuito para cálculos em tempo real.
Ele fornece tolerância a falhas, escalabilidade e garante o processamento de dados e é especialmente bom no processamento de fluxos ilimitados de dados.
Alguns bons casos de uso do Storm podem ser operações de cartão de crédito para detecção de fraude ou processamento de dados de residências inteligentes para detectar sensores defeituosos.
O Storm permite a integração com vários bancos de dados e sistemas de filas disponíveis no mercado.
3. Dependência do Maven
Antes de usar o Apache Storm, precisamos incluirthe storm-core dependency em nosso projeto:
org.apache.storm
storm-core
1.2.2
provided
Devemos usar apenas oprovided scope se pretendemos executar nosso aplicativo no cluster Storm.
Para executar a aplicação localmente, podemos usar o chamado modo local que irá simular o cluster Storm em um processo local, neste caso devemos remover oprovided.
4. Modelo de dados
O modelo de dados do Apache Storm consiste em dois elementos: tuplas e fluxos.
4.1. Tuple
ATuple é uma lista ordenada de campos nomeados com tipos dinâmicos. This means that we don’t need to explicitly declare the types of the fields.
O Storm precisa saber como serializar todos os valores usados em uma tupla. Por padrão, ele já pode serializar tipos primitivos, matrizesStringsebyte.
E como Storm usa serialização Kryo, precisamos registrar o serializador usandoConfig para usar os tipos personalizados. Podemos fazer isso de duas maneiras:
Primeiro, podemos registrar a classe para serializar usando seu nome completo:
Config config = new Config();
config.registerSerialization(User.class);
Nesse caso, Kryo serializará a classe usandoFieldSerializer. Por padrão, isso serializará todos os campos não transitórios da classe, tanto privados quanto públicos.
Ou então, podemos fornecer a classe a ser serializada e o serializador que queremos que o Storm use para essa classe:
Config config = new Config();
config.registerSerialization(User.class, UserSerializer.class);
Para criar o serializador personalizado, precisamos deto extend the generic classSerializer que tem dois métodoswrite andread.
4.2. Corrente
AStream é a abstração central no ecossistema Storm. The Stream is an unbounded sequence of tuples.
Tempestades permite processar vários fluxos em paralelo.
Todo fluxo tem um ID que é fornecido e designado durante a declaração.
5. Topologia
A lógica do aplicativo Storm em tempo real é empacotada na topologia. A topologia consiste emspoutsebolts.
5.1. Bico; esquichar
Bicos são as fontes dos riachos. Eles emitem tuplas para a topologia.
As tuplas podem ser lidas em vários sistemas externos, como Kafka, Kestrel ou ActiveMQ.
Os bicos podem serreliable ouunreliable. Reliable significa que o spout pode responder que a tupla falhou ao ser processada pelo Storm. Unreliable significa que o spout não responde, uma vez que vai usar um mecanismo disparar e esquecer para emitir as tuplas.
Para criar o spout customizado, precisamos implementar a sinterfaceIRichSpout ou estender qualquer classe que já implemente a interface, por exemplo, uma classeBaseRichSpout abstrata.
Vamos criar umunreliable 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"));
}
}
NossoRandomIntSpout customizado irá gerar inteiros aleatórios e timestamp a cada segundo.
5.2. Bolt
Bolts process tuples in the stream. Eles podem realizar várias operações como filtragem, agregações ou funções personalizadas.
Algumas operações requerem várias etapas e, portanto, precisaremos usar vários parafusos nesses casos.
Para criar oBolt personalizado, precisamos implementar oIRichBolt or para operações mais simples da interfaceIBasicBolt.
Existem também várias classes auxiliares disponíveis para a implementação deBolt. . Nesse caso, usaremosBaseBasicBolt:
public class PrintingBolt extends BaseBasicBolt {
@Override
public void execute(Tuple tuple, BasicOutputCollector basicOutputCollector) {
System.out.println(tuple);
}
@Override
public void declareOutputFields(OutputFieldsDeclarer outputFieldsDeclarer) {
}
}
EstePrintingBolt personalizado simplesmente imprimirá todas as tuplas no console.
6. Criando uma topologia simples
Vamos colocar essas ideias juntas em uma topologia simples. Nossa topologia terá um bico e três parafusos.
6.1. RandomNumberSpout
No início, vamos criar um bico não confiável. Ele irá gerar números inteiros aleatórios do intervalo (0,100) a cada segundo:
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
A seguir, criaremos um parafuso que filtrará todos os elementos comoperation igual a 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
A seguir, vamos criar umBolt t mais complicado que agregará todas as operações positivas de cada dia.
Para este propósito, usaremos uma classe específica criada especialmente para implementar parafusos que operam em janelas em vez de operar em tuplas simples:BaseWindowedBolt.
Windows são um conceito essencial no processamento de fluxo, dividindo os fluxos infinitos em pedaços finitos. Podemos então aplicar cálculos a cada pedaço. Geralmente, existem dois tipos de janelas:
Time windows are used to group elements from a given time period using timestamps. As janelas de tempo podem ter um número diferente de elementos.
Count windows are used to create windows with a defined size. Nesse caso, todas as janelas terão o mesmo tamanho e a janela seránot be emitted if there are fewer elements than the defined size.
NossoAggregatingBolt irá gerar a soma de todas as operações positivas de umtime window junto com seus carimbos de data / hora de início e término:
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");
}
}
Observe que, nesse caso, é seguro obter o primeiro elemento da lista diretamente. Isso ocorre porque cada janela é calculada usando o campotimestamp doTuple, sothere has to be at least one element in each window.
6.4. FileWritingBolt
Por fim, criaremos um parafuso que pegará todos os elementos comsumOfOperations maior que 2.000, serializá-los e gravá-los no arquivo:
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
}
Observe que não precisamos declarar a saída, pois este será o último parafuso em nossa topologia
6.5. Executando a topologia
Por fim, podemos reunir tudo e executar nossa topologia:
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());
}
Para fazer com que os dados fluam por cada parte da topologia, precisamos indicar como conectá-los. shuffleGroup nos permite afirmar que os dados parafilteringBolt virão derandomNumberSpout.
For each Bolt, we need to add shuffleGroup which defines the source of elements for this bolt. A fonte dos elementos pode ser umSpout ou outroBolt. E se definirmos a mesma fonte para mais de um parafuso, , a fonte irá emitir todos os elementos para cada um deles.
Nesse caso, nossa topologia usaráLocalCluster para executar o trabalho localmente.
7. Conclusão
Neste tutorial, apresentamos o Apache Storm, um sistema de computação distribuído em tempo real. Criamos um bico, alguns parafusos e os juntamos em uma topologia completa.
E, como sempre, todas as amostras de código podem ser encontradasover on GitHub.