Introdução ao Apache Flink com Java
1. Visão geral
O Apache Flink é uma estrutura de processamento de Big Data que permite que os programadores processem a grande quantidade de dados de maneira muito eficiente e escalável.
Neste artigo, apresentaremos alguns doscore API concepts and standard data transformations available in the Apache Flink Java API. O estilo fluente desta API facilita o trabalho com a construção central do Flink - a coleção distribuída.
Primeiro, vamos dar uma olhada nas transformações de APIDataSet de Flink e usá-las para implementar um programa de contagem de palavras. Em seguida, daremos uma breve olhada na APIDataStream do Flink, que permite processar fluxos de eventos em tempo real.
2. Dependência do Maven
Para começar, precisaremos adicionar dependências Maven às bibliotecasflink-javaeflink-test-utils:
org.apache.flink
flink-java
1.2.0
org.apache.flink
flink-test-utils_2.10
1.2.0
test
3. Conceitos básicos da API
Ao trabalhar com o Flink, precisamos conhecer algumas coisas relacionadas à sua API:
-
Every Flink program performs transformations on distributed collections of data. Uma variedade de funções para transformar dados são fornecidas, incluindo filtragem, mapeamento, junção, agrupamento e agregação
-
A sink operation in Flink triggers the execution of a stream to produce the desired result of the program, como salvar o resultado no sistema de arquivos ou imprimi-lo na saída padrão
-
As transformações Flink são lentas, o que significa que não são executadas até que uma operaçãosink seja invocada
-
The Apache Flink API supports two modes of operations — batch and real-time. Se você estiver lidando com uma fonte de dados limitada que pode ser processada no modo em lote, você usará a APIDataSet. Se você quiser processar fluxos ilimitados de dados em tempo real, precisará usar a APIDataStream
4. Transformações da API DataSet
O ponto de entrada para o programa Flink é uma instância da classeExecutionEnvironment - isso define o contexto no qual um programa é executado.
Vamos criar umExecutionEnvironment para iniciar nosso processamento:
ExecutionEnvironment env
= ExecutionEnvironment.getExecutionEnvironment();
Note that when you launch the application on the local machine, it will perform processing on the local JVM. Se desejar iniciar o processamento em um cluster de máquinas, você precisará instalarApache Flink nessas máquinas e configurarExecutionEnvironment de acordo.
4.1. Criação de um DataSet
Para começar a realizar transformações de dados, precisamos fornecer ao nosso programa os dados.
Vamos criar uma instância da classeDataSet usando nossoExecutionEnvironement:
DataSet amounts = env.fromElements(1, 29, 40, 50);
Você pode criar umDataSet a partir de várias fontes, como Apache Kafka, um CSV, arquivo ou virtualmente qualquer outra fonte de dados.
4.2. Filtrar e reduzir
Depois de criar uma instância da classeDataSet, você pode aplicar transformações a ela.
Digamos que você queira filtrar os números que estão acima de um certo limite e, em seguida, somá-los todos.. Você pode usar as transformaçõesfilter()ereduce() para conseguir isso:
int threshold = 30;
List collect = amounts
.filter(a -> a > threshold)
.reduce((integer, t1) -> integer + t1)
.collect();
assertThat(collect.get(0)).isEqualTo(90);
Observe que o métodocollect() é uma operaçãosink que dispara as transformações de dados reais.
4.3. Map
Digamos que você tenha umDataSet de objetosPerson:
private static class Person {
private int age;
private String name;
// standard constructors/getters/setters
}
A seguir, vamos criar umDataSet desses objetos:
DataSet personDataSource = env.fromCollection(
Arrays.asList(
new Person(23, "Tom"),
new Person(75, "Michael")));
Suponha que você queira extrair apenas o campoage de cada objeto da coleção. Você pode usar a transformaçãomap() para obter apenas um campo específico da classePerson:
List ages = personDataSource
.map(p -> p.age)
.collect();
assertThat(ages).hasSize(2);
assertThat(ages).contains(23, 75);
4.4. Join
Quando você tem dois conjuntos de dados, pode querer juntá-los em algum campoid. Para isso, você pode usar a transformaçãojoin().
Vamos criar coleções de transações e endereços de um usuário:
Tuple3 address
= new Tuple3<>(1, "5th Avenue", "London");
DataSet> addresses
= env.fromElements(address);
Tuple2 firstTransaction
= new Tuple2<>(1, "Transaction_1");
DataSet> transactions
= env.fromElements(firstTransaction, new Tuple2<>(12, "Transaction_2"));
O primeiro campo em ambas as tuplas é do tipoInteger, e este é um campoid no qual queremos juntar os dois conjuntos de dados.
Para realizar a lógica de junção real, precisamos implementar uma interfaceKeySelector para endereço e transação:
private static class IdKeySelectorTransaction
implements KeySelector, Integer> {
@Override
public Integer getKey(Tuple2 value) {
return value.f0;
}
}
private static class IdKeySelectorAddress
implements KeySelector, Integer> {
@Override
public Integer getKey(Tuple3 value) {
return value.f0;
}
}
Cada seletor está retornando apenas o campo no qual a junção deve ser executada.
Infelizmente, não é possível usar expressões lambda aqui porque Flink precisa de informações de tipo genérico.
A seguir, vamos implementar a lógica de mesclagem usando esses seletores:
List, Tuple3>>
joined = transactions.join(addresses)
.where(new IdKeySelectorTransaction())
.equalTo(new IdKeySelectorAddress())
.collect();
assertThat(joined).hasSize(1);
assertThat(joined).contains(new Tuple2<>(firstTransaction, address));
4.5. Sort
Digamos que você tenha a seguinte coleção deTuple2:
Tuple2 secondPerson = new Tuple2<>(4, "Tom");
Tuple2 thirdPerson = new Tuple2<>(5, "Scott");
Tuple2 fourthPerson = new Tuple2<>(200, "Michael");
Tuple2 firstPerson = new Tuple2<>(1, "Jack");
DataSet> transactions = env.fromElements(
fourthPerson, secondPerson, thirdPerson, firstPerson);
Se você quiser classificar essa coleção pelo primeiro campo da tupla, você pode usar a transformaçãosortPartitions():
List> sorted = transactions
.sortPartition(new IdKeySelectorTransaction(), Order.ASCENDING)
.collect();
assertThat(sorted)
.containsExactly(firstPerson, secondPerson, thirdPerson, fourthPerson);
5. Contagem de palavras
O problema da contagem de palavras é aquele geralmente usado para mostrar os recursos das estruturas de processamento de Big Data. A solução básica envolve contar ocorrências de palavras em uma entrada de texto. Vamos usar o Flink para implementar uma solução para este problema.
Como primeiro passo em nossa solução, criamos uma classeLineSplitter que divide nossa entrada em tokens (palavras), coletando para cada token umTuple2 de pares de valores-chave. Em cada uma dessas tuplas, a chave é uma palavra encontrada no texto e o valor é o número inteiro (1).
Esta classe implementa a interfaceFlatMapFunction que levaString como entrada e produz umTuple2<String, Integer>:
public class LineSplitter implements FlatMapFunction> {
@Override
public void flatMap(String value, Collector> out) {
Stream.of(value.toLowerCase().split("\\W+"))
.filter(t -> t.length() > 0)
.forEach(token -> out.collect(new Tuple2<>(token, 1)));
}
}
Chamamos o métodocollect() na classeCollector para enviar os dados para frente no pipeline de processamento.
Nossa próxima e última etapa é agrupar as tuplas por seus primeiros elementos (palavras) e, em seguida, realizar uma agregaçãosum nos segundos elementos para produzir uma contagem das ocorrências de palavras:
public static DataSet> startWordCount(
ExecutionEnvironment env, List lines) throws Exception {
DataSet text = env.fromCollection(lines);
return text.flatMap(new LineSplitter())
.groupBy(0)
.aggregate(Aggregations.SUM, 1);
}
Estamos usando três tipos de transformações Flink:flatMap(),groupBy() eaggregate().
Vamos escrever um teste para garantir que a implementação da contagem de palavras está funcionando conforme o esperado:
List lines = Arrays.asList(
"This is a first sentence",
"This is a second sentence with a one word");
DataSet> result = WordCount.startWordCount(env, lines);
List> collect = result.collect();
assertThat(collect).containsExactlyInAnyOrder(
new Tuple2<>("a", 3), new Tuple2<>("sentence", 2), new Tuple2<>("word", 1),
new Tuple2<>("is", 2), new Tuple2<>("this", 2), new Tuple2<>("second", 1),
new Tuple2<>("first", 1), new Tuple2<>("with", 1), new Tuple2<>("one", 1));
6. API DataStream
6.1. Criando um DataStream
O Apache Flink também suporta o processamento de fluxos de eventos por meio de sua API DataStream. Se quisermos começar a consumir eventos, primeiro precisamos usar a classeStreamExecutionEnvironment:
StreamExecutionEnvironment executionEnvironment
= StreamExecutionEnvironment.getExecutionEnvironment();
Em seguida, podemos criar um fluxo de eventos usandoexecutionEnvironment de uma variedade de fontes. Pode ser algum barramento de mensagem comoApache Kafka, mas neste exemplo, vamos simplesmente criar uma fonte a partir de alguns elementos de string:
DataStream dataStream = executionEnvironment.fromElements(
"This is a first sentence",
"This is a second sentence with a one word");
Podemos aplicar transformações a cada elemento deDataStream como na classeDataSet normal:
SingleOutputStreamOperator upperCase = text.map(String::toUpperCase);
Para acionar a execução, precisamos invocar uma operação sink, comoprint(), que apenas imprimirá o resultado das transformações na saída padrão, seguindo o métodoexecute() na classeStreamExecutionEnvironment:
upperCase.print();
env.execute();
Produzirá a seguinte saída:
1> THIS IS A FIRST SENTENCE
2> THIS IS A SECOND SENTENCE WITH A ONE WORD
6.2. Janelas de eventos
Ao processar um fluxo de eventos em tempo real, às vezes pode ser necessário agrupar eventos e aplicar algum cálculo em uma janela desses eventos.
Suponha que tenhamos um fluxo de eventos, em que cada evento é um par que consiste no número do evento e no registro de data e hora quando o evento foi enviado ao nosso sistema, e que podemos tolerar eventos que estão fora de ordem, mas apenas se não houver. mais de vinte segundos atrasado.
Para este exemplo, vamos primeiro criar um fluxo simulando dois eventos com vários minutos de intervalo e definir um extrator de carimbo de data / hora que especifica nosso limite de atraso:
SingleOutputStreamOperator> windowed
= env.fromElements(
new Tuple2<>(16, ZonedDateTime.now().plusMinutes(25).toInstant().getEpochSecond()),
new Tuple2<>(15, ZonedDateTime.now().plusMinutes(2).toInstant().getEpochSecond()))
.assignTimestampsAndWatermarks(
new BoundedOutOfOrdernessTimestampExtractor
>(Time.seconds(20)) {
@Override
public long extractTimestamp(Tuple2 element) {
return element.f1 * 1000;
}
});
A seguir, vamos definir uma operação de janela para agrupar nossos eventos em janelas de cinco segundos e aplicar uma transformação a esses eventos:
SingleOutputStreamOperator> reduced = windowed
.windowAll(TumblingEventTimeWindows.of(Time.seconds(5)))
.maxBy(0, true);
reduced.print();
Ele obterá o último elemento de cada janela de cinco segundos e será impresso:
1> (15,1491221519)
Observe que não vemos o segundo evento porque ele chegou depois do limite de atraso especificado.
7. Conclusão
Neste artigo, apresentamos a estrutura do Apache Flink e analisamos algumas das transformações fornecidas com sua API.
Implementamos um programa de contagem de palavras usando a API DataSet fluente e funcional do Flink. Em seguida, analisamos a API DataStream e implementamos uma transformação simples em tempo real em um fluxo de eventos.
A implementação de todos esses exemplos e trechos de código pode ser encontradaover on GitHub - este é um projeto Maven, portanto, deve ser fácil de importar e executar como está.