本文作者:倪泽,Apache RocketMQ committer、RSQLDB/RocketMQ Streams Maintainer

01 背景

RocketMQ Streams 1.1.0版本已于近期发布,相对之前版本有以下改进和优化:

1、API层面支持泛型,可自定义输入输出数据;

2、去掉冗余逻辑,简化代码,重写拓扑图构建和数据处理过程;

本文章承接上篇:RocketMQ Streams 1.1.0: 轻量级流处理再出发,从实现原理上介绍RocketMQ Streams是如何实现流计算拓扑图构建的以及探讨了数据流转过程和流转过程中的状态变化。

02 流处理拓扑构建过程

public class example {
public static void main(String[] args) {
StreamBuilder builder = new StreamBuilder("wordCount"); builder.source("sourceTopic", total -> {
String value = new String(total, StandardCharsets.UTF_8);
return new Pair<>(null, value);
})
.flatMap((ValueMapperAction<String, List<String>>) value -> {
String[] splits = value.toLowerCase().split("\W+");
return Arrays.asList(splits);
})
.keyBy(value -> value)
.count()
.toRStream()
.print(); TopologyBuilder topologyBuilder = builder.build(); Properties properties = new Properties();
properties.put(MixAll.NAMESRV_ADDR_PROPERTY, "127.0.0.1:9876"); RocketMQStream rocketMQStream = new RocketMQStream(topologyBuilder, properties); Runtime.getRuntime().addShutdownHook(new Thread("wordcount-shutdown-hook") {
@Override
public void run() {
rocketMQStream.stop();
}
}); rocketMQStream.start();
}
}

在使用者书写上述及连表达式时,发生第一次构建,即逻辑节点的添加,前后算子具有父子关系,构建后形成逻辑节点,多个逻辑节点形成链表。

逻辑构建结束后,调用StreamBuilder#build()方法进行第二次构建,将逻辑节点中可能包含的多个真实节点添加拓扑,形成处理拓扑图。

经过两次两次构建后,处理拓扑已经完整。但是为了区分不同topic使用不同拓扑处理,需要在数据来临前的重平衡阶段,创建真实数据处理节点,这是第三次构建。

逻辑构建(第一次构建)

逻辑节点本身不包括实际操作,但是可由逻辑节点继续构建出实际节点,一个逻辑节点可能包含一实际节点,也可能包含多个实际节点,例如count逻辑算子不仅仅包含累加这个实际操作,累加前需要对相同key的数据路由到同一计算实例上,因此还需要包含sink、source两个实际节点,但是这些只会在构建实际节点时体现出来,不会在添加逻辑节点阶段体现。

每个逻辑节点都是GraphNode的子类,构建时,将子节点算子加入父节点child集合中,将父节点加入子节点parent集合中。这个构建过程中使用Pipeline均为同一个实例。最终在Pipeline中,形成以root节点为根节点的链表。

添加逻辑节点逻辑:

@Override
public <OUT> GroupedStream<K, OUT> map(ValueMapperAction<V, OUT> mapperAction) {
//1、确定节点名称 //2、实现Supplier类,实现数据处理逻辑 //3、实例化逻辑节点类GraphNode //4、将逻辑节点GraphNode添加到pipeline中形成链表
}

可以看到逻辑节点的添加非常通用,实现不同功能的算子,只需要实现算子对应的数据实际处理逻辑即可,如将新增算子形成拓扑图等等后续工作完全不用关心,降低了新算子开发的门槛。

在逻辑节点的构建过程中,有两类比较特殊的算子,一个是实现数据分组的shuffle算子,一个是实现双流聚合的Join算子。

shuffle逻辑算子的功能是将含有相同key的数据发送到同一个队列中,方便后续算子对相同key的数据进行统计。他通常是keyBy后面紧跟的算子,例如keyBy("年级").count(),那么count就是一个shuffle算子类型。shuffle逻辑算子包含三个实际处理过程:

  • 将数据按照Key的hash%queueNum发送到对应队列;
  • 从RocketMQ中拉取上述数据到本地;
  • 按照shuffle节点中定义的逻辑进行处理,例如累加。

Join算子的功能是实现双流聚合,将两个数据流聚合成一个。

Join拓扑图

在左流和右流上添加KeyBy算子,对左流和右流进行分别过滤;之后在左流和右流上分别添加标签节点,标记此数据是左流还是右流,之后将两个标签节点,指向一个共同的Join节点,数据在此完成汇聚,按照使用者给定的ValueJoinAction节点处理。

Join使用方式:

StreamBuilder builder = new StreamBuilder(jobId);

