Storm是一个免费开源、分布式、高容错的实时计算系统。Storm令持续不断的流计算变得容易,弥补了Hadoop批处理所不能满足的实时要求。Storm经常用于在实时分析、在线机器学习、持续计算、分布式远程调用和ETL等领域。Storm的部署管理非常简单,而且,在同类的流式计算工具,Storm的性能也是非常出众的,主要有一下特点:
- 分布式系统:可横向拓展,根据需求随时添加删除节点。
- 运维简单:Storm的部署的确简单。虽然没有Mongodb的解压即用那么简单,但是它也就是多安装两个依赖库而已。
- 高度容错:模块都是无状态的,随时宕机重启。
- 无数据丢失:Storm创新性提出的ack消息追踪框架和复杂的事务性处理,能够满足很多级别的数据处理需求。不过,越高的数据处理需求,性能下降越严重。
- 多语言:Storm使用Thrift框架,因为Thrift本身就支持多语言,所以Storm自然支持多语言。
Topology
Storm可以用于3种不同场景: 事件流(stream processing),持续计算(continuous computation)以及分布式RPC (DistributedRPC)。
针对这些场景,Storm设计了自己独特的计算模型。如下图所示,Storm计算模型以Topology为单位。 一个Topology是由一系列Spout和Bolt构成的图。消息传递的基本单元Tuple会在构成Topology的Spout和Bolt之间流动。Spout负责产生Tuple,而Bolt负责处理对接收到的Tuple进行各种处理,包括过滤、函数操作、合并、写数据库等操作,得出需要的计算结果。Bolt可以级联,也可以外发送Tuple。
Stream Grouping控制着Tuple在Topology中如何流动。如下图所示的Topology中,Spout有2个实例,Bolt A,B, C 分别有4,3,2个实例。Bolt A的实例之一向外送Tuple时,Stormruntime将按照用户创建Topology时指定的Stream Grouping策略把Tuple发送到Bolt B特定的实例。Storm提供了多种Stream Grouping的实现,比如Shuffle Grouping,Shuffle Grouping可以保证Tuple在Bolt实例间随机分布,每个实例都收到相同数量的Tuple。
Storm架构
Storm的总体架构非常简洁和优雅。在一个Storm集群中,有主从两种不同的节点,三种不同的daemon:Nimbus运行在主节点上,通关全局;从节点上运行Supervisor,管理相关节点上的任务;每个从节点上还有一系列的worker process来运行具体任务。和我们熟知的Hadoop不一样的是, 这些daemon间并不直接发送心跳信息或者存在其他RPC控制协议。如下图所示,他们之间的信息交换统统是通过Zookeeper来实现。这样的设计虽然引入了Zookeeper这个第三方依赖,但其极大的简化了Nimbus/Supervisor/Worker本身的设计,考虑到Zookeeper已经被广泛接受,已经成为分布式系统metadata store的事实解决方案,Storm在设计时所做的这个折中相当不错。
作为主节点, Nimbus类似于Hadoop中的Jobtracker,主要负责接收客户端提交的Topology,进行相应的验证,分配任务,进而把任务相关的元信息写入Zookeeper相应目录。此外,Nimbus还负责通过Zookeeper来监控任务执行情况。而Supervisor则类似于TaskTracker,负责监听任务分配情况,根据实际情况启动/停止工作进程(worker)。相应的,Worker和Hadoop中的map/reduce task很类似,实际的数据处理发生在这里。 不同的是,map/reduce task 终究会结束,但worker则会一直执行下去。同样,Storm引入了WorkerSlot的概念,也就是说,slave节点上的worker的数量是有限的。
相比于s4, puma等其他实时计算系统,Storm最大的亮点在于其记录级容错和能够保证消息精确处理的事务功能。下面就重点来看一下这两个亮点的实现原理。
首先来看一下什么叫做记录级容错?Storm允许用户在Spout中产生一个新的源Tuple时为其指定一个message id, 这个message id可以是任意的object对象。多个源Tuple可以共用一个message id,表示这多个源 Tuple对用户来说是同一个消息单元。Storm中记录级容错的意思是说,Storm会告知用户每一个消息单元是否在指定时间内被完全处理了。完全处理呢就是该message id绑定的源Tuple及由该源Tuple后续生成的Tuple经过了Topology中每一个应该到达的Bolt的处理。举个例子,在下图中,在Spout由message 1绑定的tuple1和tuple2经过了bolt1和bolt2的处理生成两个新的Tuple,并最终都流向了bolt3。当这个过程完成处理完时,称message 1被完全处理了。
在Storm的Topology中有一个系统级组件,叫做acker。这个acker的任务就是追踪从Spout中流出来的每一个message id绑定的若干Tuple的处理路径。如果在用户设置的最大超时时间内这些Tuple没有被完全处理,那么acker就会告知Spout该消息处理失败了,相反则会告知Spout该消息处理成功了。Storm使用了一种非常巧妙的方法来检查一个Tuple是否经过了所有路径,其利用了以下数学原理:
A xor B…xor B xor A = 0,其中每一个操作数出现且仅出现两次。具体过程是这样的:在Spout中系统会为用户指定的message id生成一个对应的64位整数,作为一个root id。root id会传递给acker及后续的Bolt作为该消息单元的唯一标识。同时,无论是Spout还是Bolt每次新生成一个Tuple的时候,都会赋予该Tuple一个64位的整数的id。Spout发射完某个message id对应的源Tuple之后,会告知acker自己发射的root id及生成的那些源Tuple的id。而Bolt每次接受到一个输入Tuple处理完之后,也会告知acker自己处理的输入tuple的id及新生成的那些Tuple的id。Acker只需要对这些id做一个简单的异或运算,就能判断出该root id对应的消息单元是否处理完成了。下面通过一个图示来说明这个过程。
1. spout中绑定message 1生成了两个源tuple,id分别是0010和1011。
2. bolt1处理tuple 0010时生成了一个新的tuple,id为0110。
3. bolt2处理tuple 1011时生成了一个新的tuple,id为0111
4. bolt3中接收到tuple 0110和tuple 0111,没有生成新的tuple
Storm中每个Tuple会被处理多次,为了保证每一个Tuple至少被处理一次,Storm引入了Trident的概念。Trident是基于Storm的一种实时计算的高度抽象,允许进行高并发、有状态的流式计算的同时,也支持低延迟的分布式查询。Trident与Pig、Cascading这种高级批量处理工具在概念和思想上有很多相似之处。Trident提供了 joins, aggregations, grouping, functions, 以及 filters等操作。除此之外,Trident 还提供了一些专门的原语,从而在基于数据库或者其他存储的前提下来应付有状态的递增式处理。
Trident
在实时计算领域的一个主要问题就是怎么样来管理状态并能轻松应对错误和重试。消除错误的影响是非常重要的,因为当一个节点死掉,或者一些其他的问题出现时,那行batch需要被重新处理。问题是如何做状态更新来保证每一个消息被处理且只被处理一次。
Trident通过做下面两件事情来解决这个问题:(1)每一个batch被赋予一个唯一标识transaction id。如果一个batch被重试,它将会拥有和之前同样的transaction id。(2)状态更新是以batch为单位有序进行的。也就是说,batch 3的状态更新必须等到batch 2的状态更新成功之后才可以进行。
Trident的topology会被编译成尽可能高效的Storm topology。只有在需要对数据进行repartition的时候(如groupby或者shuffle)才会把Tuple通过network发送出去,如果你有一个trident如下:
它将会被编译成如下的Storm Topology: