chapter7 流计算系统Storm
批处理:处理的输入数据是静态的,即输入数据在计算开始前已确定
流计算:处理的输入数据是动态的,即输入数据在计算开始后才逐步到达
流数据:大量、快速、时变并持续到达的数据
Storm 是开源的分布式流计算系统,用于支持实时计算。
1 设计思想
与MapReduce、Spark等批处理系统不同,Storm流计算系统要处理的数据以流(Stream)的形式存在,理论上是无界的,且计算需持续进行。
Storm系统将流数据抽象为无界的元组(Tuple)序列,并使用拓扑(Topology)来抽象计算过程。
1.1 连续处理
短时运行 🆚 长期驻留:
- 批处理系统中负责执行计算的任务是短时运行的
- 流计算系统中负责执行计算任务的线程或进程长期驻留在系统中,因为数据以流的形式存在且理论上是无界的
连续处理
:执行流计算的一种直观方式。输入的流数据记录不断地进入系统,计算任务长期驻留并且更新自身的状态。
状态
:一种特殊的数据,用于保存从流计算开始到目前为止得到的计算结果
Why 要有状态?
输入的数据以流的形式存在,若发生故障,原数据无法再获取。
MapReduce和Spark中数据是静态的,若发生故障,最差也可用原数据重新运行。
1.2 数据模型:Tuple
Storm将流数据视为是一个无界的、连续的元组序列。
- 一个元组就是系统处理的一条记录,每一条记录(元组)包含若干个字段。
🆚 数据模型的比较
- Storm的数据模型为元组 Tuple,系统处理的一条记录就是一个 Tuple
- 字段角度,一个记录拥有多个字段
- MapReduce的数据模型为键值对 Key-value,系统处理的一条记录就是一个 Key-value
- 字段角度:一个记录仅拥有两个字段:key、value
- Spark的数据模型为RDD,根据一组记录进行建模
- Storm和MapReduce的数据模型都根据一条记录进行建模
1.3 计算模型
逻辑计算模型
Storm使用拓扑描述计算过程,逻辑上是由Spout和Bolts组成的DAG。
- 顶点:Spout或Bolt(描述数据处理逻辑)
- Spout:流数据的源头,负责从外部数据源读取数据,封装成Tuple发送给Bolt
- Bolt:描述流数据的转换过程,将处理后的Tuple作为新的流数据发送给其他Bolt
- 边:Bolt订阅的流数据(描述数据流动的方向)
- 当Spout或Bolt发送元组时,会把元组发送到每个订阅了该流数据的Bolt上进行处理
物理计算模型
Spout/Bolt 物理上由若干任务(Task)实现,Storm需要相应的线程来运行任务。

2 体系架构
2.1 架构图
采用主从架构,主要工作部件:Nimbus、Supervisor和Worker、ZooKeeper。
Nimbus位于主节点,Supervisor和Worker位于从节点,主从节点之间的协调和控制依赖于ZooKeeper。
- Nimbus:主节点运行的后台程序,负责分发代码、分配任务和监测故障
- Supervisor:从节点运行的后台程序
- 负责监听所在机器的工作,根据Nimbus分配的任务来决定启动或停止Worker进程
- 一个从节点上同时运行若干个Worker进程
- Zookeeper:负责Nimbus和Supervisor之间的所有协调工作
- 若Nimbus或Supervisor进程意外终止,重启时能读取、恢复之前的状态并继续工作
- Worker:运行一个或多个 Executor 线程,从而实际执行任务
- Executor:产生于worker进程内部的线程,会执行同一个组件的一个或多个task
- Task:执行数据处理的代码实例(Spout/Bolt)

Storm 🆚 MapReduce1.0 🆚 standalone模式的Spark:
- 系统进程角度:三者很相似,均由主节点负责整个系统管理的进程、从节点负责该节点管理的进程and负责任务执行的进程构成。只是不同系统使用了不同的名称。
- 任务执行角度:MapReduce采用多进程执行模型,Child进程执行任务代码Task;Spark采用多线程执行模型,Task任务代码同时以工作线程的形式存在;Storm采用多线程执行模型,Executor作为工作线程,在Task中仅实现任务代码。
- 基础接口角度:MapReduce仅提供Map、Reduce两个编程接口;Spark提供基于RDD的编程接口。Storm提供Spout、Bolt两个编程接口。