RStream<T> leftStream = builder.source(...);
RStream<V> rightStream = builder.source(...); ValueJoinAction<T, V, R> action = new ValueJoinAction<T, V, R>(){...}; leftStream.join(rightStream)
.where(左流字段)
.equalTo(右流字段)
.apply(action)
.print();

Join实现伪代码:

//左右流按照各自字段分组,含有相同key的字段会被回写到同一个队列里面;
GroupedStream<K, V1> leftGroupedStream = leftStream.keyBy(左流字段);
//因为后面左右流数据会在一起处理,为了区分数据来源,在数据中添加标记是左流还是右流
leftGroupedStream.addGraphNode(addTag);
//获取leftGroupedStream最后的逻辑节点
GraphNode leftLast = leftGroupedStream.getLast(); GroupedStream<K, V1> rightGroupedStream = leftStream.keyBy(右流字段);
rightGroupedStream.addGraphNode(addTag);
GraphNode rightLast = rightGroupedStream.getLast(); //数据汇聚节点
ProcessorNode<OUT> commChild = new ProcessorNode(name, temp, “聚合数据实际操作”);
commChild.addParent(leftLast);
commChild.addParent(rightLast); //统一数据流
RStreamImpl commRStream = new RStreamImpl<>(Pipeline, commChild);
//继续在统一数据流上操作
commRStream...

实际构建(第二次构建)

构建逻辑节点完毕后,从ROOT节点开始遍历,调用GraphNode逻辑节点addRealNode方法,构建真实节点工厂类。

在第二次构建实际节点过程中,会对逻辑节点进行拆解,对于大多数逻辑节点,只需要构建一个实际节点,但是对于某些特殊的逻辑节点需要构建多个实际节点才能与之对应,例如shuffle类型逻辑节点,他需要包含三个实际节点:发送数据节点、消费数据节点、处理数据节点。shuffle类型逻辑节点父节点必须是GroupBy,例如上图所示的count是shuffle节点,Window节点也可以是逻辑节点。

第二次构建并不会直接生成处理数据的Processor,而是产生ProcessorFactory对象。为什么不生成直接能处理数据的Processor对象呢?因为一个RocketMQ Streams实例需要同时拉取不同队列进行流计算,为了能将不同队列的流计算过程区别开,针对每个队列会由独立的Processor实例进行处理,因此第二次构建仅仅构建出ProcessorFactory,在重平衡确定流处理实例要拉去哪些队列后,再由ProcessorFactory实例化Processor。

第三次构建

客户程序依赖RocketMQ Streams获得流计算能力,因此客户程序本质上是就是一个RocketMQ Client(见方案架构图)。在RocketMQ Client发生重平衡时,会将RocketMQ Server所包含的队列在客户端中重新分配,第三次构建,也就是由ProcessorFactory实例化Processor,就发生在重平衡发生后,拉取数据前。第三次真实的构建出了处理数据的Processor,并将子节点Processor添加进入父节点Processor中。

03数据处理过程

状态恢复

流处理过程中产生的计算状态保存、恢复涉及到流处理过程的正确性。在流处理实例宕机的情况下,该流处理实例上消费的队列会被重平衡到其他流处理实例上。如果对该队列进行了有状态计算,那么产生的状态也需要在新的流计算实例上恢复。如上图中,Instance1宕机,他消费的MQ2和MQ3被分别迁移到Instance2和Instance3上,MQ2和MQ3对应的状态(紫色和蓝色)也需要在Instance2和Instance3上恢复出来。

  • 存储介质

使用本地RocksDB,远程RocketMQ的组合,作为状态存储介质。流计算在计算状态时,RocksDB在使用有限内存情况下,作为状态的临时存储,用于算子交互,在计算结束后提交消费位点时将本次计算产生的状态一并写入RocketMQ中。消费位点提交、计算结果写出、状态保存需要保持原子状态,这一内容在后面流计算正确性中讨论。

  • 状态持久化存储

RocketMQ作为消息临时存储,存在数据最大过期时间,一旦过期后,数据会被删除。但是状态存储介质本质上是以KV方式存储数据,不希望KV数据随着时间过期而被删除。因此,使用Compact topic作为状态存储,他会对同一队列的数据按照Key对数据进行压缩,相同Key的数据只保留offset最大的一条。

//key如果决定数据被发送到某个Broker的哪个队列
int queueId = hash(key) % queueNum

但是在RocketMQ中队列数会随着Broker扩缩容而增加或者减少,扩缩Broker数量前后,相同的Key可能被发送到不同的队列,那么按照上述规则进行Compact后得到某个key所在的queueId就是错误的,使用Compact topic作为KV存储就失去了意义。

