在这篇博客中,我将展示如何在事件驱动架构中聚合事件数据,并利用 Apache Flink 作为引擎来达到我们想要的结果。我们将只在本地环境中搭建项目并专注于此,而不涉及将它部署到生产环境的复杂过程。
介绍在一些系统中,一个真实的用例是需要汇总一些相互关联事件的数据。假设我们有一个系统会发送给我们所有带有ID的日志条目,但是这个系统在某项交易的不同阶段发送不同的日志条目。那么,如果我们想实时了解这笔交易的状态,应该怎么做?
为了简单起见,我们接收整个系统的日志:
- 一条日志是:"id: {id}, action: received, timestamp"
。
- 另一条日志是:"id: {id}, stage: processing"
。
- 第三条日志可能是:"id: {id}, product: pen"
。
- 第四条日志是:"transactionId: {id}, status: success"
。
有时可能会出现第五条日志来代替第四条日志,格式为:"id: {id}, status: failed"
。
在这种情况下,聚合事件就是拥有相同transactionId
但包含所有键的新事件。它可以表示为如下:
“id: {id}, 阶段:处理中, 操作:已接收, 状态:成功, 产品:笔”
在上述例子中,系统中的不同部分在流程的不同阶段发送日志信息,但我们需要了解整个交易的完整状态。在很多情况下,最好的解决方案可以是将最后几步的信息传递到当前步骤,提供足够的元数据来评估从开始到成功的全过程——一种“上下文信息”。
但如果是这样的情况,日志系统完全独立于实际系统,我们无法对其进行任何修改?又或者它是来自另一个供应商的产品,我们无法对其进行管理,并且我们仅拥有输入?这时,Apache Flink 就能大显身手了。
我对 Apache Flink 还不太熟悉,但从读了相关文档之后,我能看出它正好可以帮助我们实现所需的功能。让我们来看看为什么。
Apache Flink 是什么呢?Apache Flink 是一个用于无界和有界数据流的状态处理的分布式处理引擎。本质上,它提供了无界数据流的状态处理能力。
- 无界流式:表示源源不断进入系统的日志,可能不会停止。
- 有状态的:指的是业务逻辑需要我们对事件进行聚合。
作为一个数学家,我会以一种通用的方式,描述我们正试图解决的问题。
- 我们有这种格式的事件:{id: id, k1: val1, k2: val2} —— 其中每个 ki 是可选的,但 id 是必需的。
- 有时我们收到的输入是
{id: id, k1: val1}
,有时是{id: id, k1: val1, k3: val3}
。 - 每个事件 Ei 的形式是:_{id: id, k_i1: val_i1, k_i2: val_i2, …, k_in: valin},其中 {i1…in} 是一组不同的整数。
- 一个 结构 S 的形式是:{kj1, kj2, …, kjn},其中 {j1…jn} 是一组不同的索引。
我们旨在创建一个接收事件并根据模式 S 的服务。
具有相同 id 的不同事件应该被聚合在一起。这些事件将根据 ID 和模式 S 聚合成一个聚合事件。聚合事件可以从服务发送出去。
如果这段描述有点不清楚,只需记住,我们想将具有相同ID的多个日志行合并成一条日志行。
Kafka + Flink? 这是啥?让我们试着用Apache Flink来解决这个问题。Apache Flink能够从各种不同的来源读取消息,所以我们假设之前提到的日志可以解析成符合问题要求的Kafka事件。
所以我们会有两个话题要讨论。
输入话题 — 表示日志行
输出话题 — 表示聚合后的日志
以下是使用Apache Flink和Kafka来配置EventAggregatorJob
的方法。
我们需要在机器安装以下程序来运行示例:
安装一下- Java
- Kafka的安装
- Flink的安装
- Maven (马威恩)
我在用 IntelliJ,它与 Maven 的集成非常流畅,但如果你不使用这个 IDE 的话,可以运行:
mvn install
此命令用于将项目安装到本地Maven仓库。
Kafka 配置这份指南不会详细介绍如何管理和操作Kafka生态系统,不过简单提一下,为了教程的需要,我搭建了一个带有Zookeeper的本地Kafka代理,并创建了两个主题。
在代理启动后,我创建了两个主题。
# 创建输入话题:
./bin/kafka-topics -create -topic input-events -bootstrap-server localhost:9092 -partitions 1 -replication-factor 1
# 创建输出话题:
./bin/kafka-topics -create -topic output-events -bootstrap-server localhost:9092 -partitions 1 -replication-factor 1
我用Kcat cli来监控这些话题,这在演示中表现得很不错。
生成示例事件使用Kafka生产者将示例事件生产到input-events
主题,并看看output-events
主题中的聚合结果。
为了方便演示的目的,我在终端上运行它非常简单。
./bin/kafka-console-producer.sh --broker-list localhost:9092 --topic input-events
该命令用于启动Kafka控制台生产者,将消息发送到特定的主题(topic)。其中,--broker-list localhost:9092
(将broker列表设置为本地主机的9092端口),用于指定Kafka代理的地址和端口;--topic input-events
用于指定发送消息的目标主题。
实际生产系统的设置更为复杂得多,但这些内容在本博客中不会深入讨论。后面我会提到一些重要的内容。
我们的Kafka设置正在运行,接下来要做什么?
写你的第一个 Flink 任务: 岗位代码这里是一个 EventAggregatorJob
类的示例:
包 demo
import org.apache.flink.api.common.serialization.SimpleStringSchema;
import org.apache.flink.api.java.utils.ParameterTool;
import org.apache.flink.streaming.api.datastream.DataStream;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
import org.apache.flink.streaming.connectors.kafka.FlinkKafkaConsumer;
import org.apache.flink.streaming.connectors.kafka.FlinkKafkaProducer;
import org.apache.flink.util.Collector;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.apache.flink.api.common.state.MapState;
import org.apache.flink.api.common.state.MapStateDescriptor;
import java.util.*;
import org.apache.flink.configuration.Configuration;
public class EventAggregatorJob {
public static void main(String[] args) throws Exception {
final ParameterTool params = ParameterTool.fromArgs(args);
final String kafkaBroker = params.getRequired("kafkaBroker");
final String inputTopic = params.getRequired("inputTopic");
final String outputTopic = params.getRequired("outputTopic");
final String requiredKeysStr = params.getRequired("requiredKeys");
// 将 requiredKeys 转换为 Set
Set<String> requiredKeys = new HashSet<>(Arrays.asList(requiredKeysStr.split(",")));
final StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
// Kafka 属性
Properties properties = new Properties();
properties.setProperty("bootstrap.servers", kafkaBroker);
properties.setProperty("group.id", "flink-event-aggregator");
// 定义 Kafka 消费者
FlinkKafkaConsumer<String> 消费者 = new FlinkKafkaConsumer<>(inputTopic, new SimpleStringSchema(), properties);
消费者.setStartFromEarliest();
// 定义 Schema
Schema schema = new Schema(requiredKeys);
// 创建 Kafka 生产者
FlinkKafkaProducer<String> 生产者 = new FlinkKafkaProducer<>(outputTopic, new SimpleStringSchema(), properties);
// 定义数据流
DataStream<String> 流 = env.addSource(消费者);
流
.map(value -> {
ObjectMapper mapper = new ObjectMapper();
return mapper.readValue(value, Event.class);
})
.keyBy(event -> event.key)
.process(new EventAggregatorFunction(schema))
.map(event -> {
ObjectMapper mapper = new ObjectMapper();
return mapper.writeValueAsString(event);
})
.addSink(生产者);
env.execute("demo.Event Aggregator Job");
}
public static class EventAggregatorFunction extends org.apache.flink.streaming.api.functions.KeyedProcessFunction<String, Event, Event> {
private final Schema schema;
private transient MapState<String, String> 聚合状态;
public EventAggregatorFunction(Schema schema) {
this.schema = schema;
}
@Override
public void open(Configuration parameters) {
MapStateDescriptor<String, String> 描述符 = new MapStateDescriptor<>("aggregatedState", String.class, String.class);
聚合状态 = getRuntimeContext().getMapState(描述符);
}
@Override
public void processElement(Event event, Context context, Collector<Event> collector) throws Exception {
for (Map.Entry<String, String> entry : event.attributes.entrySet()) {
聚合状态.put(entry.getKey(), entry.getValue());
}
聚合状态.put("key", event.key);
boolean isComplete = true;
for (String key : schema.requiredKeys) {
if (!聚合状态.contains(key)) {
isComplete = false;
break;
}
}
if (isComplete) {
Map<String, String> 完整事件Map = new HashMap<>();
for (Map.Entry<String, String> entry : 聚合状态.entries()) {
完整事件Map.put(entry.getKey(), entry.getValue());
}
String key = 完整事件Map.get("key");
完整事件Map.remove("key");
Event 完整事件 = new Event(key, 完整事件Map);
collector.collect(完整事件);
聚合状态.clear();
}
}
}
}
搭建Flink集群
./bin/start-cluster.sh
运行 Flink 任务
要运行EventAggregatorJob
,请使用以下命令:
./bin/flink run -c demo.EventAggregatorJob <path_to_jar>
--kafkaBroker localhost:9092
--inputTopic input-events
--outputTopic output-events
--requiredKeys 指定键 name,age
## 请用你的编译好的 JAR 文件路径替换 `<path_to_jar>`。
一旦任务开始运行起来,你就可以开始测试它,
在输入主题中加入事件,看看发生了什么。
输入的事件
事件输出
监控您的工作:默认,Flink 自带一个很好的 UI,你可以在 localhost:8081 查看它。
等等,啥?似乎我们只用了不到200行代码就创建了任务和相关的类,理论上能够处理实际吞吐量的Flink任务,之类的……
但这段代码中的“flink魔法”是什么?我们创建了...
通过阅读代码,你可以很容易猜出发生了什么事。
Flink Magic 简介在下面这段代码里,我们发现了Flink引擎和Flink Java SDK很多不错的地方。
简单来说,我们可以看到以下所有内容等等。
- 数据摄入: Flink 从 Kafka 读取数据流。
- 反序列化: 将 JSON 字符串转换为
Event
对象。 - 键分区: 根据事件键对流进行分区,方便本地化处理。
- 有状态处理: 使用 Flink 管理的状态(如
MapState
)聚合事件。 - 序列化: 将聚合后的事件转换回 JSON 字符串。
- 数据输出: 将处理后的数据写回 Kafka。
虽然像数据摄入、反序列化、序列化和数据输出这类事情因为我们要处理数据所以可能看起来很简单,但具有状态处理和键控分区的功能在Flink中的实现非常优雅。
Spark被广泛认为是一个强大的大数据处理框架。
然而,Spark最初是为有界数据流(例如ETL作业)设计的,
而Flink作业(比如这个例子中的)可以作为无界流运行,随时准备处理到来的事件。
说实话,我挺惊讶的,我查了下维基百科,Flink其实也不是什么新东西,该项目在2011年开始,2016年发布。
对我来说这只是一条新闻而已。
注释和要点:
如果你想在实际生产系统中使用Flink,你还有很多问题需要问自己,
如果你想将Flink投入生产,这里有4个方面在下面的小演示中没有讲到:
- 生产环境中的 Flink 集群——在理论上,如果你使用 Kubernetes,Flink 已经有一个 Kubernetes 运算符来帮助你在 Kubernetes 集群中运行 Flink 任务。
- 部署,请参阅此链接 https://nightlies.apache.org/flink/flink-docs-master/docs/deployment/overview/
请参阅https://github.com/orsahar/flink-kafka-event-aggregation
玩得开心,编程!
共同学习,写下你的评论
评论加载中...
作者其他优质文章