2.2 应用程序执行流程
- 用户编写的Topology程序,经过序列化、打包并提交给主节点Nimbus
- Nimbus创建一个组件与物理节点的对应关系文件,将该文件原子地写入Zookeeper中某Znode
- 所有Supervisor监听Znode来得到通知从而获取所在节点所需执行的组件任务(Task)
- Supervisor从Nimbus处拉取可执行的代码
- Supervisor启动若干Worker进程执行具体的任务
- Worker进程根据ZooKeeper中获取的文件信息, 启动若干个Executor线程,该线程负责执行组件 (Spout或Bolt)所描述的任务(Task)

3 工作原理
Spout负责数据的输入,Bolt负责数据的转换和输出。
Storm运行拓扑程序时,一个Spout/Bolt通常由多个任务Task同时执行。Spout和Bolt之间/不同的Bolt之间的 Tuple传输表现为属于上游组件的task和属于下游组件的task之间的Tuple传输
Task之间的Tuple传输的问题:
- 对于流数据(一组Tuple)来说,上游组件的task发送哪些Tuple给下游组件的task?
- 如何对这组Tuple进行划分?
- 对于一条Tuple来说,上游组件的task如何向下游组件的task传递Tuple?
- 立即传输还是等待多条Tuple成批次传输?
3.1 流数据分组策略
定义了两个有订阅关系的组件间(如Spout和Bolt之间,或者不同的Bolt之间)进行Tuple传输的方式。
组件在物理上由若干任务实现,上游的任务可能属于Spout/Bolt组件,下游任务必然属于Bolt组件。
- 对于上游的组件来说,流数据的分组策略定义了:属于该组件的多个任务发送哪些Tuple给下游组件的任务
- 对于下游的组件来说,流数据的分组策略定义了:下游组件的多个任务之间对于元组的划分策略
常见的流数据分组策略
(Stream Groupings):
- 随机分组(Shuffle Grouping):随机分发Tuple,保证每个Bolt的Task接收Tuple数量大致一致
- 按照字段分组(Fields Grouping):保证指定字段相同的Tuple分配到同一个Task中
- 广播分组(All Grouping):每一个Task都会收到所有的Tuple
- 全局分组(Global Grouping):所有的Tuple都发送到同一个Task中
- 直接分组(Direct Grouping):直接指定由某个Task来执行Tuple的处理
- 本地或随机分组(Local or Shuffle Grouping):若下游组件有一个或多个任务与上游组件的任务在同一Worker进程中,则Tuple被随机分发到该进程的任务中。否则,此策略与随机分组策略相同。

MapReduce中Shuffle采用的按键划分键值对,与Storm中按字段分组的流数据分组策略达到的效果类似
3.2 元组传递方式
Storm采用一次一元组或一次一记录
的消息传递机制。
- 一旦上游组件的任务处理完一条元组,就立即发送给下游组件的任务,且一次发送一条元组
- 对于连续两条元组,上游组件的任务处理了第一条元组就可立即发送给下游组件的任务,而不必等待下一条元组的处理结果
- 这种立即发送的消息传递机制有利于减少处理的延迟,从而满足实时性需求
流计算 🆚 批处理:
- 流计算:一次一元组;数据传输立即进行,无阻塞
- 批处理:数据传输成块进行;MapReduce和Spark的Shuffle阶段存在阻塞
4 容错机制
- 主节点故障:Nimbus进程故障:重启。
- 但为了保证主节点的高可用,Storm可类似MapReduce、Spark,配置一个Nimbus列表(元信息存储在所有主备Nimbus或外部可靠的分布式存储系统e.g.,HDFS),一旦主Nimbus出故障,系统就在若干备Nimbus中选一个作为新的主Nimbus。
- 从节点故障:
- Supervisor故障
- 判断该节点能否重启Supervisor,若能则重新启动;
- 若不能则启动新的Supervisor进程,原来监控的所有Worker重新调度、启动。
- Worker故障:重新启动。
- Supervisor与Worker同时故障:Nimbus命令其他节点的Supervisor启动Worker进程。
- Supervisor故障
- ZooKeeper故障:ZooKeeper本身具有容错机制,可认为该部件是可靠的。极端情况下,ZooKeeper不可用将导致Storm系统无法正常工作
4.1 容错语义
简单重启Worker进程并不意味着能保证容错,容错保障还要考虑语义的正确性。
流计算系统的容错语义
:
- 至多一次(At Most Once):消息可能会丢失
- 消息至多收到一次,意味着消息可能丢失
- 至少一次(At Last Once):消息不会丢失,可能会重复
- 消息至少收到一次,意味着消息不会丢失但可能重复
- 准确一次(Exactly Once):消息不丢失,不重复