因此在状态topic是Compact topic的基础上,再将状态topic创建为Static topic(Logic Queue),即状态topic即是Compact topic也是Static topic。这样才能解耦队列数量与Broker数量,使队列数量在扩缩Broker情况下仍然不变,保证含有相同Key的数据能被发送到同一队列中。

  • 状态重放

从被迁移状态队列拉取数据到本地进行重放,需要从队列头开始消费,相同Key的数据只保留offset最大的数据,形成K-V状态对,放入本地临时存储RocksDB中;

  • 状态topic与source topic对应关系

因为状态topic中的队列会随着source topic队列迁移而迁移,保证对source topic队列中数据进行有状态处理得到正确的结果,因此在队列层面,状态topic与source topic应该是一一对应的关系。即状态topic名称与source topic名称一一对应,状态topic的队列数量等于source topic队列数量。source topic队列的流计算状态保存在状态topic的对应队列中。

数据处理

图中黑色线表示控制流,黄色线表示数据流;rebalance部分先于litePull部分被调用。

重平衡部分:

  • 根据分配到的队列,到相应状态topic的相应队列中从头拉取数据,到本地重放,获得KV状态对,放入到本地RocksDB中。
  • 根据数据源topic,构建对应的数据处理器processor(即第三次构建过程),保存起来;

数据处理部分:

  • 使用litePull模式拉取数据,可以独立控制消费位点提交;
  • 数据反序列化;
  • 使用topic查找processor;
  • 将processor的子节点保存起来(子节点在第三次构建过程中添加);
  • 数据向上下文StreamContext中传递,由他将数据路由到下游节点;
  • 数据处理前,现将下游节点的子节点保存起来供后续查找;
  • 数据处理,如果有状态算子则与RocksDB交互,如果还有下游节点则继续进入StreamContext,如果没有下游节点则结束处理。

数据每次到下游节点前,先进入StreamContext中,由它统一向下游节点传递数据。StreamContext中包含了处理数据所需要的所有信息,包括数据来源、状态存储、下游子节点等等;

StreamContext不断递归迭代,将数据向下游传递,最终数据会被拓扑图上所有节点处理,由sink节点写出结果。

04 参与贡献

RocketMQ Streams是Apache RocketMQ的子项目,已经在社区开源,并且提出了一些Good First Issue供感兴趣同学参加。参与RocketMQ Streams相关工作,请参考以下资源:

1、试用RocketMQ Streams,并阅读相关文档以了解更多信息;

maven仓库坐标:

<dependency>
<groupId>org.apache.rocketmq</groupId>
<artifactId>rocketmq-streams</artifactId>
<version>1.1.0</version>
</dependency>

RocketMQ Streams文档:

​https://rocketmq.apache.org/zh/docs/streams/01RocketMQ%20Streams%20Overview​

2、参与贡献:如果你有任何功能请求或错误报告,请随时提交 Pull Request 来分享你的反馈和想法;

社区仓库:​​https://github.com/apache/rocketmq-streams​

3、联系我们:可以在 GitHub上创建 Issue,向 RocketMQ 邮件列表发送电子邮件,或在 RocketMQ Streams SIG 交流群与专家共同探讨,RocketMQ Streams SIG加入方式:添加“小火箭”微信,回复RocketMQ Streams。

邮件列表:​​https://lists.apache.org/list.html?dev@rocketmq.apache.org​

