使用Java的Apache Beam简介
主要收获
- Apache Beam是一个强大的批处理和流处理开源项目。
- 它的可移植性允许在不同的后端(从Apache Spark到Google Cloud Dataflow)运行管线
- Beam是可扩展的,这意味着你可以编写和分享新的SDK、IO连接器和转化器
- Beam目前支持Python、Java和Go
- 通过使用其Java SDK,您可以利用JVM的所有优势
在这篇文章中,我们将介绍Apache Beam,一个强大的批处理和流处理开源项目,被eBay这样的大公司用来整合其流式管道,被Mozilla用来在其系统之间安全地移动数据。
概述
Apache Beam是一个处理数据的编程模型,支持批处理和流处理。
使用为Java、Python和Go提供的SDK,你可以开发管道,然后选择一个后端来运行管道。
Apache Beam的优势
/filters:no_upscale()/articles/apache-beam-intro/en/resources/5pasted%20image%200-1654188064991.jpeg)
Beam模型(Frances Perry & Tyler Akidau)
- 内置I/O连接器
- Apache Beam连接器允许从几种类型的存储中轻松提取和加载数据
- 主要的连接器类型有
- 基于文件(例如:Apache Parquet,Apache Thrift)。
- 文件系统(例如:Hadoop、Google云存储、Amazon S3)。
- 消息传递(例如:Apache Kafka、Google Pub/Sub、Amazon SQS)
- 数据库(例如:Apache Cassandra、Elastic Search、MongoDb)
- 作为一个开放源码软件项目,对新连接器的支持在不断增加(例如:InfluxDB、Neo4J)。
- 可移植性。
- Beam提供了几个运行器来运行管道,让你为每个用例选择最好的,避免厂商锁定。
- 像Apache Flink、Apache Spark或Google Cloud Dataflow这样的分布式处理后端可以作为运行器使用。
- 分布式并行处理。
- 默认情况下,数据集上的每个项目都是独立处理的,因此可以通过并行运行来优化其处理。
- 开发人员不需要在工作者之间手动分配负载,因为Beam为其提供了一个抽象概念。
Beam模型
Beam编程模型的关键概念是:
- PCollection:代表一个数据集合,即:一个数字数组或从文本中提取的单词。
- PTransform:一个转换函数,接收并返回一个PCollection,即:所有数字之和。
- Pipeline:管理PTransform和PCollections之间的交互。
- PipelineRunner:指定流水线的执行地点和方式。
快速启动
/filters:no_upscale()/articles/apache-beam-intro/en/resources/1figure-4-1654188238779.jpeg)
一个基本的流水线操作包括3个步骤:读取、处理和写入转换结果。每一个步骤都是用Apache Beam SDK中的一个来编程定义的。
在本节中,我们将使用Java SDK创建管道。你可以选择创建一个本地应用程序(使用Gradle或Maven),也可以使用Online Playground。例子将使用本地运行器,因为使用JUnit断言会更容易验证结果。
Java本地依赖性
- beam-sdks-java-core:包含所有Beam模型类。
- beam-runners-direct-java:默认情况下,Apache Beam SDK将使用直接运行器,这意味着管道将在你的本地机器上运行。
乘以2
在这第一个例子中,管道将接收一个数字数组,并将每个元素乘以2进行映射。
第一步是创建流水线实例,它将接收输入数组并运行转换函数。 由于我们使用JUnit来运行Apache Beam,我们可以轻松地创建一个TestPipeline ,作为测试类的属性。如果你更喜欢在你的主应用程序上运行,你需要设置 管道配置选项。
@Rule
public final transient TestPipeline pipeline = TestPipeline.create();
现在我们可以创建PCollection,它将被用作管道的输入。它将是一个直接从内存实例化的数组,但它可以从Apache Beam支持的任何地方读取:
PCollection<Integer> numbers =
pipeline.apply(Create.of(1, 2, 3, 4, 5));
然后我们应用我们的转换函数,将每个数据集元素乘以2:
PCollection<Integer> output = numbers.apply(
MapElements.into(TypeDescriptors.integers())
.via((Integer number) -> number * 2)
);
为了验证结果,我们可以写一个断言:
PAssert.that(output)
.containsInAnyOrder(2, 4, 6, 8, 10);
请注意,结果不应该像输入那样被排序,因为Apache Beam是独立地、并行地处理每个项目。
此时的测试已经完成,我们通过调用来运行管道:
pipeline.run();
减少操作
缩减操作是将多个输入元素组合起来,形成一个较小的集合,通常包含一个元素:
/filters:no_upscale()/articles/apache-beam-intro/en/resources/1pasted%20image%200-2-1654188064991.jpeg)
MapReduce (Frances Perry & Tyler Akidau)
现在让我们扩展上面的例子,将所有项目的总和乘以2,产生一个MapReduce变换。
每个PCollection转换都会产生一个新的PCollection实例,这意味着我们可以使用apply 方法进行连锁转换。在这种情况下,在将每个输入项乘以2之后,将使用Sum操作:
PCollection<Integer> numbers =
pipeline.apply(Create.of(1, 2, 3, 4, 5));
PCollection<Integer> output = numbers
.apply(
MapElements.into(TypeDescriptors.integers())
.via((Integer number) -> number * 2))
.apply(Sum.integersGlobally());
PAssert.that(output)
.containsInAnyOrder(30);
pipeline.run();
FlatMap操作
FlatMap是一个操作,首先在每个输入元素上应用一个通常会返回一个新的集合的映射,从而形成一个集合的集合。然后应用一个平面操作来合并所有的嵌套集合,形成一个单一的集合。
下一个例子将是把字符串的数组转化为包含每个单词的唯一数组。
首先,我们声明我们的单词列表,它将被用作管道输入:
final String[] WORDS_ARRAY = new String[] {
"hi bob", "hello alice", "hi sue"};
final List<String> WORDS = Arrays.asList(WORDS_ARRAY);
然后我们使用上面的列表创建输入的PCollection:
PCollection<String> input = pipeline.apply(Create.of(WORDS));
现在我们应用flatmap转换,它将分割每个嵌套数组中的单词,并将结果合并到一个单一的列表中:
PCollection<String> output = input.apply(
FlatMapElements.into(TypeDescriptors.strings())
.via((String line) -> Arrays.asList(line.split(" ")))
);
PAssert.that(output)
.containsInAnyOrder("hi", "bob", "hello", "alice", "hi", "sue");
pipeline.run();
集合操作
在数据处理中,一个常见的工作是通过一个特定的键进行聚集或计数。我们将通过计算前面例子中每个词的出现次数来演示。
在有了平坦的字符串数组之后,我们可以连锁另一个PTransform:
PCollection<KV<String, Long>> output = input
.apply(
FlatMapElements.into(TypeDescriptors.strings())
.via((String line) -> Arrays.asList(line.split(" ")))
)
.apply(Count.<String>perElement());
导致
PAssert.that(output)
.containsInAnyOrder(
KV.of("hi", 2L),
KV.of("hello", 1L),
KV.of("alice", 1L),
KV.of("sue", 1L),
KV.of("bob", 1L));
从文件中读取
Apache Beam的原则之一是从任何地方读取数据,所以让我们在实践中看看如何使用一个文本文件作为数据源。
下面的例子将读取一个 "word.txt "的内容,内容是 "高级统一编程模型"。然后转换函数将返回一个包含文本中每个单词的PCollection:
PCollection<String> input =
pipeline.apply(TextIO.read().from("./src/main/resources/words.txt"));
PCollection<String> output = input.apply(
FlatMapElements.into(TypeDescriptors.strings())
.via((String line) -> Arrays.asList(line.split(" ")))
);
PAssert.that(output)
.containsInAnyOrder("An", "advanced", "unified", "programming", "model");
pipeline.run();
将输出写入文件
正如在前面的输入例子中看到的,Apache Beam有多个内置的输出连接器。在下面的例子中,我们将计算文本文件 "words.txt "中存在的每个单词的数量,该文件只包含一个句子("高级统一编程模型"),输出将以文本文件格式持久化:
PCollection<String> input =
pipeline.apply(TextIO.read().from("./src/main/resources/words.txt"));
PCollection<KV<String, Long>> output = input
.apply(
FlatMapElements.into(TypeDescriptors.strings())
.via((String line) -> Arrays.asList(line.split(" ")))
)
.apply(Count.<String>perElement());;
PAssert.that(output)
.containsInAnyOrder(
KV.of("An", 1L),
KV.of("advanced", 1L),
KV.of("unified", 1L),
KV.of("programming", 1L),
KV.of("model", 1L)
);
output
.apply(
MapElements.into(TypeDescriptors.strings())
.via((KV<String, Long> kv) -> kv.getKey() + " " + kv.getValue()))
.apply(TextIO.write().to("./src/main/resources/wordscount"));
pipeline.run();
甚至在默认情况下,文件的编写也是针对并行性进行优化的,这意味着Beam将确定最佳的分片(文件)数量来持久化结果。这些文件将位于src/main/resources文件夹下,其前缀为 "wordcount"、分片号和最后一次输出转换中定义的分片总数。
在我的笔记本电脑上运行时,生成了四个分片。
第一个碎片(文件名:wordcount-00001-of-00003):
An 1
advanced 1
第二个碎片(文件名:wordcount-00002-of-00003):
unified 1
model 1
第三个碎片(文件名:wordcount-00003-of-00003):
programming 1
最后一个分片被创建,但最后是空的,因为所有的词都已经被处理了。
扩展Apache Beam
我们可以通过编写一个自定义的转换函数来利用Beam的扩展性。一个自定义的转换函数将提高代码的可维护性,因为它将消除重复。
基本上我们需要创建一个PTransform的子类,将输入和输出的类型说明为Java Generics。然后我们覆盖expand方法,在其内容中放置重复的逻辑,接收一个字符串并返回一个包含每个单词的PCollection:
public class WordsFileParser extends PTransform<PCollection<String>, PCollection<String>> {
@Override
public PCollection<String> expand(PCollection<String> input) {
return input
.apply(FlatMapElements.into(TypeDescriptors.strings())
.via((String line) -> Arrays.asList(line.split(" ")))
);
}
}
重构后的测试场景使用WordsFileParser,现在变成了:
public class FileIOTest {
@Rule
public final transient TestPipeline pipeline = TestPipeline.create();
@Test
public void testReadInputFromFile() {
PCollection<String> input =
pipeline.apply(TextIO.read().from("./src/main/resources/words.txt"));
PCollection<String> output = input.apply(
new WordsFileParser()
);
PAssert.that(output)
.containsInAnyOrder("An", "advanced", "unified", "programming", "model");
pipeline.run();
}
@Test
public void testWriteOutputToFile() {
PCollection<String> input =
pipeline.apply(TextIO.read().from("./src/main/resources/words.txt"));
PCollection<KV<String, Long>> output = input
.apply(new WordsFileParser())
.apply(Count.<String>perElement());
PAssert.that(output)
.containsInAnyOrder(
KV.of("An", 1L),
KV.of("advanced", 1L),
KV.of("unified", 1L),
KV.of("programming", 1L),
KV.of("model", 1L)
);
output
.apply(
MapElements.into(TypeDescriptors.strings())
.via((KV<String, Long> kv) -> kv.getKey() + " " + kv.getValue()))
.apply(TextIO.write().to ("./src/main/resources/wordscount"));
pipeline.run();
}
}
结果是一个更清晰、更模块化的管道。
窗口处理
/filters:no_upscale()/articles/apache-beam-intro/en/resources/1pasted%20image%200-3-1654188064991.jpeg)
Apache Beam中的窗口处理 (Frances Perry & Tyler Akidau)
流媒体处理中的一个常见问题是按一定的时间间隔对传入的数据进行分组,特别是在处理大量数据的时候。在这种情况下,按小时或按天分析汇总的数据比分析数据集的每个元素更有意义。
在下面的例子中,假设我们在一家金融科技公司工作,我们正在接收包含交易金额和交易发生时间的交易事件,我们想检索每天交易的总金额。
Beam提供了一种方法,可以用一个时间戳来装饰每个PCollection元素。我们可以用它来创建一个代表5个货币交易的PCollection:
- 10和20的金额是在2022-02-01转出的
- 30、40和50的金额是在2022-02-05转出的。
PCollection<Integer> transactions =
pipeline.apply(
Create.timestamped(
TimestampedValue.of(10, Instant.parse("2022-02-01T00:00:00+00:00")),
TimestampedValue.of(20, Instant.parse("2022-02-01T00:00:00+00:00")),
TimestampedValue.of(30, Instant.parse("2022-02-05T00:00:00+00:00")),
TimestampedValue.of(40, Instant.parse("2022-02-05T00:00:00+00:00")),
TimestampedValue.of(50, Instant.parse("2022-02-05T00:00:00+00:00"))
)
);
接下来,我们将应用两个转换函数:
- 使用一天的窗口对交易进行分组
- 对每组中的金额进行加总
PCollection<Integer> output =
Transactions
.apply(Window.into(FixedWindows.of(Duration.standardDays(1))))
.apply(Combine.globally(Sum.ofIntegers()).withoutDefaults());
在第一个窗口(2022-02-01)中,预计总金额为30(10+20),而在第二个窗口(2022-02-05)中我们应该看到总金额为120(30+40+50):
PAssert.that(output)
.inWindow(new IntervalWindow(
Instant.parse("2022-02-01T00:00:00+00:00"),
Instant.parse("2022-02-02T00:00:00+00:00")))
.containsInAnyOrder(30);
PAssert.that(output)
.inWindow(new IntervalWindow(
Instant.parse("2022-02-05T00:00:00+00:00"),
Instant.parse("2022-02-06T00:00:00+00:00")))
.containsInAnyOrder(120);
每个IntervalWindow实例都需要与所选择的持续时间的精确开始和结束时间戳相匹配,所以所选择的时间必须是 "00:00:00"。
总结
Apache Beam是一个强大的、经过战斗考验的数据框架,允许批处理和流处理。我们使用Java SDK来构建map、reduce、group、windowing和其他操作。
Apache Beam可以很好地适用于那些从事令人尴尬的并行任务的开发人员,以简化大规模数据处理的机制。
它的连接器、SDK和对各种运行器的支持带来了灵活性,通过选择像Google Cloud Dataflow这样的云原生运行器,你可以获得计算资源的自动化管理。