可见,重启故障的Worker,容错语义级别为至多一次。
Storm采用的容错策略:
- Storm将Spout发出的每条源元组(Spout Tuple)及其后续衍生的Tuple视为一颗
元组树
,若元组树中所有Tuple均由系统成功输出,则源元组得到成功处理。 - 在运行拓扑的过程中,Storm使用
ACK机制
对元组树中的Tuple进行确认,一旦元组树中某一Tuple因故障无法得到确认,则系统从Spout将源元组重放
。
—— Spout所在线程无故障的前提下,此容错策略达到至少一次的容错语义。
4.2 元组树
Topology中的Spout会源源不断地发射出Tuple,每一条Tuple都会由后续的Bolt处理并演化为新的Tuple。
元组树
:Spout发出的Tuple及其衍生出来的Tuple抽象为一棵树
- Spout中每一条元组都对应一棵元组树
Spout-Tuple-id(STid)
:Spout中发射Tuple时用户可以为其指定标识
- 该 STid 伴随 Tuple 在处理过程中的演化
- 若该 Tuple 经 Bolt 处理产生多个 Tuple,则 STid 会绑定到产生的所有 Tuple 上
- STid 由用户程序指定,可将多个 Tuple 绑定相同 STid

4.3 ACK机制
Mid
:元组树中Tuple传输物理表现为组件Task之间的消息传输,消息有64位标识Mid。
Mid 🆚 STid
Mid是系统定义的消息传递标识,每条消息的Mid都不同。
STid是用户定义的标识,同一元组树的STid都相同。
Storm里面有一类特殊的Task作为Acker
,负责跟踪Spout发出的元组及其元组树。
ACK机制:
- 上游组件的Task发射消息的同时,会向 Acker 报告 Mid、STid
- 当下游组件的Task接收到消息时,向 Acker 报告 Mid、STid
最简单的消息确认机制:Acker记录上游组件报告的Mid、STid,根据下游组件报告的Mid、STid判断STid对应的元组树中标号为Mid的消息对应的Tuple是否成功传输。
但元组树中Tuple很多,且可能并行向Acker报告Mid和STid,
则Acker端需要维护类似于<STid, list<Mid>>的映射表,维护List对于Acker来说内存开销太大
Acker的数据结构:<STid, ack_val> 映射表
- 收到Spout发来消息时将相应 STid 的 ack_val 初始化为0
- Acker接收上下游组件消息时,将 Mid 与映射表对应 STid 的 ack_val 做异或操作
- 若Acker在设定的时间范围内收到处于拓扑最末端的Bolt报告并且ack_val为0,则Acker会告知相应的Spout:STid对应的元组树已成功处理完

4.4 消息重放
Storm认为消息的传输发生故障
- Acker在设定时间范围内未收到拓扑最末端Bolt发来的关于STid的确认消息 or STid对应的ack_val不为0。
Spout重新发送以STid为标识的元组
- 但这种消息重放机制可能会导致消息的重复计算,达到的是至少一次的容错语义级别

本文作者:Joey-Wang
本文链接:https://www.cnblogs.com/joey-wang/p/15789725.html
版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 2.5 中国大陆许可协议进行许可。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步