RocketMQ Streams拓扑构建与数据处理过程的更多相关文章

  1. RocketMQ Streams 1.1.0: 轻量级流处理再出发

    本文作者:倪泽,Apache RocketMQ committer.RSQLDB/RocketMQ Streams Maintainer 01 背景 RocketMQ Streams是一款基于Rock ...

  2. Confluent Platform 3.0支持使用Kafka Streams实现实时的数据处理(最新版已经是3.1了,支持kafka0.10了)

    来自 Confluent 的 Confluent Platform 3.0 消息系统支持使用 Kafka Streams 实现实时的数据处理,这家公司也是在背后支撑 Apache Kafka 消息框架 ...

  3. 使用 Kafka 和 Spark Streaming 构建实时数据处理系统

    使用 Kafka 和 Spark Streaming 构建实时数据处理系统 来源:https://www.ibm.com/developerworks,这篇文章转载自微信里文章,正好解决了我项目中的技 ...

  4. 11g包dbms_parallel_execute在海量数据处理过程中的应用

    11g包dbms_parallel_execute在海量数据处理过程中的应用 一.1  BLOG文档结构图 一.2  前言部分 一.2.1  导读 各位技术爱好者,看完本文后,你可以掌握如下的技能,也 ...

  5. 大数据处理过程核心技术ETL详细介绍

    架构挑战 1.对现有数据库管理技术的挑战. 2.经典数据库技术并没有考虑数据的多类别(variety).SQL(结构化数据查询语言),在设计的一开始是没有考虑到非结构化数据的存储问题. 3.实时性技术 ...

  6. 使用 Kafka 和 Spark Streaming 构建实时数据处理系统(转)

    原文链接:http://www.ibm.com/developerworks/cn/opensource/os-cn-spark-practice2/index.html?ca=drs-&ut ...

  7. 关于React前端构建的一般过程 - 理论篇

    概要 本文以个人阅读实践经验归纳前端架构构建过程,以Step by Step方式说明创建一个前端项目的过程.并会对每个阶段所使用的技术进行可替代分析,如Express替换Hapi或者Koa的优缺点分析 ...

  8. TensorFlow NMT的数据处理过程

    在tensorflow/nmt项目中,训练数据和推断数据的输入使用了新的Dataset API,应该是tensorflow 1.2之后引入的API,方便数据的操作.如果你还在使用老的Queue和Coo ...

  9. MyBatis框架原理1:构建SqlSessionFactory的过程

    SqlSessionFactoryBuilder 首先创建了一个SqlSessionFactoryBuilder对象,然后调用该对象的build方法加载全局XML配置的流文件构建出一个SqlSessi ...

  10. 《Caffe下跑AlxNet之数据处理过程》

    环境:Windows 最近用Caffe跑了一下AlxNet网络,现在总结一下数据处理部分:(处理过的数据打包链接:http://pan.baidu.com/s/1sl8M5ad   密码:ph1y) ...

随机推荐

  1. Linux系统下使用pytorch多进程读取图片数据时的注意事项——DataLoader的多进程使用注意事项

    原文: PEP 703 – Making the Global Interpreter Lock Optional in CPython 相关内容: The GIL Affects Python Li ...

  2. js 实现俄罗斯方块(三)

    我又来啦!上一篇有点水,本篇我们来干货! 嘿嘿,首先我们先搭建游戏世界------网格 所有的操作包括左移右移下移旋转都是在这个网格中 既然是使用js来写当然跑不了html啦,实现网格最简单的 方法就 ...

  3. [POI2015] MOD 题解

    前言 题目链接:洛谷. 题意简述 给定一棵树,求断掉一条边再连上一条边所得的新树直径最小值和最大值,以及相应方案(你可以不进行任何操作,即断掉并连上同一条边). 题目分析 假设我们枚举断掉某一条边,得 ...

  4. 随时随地与 LLMs 聊天的开源项目「GitHub 热点速览」

    众所周知,本地运行 LLMs 需要下载模型(体积大),并且还比较吃硬件配置.近日 GitHub 推出了 GitHub Models 服务,让开发者可以在 GitHub 上免费测试 Llama.Phi ...

  5. Apache SeaTunnel 2.3.3 版本发布,CDC 支持 Schema Evolution!

    时隔两个月, Apache SeaTunnel 终于迎来大版本更新.此次发布的 2.3.3 版本在功能和性能上均有较大优化改进,其中大家期待已久的 CDC Schema evolution(DDL 变 ...

  6. JVM指令大全之不太全系列

    一.未归类系列A 此系列暂未归类. 指令码    助记符                            说明0x00         nop                           ...

  7. 微服务全链路跟踪:grpc集成jaeger

    微服务全链路跟踪:grpc集成zipkin 微服务全链路跟踪:grpc集成jaeger 微服务全链路跟踪:springcloud集成jaeger 微服务全链路跟踪:jaeger集成istio,并兼容u ...

  8. wifi基础(一):无线电波与WIFI信号干扰、衰减

    liwen01 2024.08.18 前言 无论是在产品开发还是在日常生活中,在使用无线网络的时候,都会经常遇到一些信号不好的问题,也会产生不少疑问: 为什么我们在高速移动的高铁上网络会变慢? 为什么 ...

  9. Graphics2D绘图方法总结

    一.简介 在开发中可能会遇到这样一类场景,业务复杂度不算太高,技术难度不算太深,但是做起来就很容易把人整破防,伤害很高侮辱性很强的:绘图. 绘图最怕有人挑刺:这里变形,那里不对,全图失真. 最近在处理 ...

  10. Ubuntu 安裝 VMware Workstation Pro

    安装 下载依赖: # Ubuntu 22.04 及以前 sudo apt install libaio1 # Ubuntu 24.04 及以后 sudo apt install libaio1t64 ...